From beb35acb84fe4bc5239b7af8e0a5efba60701161 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Feb 2024 14:10:23 +0100 Subject: [PATCH 001/902] feat: initial minimal shapes --- rocrate-shapes.ttl | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 rocrate-shapes.ttl diff --git a/rocrate-shapes.ttl b/rocrate-shapes.ttl new file mode 100644 index 00000000..f83aa836 --- /dev/null +++ b/rocrate-shapes.ttl @@ -0,0 +1,31 @@ +@prefix : <./> . +@prefix ex: . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . + +schema_org:CreativeWork a sh:NodeShape ; + sh:targetNode :ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "conformsTo" ; + sh:description "The RO-Crate version this metadata conforms to" ; + sh:maxCount 2; + sh:minCount 1 ; + sh:nodeKind sh:IRI ; + sh:path dct:conformsTo ; + sh:in ( ) + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "about" ; + sh:description "The main entity described in the RO-Crate" ; + sh:maxCount 2; + sh:minCount 2 ; + sh:nodeKind sh:IRI ; + sh:path schema_org:about ; + sh:hasValue <./> + ] . + From 5aba03848252be4f9de53b7607db725663e27024 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Feb 2024 14:31:57 +0100 Subject: [PATCH 002/902] feat: add utils module --- utils.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 utils.py diff --git a/utils.py b/utils.py new file mode 100644 index 00000000..73a7be51 --- /dev/null +++ b/utils.py @@ -0,0 +1,16 @@ +import os + + +def get_all_files(directory: str = '.', extension: str = '.ttl'): + # initialize an empty list to store the file paths + file_paths = [] + # iterate through the directory and subdirectories + for root, dirs, files in os.walk(directory): + # iterate through the files + for file in files: + # check if the file has a .ttl extension + if file.endswith(extension): + # append the file path to the list + file_paths.append(os.path.join(root, file)) + # return the list of file paths + return file_paths From af2822e42f3cff0ca52f6b3c54c9c97464190565 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Feb 2024 14:32:48 +0100 Subject: [PATCH 003/902] refactor: move ttl file --- rocrate-shapes.ttl => shapes/rocrate-shapes.ttl | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rocrate-shapes.ttl => shapes/rocrate-shapes.ttl (100%) diff --git a/rocrate-shapes.ttl b/shapes/rocrate-shapes.ttl similarity index 100% rename from rocrate-shapes.ttl rename to shapes/rocrate-shapes.ttl From 52191ce385ac81b9e610a7e010a19e234342e509 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Feb 2024 14:35:07 +0100 Subject: [PATCH 004/902] build: add gitignore file --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d731cf2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +**/__pycache__ \ No newline at end of file From f53ff05c9607d86e26abc0ed06e6bacf98634f59 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Feb 2024 14:48:33 +0100 Subject: [PATCH 005/902] refactor: move code to the src folder --- {shapes => src/shapes}/rocrate-shapes.ttl | 0 utils.py => src/utils.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {shapes => src/shapes}/rocrate-shapes.ttl (100%) rename utils.py => src/utils.py (100%) diff --git a/shapes/rocrate-shapes.ttl b/src/shapes/rocrate-shapes.ttl similarity index 100% rename from shapes/rocrate-shapes.ttl rename to src/shapes/rocrate-shapes.ttl diff --git a/utils.py b/src/utils.py similarity index 100% rename from utils.py rename to src/utils.py From d44fa962d1e768248c5643ec979fbd3de5a18dd2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Feb 2024 15:00:54 +0100 Subject: [PATCH 006/902] build: init poetry project --- pyproject.toml | 14 ++++++++++++++ rocrate_validator/__init__.py | 0 2 files changed, 14 insertions(+) create mode 100644 pyproject.toml create mode 100644 rocrate_validator/__init__.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..27f04959 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +name = "rocrate-validator" +version = "0.1.0" +description = "" +authors = ["Marco Enrico Piras "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/rocrate_validator/__init__.py b/rocrate_validator/__init__.py new file mode 100644 index 00000000..e69de29b From b1b020c4ccd27de2102dd9f0b1e1e27e83c22d46 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Feb 2024 15:01:44 +0100 Subject: [PATCH 007/902] build(dep): add rdflib dependency --- poetry.lock | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + 2 files changed, 67 insertions(+) create mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..37e389e9 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,66 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "isodate" +version = "0.6.1" +description = "An ISO 8601 date/time/duration parser and formatter" +optional = false +python-versions = "*" +files = [ + {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, + {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "pyparsing" +version = "3.1.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, + {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "rdflib" +version = "7.0.0" +description = "RDFLib is a Python library for working with RDF, a simple yet powerful language for representing information." +optional = false +python-versions = ">=3.8.1,<4.0.0" +files = [ + {file = "rdflib-7.0.0-py3-none-any.whl", hash = "sha256:0438920912a642c866a513de6fe8a0001bd86ef975057d6962c79ce4771687cd"}, + {file = "rdflib-7.0.0.tar.gz", hash = "sha256:9995eb8569428059b8c1affd26b25eac510d64f5043d9ce8c84e0d0036e995ae"}, +] + +[package.dependencies] +isodate = ">=0.6.0,<0.7.0" +pyparsing = ">=2.1.0,<4" + +[package.extras] +berkeleydb = ["berkeleydb (>=18.1.0,<19.0.0)"] +html = ["html5lib (>=1.0,<2.0)"] +lxml = ["lxml (>=4.3.0,<5.0.0)"] +networkx = ["networkx (>=2.0.0,<3.0.0)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "036ed7fb018709708f370a7646048203cf0fa92169d910f113cbe042cba147bf" diff --git a/pyproject.toml b/pyproject.toml index 27f04959..bb76ef54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.11" +rdflib = "^7.0.0" [build-system] From 9b71dedf6282ccff4ab010bc127e393d3f0074da Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Feb 2024 15:02:20 +0100 Subject: [PATCH 008/902] build(dep): add pyshacl dependency --- poetry.lock | 150 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 37e389e9..5da1f65a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,45 @@ # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +[[package]] +name = "html5lib" +version = "1.1" +description = "HTML parser based on the WHATWG HTML specification" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, + {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, +] + +[package.dependencies] +six = ">=1.9" +webencodings = "*" + +[package.extras] +all = ["chardet (>=2.2)", "genshi", "lxml"] +chardet = ["chardet (>=2.2)"] +genshi = ["genshi"] +lxml = ["lxml"] + +[[package]] +name = "importlib-metadata" +version = "7.0.1" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, + {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] + [[package]] name = "isodate" version = "0.6.1" @@ -14,6 +54,48 @@ files = [ [package.dependencies] six = "*" +[[package]] +name = "owlrl" +version = "6.0.2" +description = "OWL-RL and RDFS based RDF Closure inferencing for Python" +optional = false +python-versions = "*" +files = [ + {file = "owlrl-6.0.2-py3-none-any.whl", hash = "sha256:57eca06b221edbbc682376c8d42e2ddffc99f61e82c0da02e26735592f08bacc"}, + {file = "owlrl-6.0.2.tar.gz", hash = "sha256:904e3310ff4df15101475776693d2427d1f8244ee9a6a9f9e13c3c57fae90b74"}, +] + +[package.dependencies] +rdflib = ">=6.0.2" + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "prettytable" +version = "3.10.0" +description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" +optional = false +python-versions = ">=3.8" +files = [ + {file = "prettytable-3.10.0-py3-none-any.whl", hash = "sha256:6536efaf0757fdaa7d22e78b3aac3b69ea1b7200538c2c6995d649365bddab92"}, + {file = "prettytable-3.10.0.tar.gz", hash = "sha256:9665594d137fb08a1117518c25551e0ede1687197cf353a4fdc78d27e1073568"}, +] + +[package.dependencies] +wcwidth = "*" + +[package.extras] +tests = ["pytest", "pytest-cov", "pytest-lazy-fixtures"] + [[package]] name = "pyparsing" version = "3.1.1" @@ -28,6 +110,35 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pyshacl" +version = "0.25.0" +description = "Python SHACL Validator" +optional = false +python-versions = ">=3.8.1,<4.0.0" +files = [ + {file = "pyshacl-0.25.0-py3-none-any.whl", hash = "sha256:716b65397486b1a306efefd018d772d3c112a3828ea4e1be27aae16aee524243"}, + {file = "pyshacl-0.25.0.tar.gz", hash = "sha256:91e87ed04ccb29aa47abfcf8a3e172d35a8831fce23a011cfbf35534ce4c940b"}, +] + +[package.dependencies] +html5lib = ">=1.1,<2" +importlib-metadata = {version = ">6", markers = "python_version < \"3.12\""} +owlrl = ">=6.0.2,<7" +packaging = ">=21.3" +prettytable = [ + {version = ">=3.5.0", markers = "python_version >= \"3.8\" and python_version < \"3.12\""}, + {version = ">=3.7.0", markers = "python_version >= \"3.12\""}, +] +rdflib = {version = ">=6.3.2,<8.0", markers = "python_full_version >= \"3.8.1\""} + +[package.extras] +dev-coverage = ["coverage (>6.1,!=6.1.1,<7)", "platformdirs", "pytest-cov (>=2.8.1,<3.0.0)"] +dev-lint = ["black (==23.11.0)", "platformdirs", "ruff (>=0.1.5,<0.2.0)"] +dev-type-checking = ["mypy (>=0.812,<0.900)", "mypy (>=0.900,<0.1000)", "platformdirs", "types-setuptools"] +http = ["sanic (>=22.12,<23)", "sanic-cors (==2.2.0)", "sanic-ext (>=23.3,<23.6)"] +js = ["pyduktape2 (>=0.4.6,<0.5.0)"] + [[package]] name = "rdflib" version = "7.0.0" @@ -60,7 +171,44 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + +[[package]] +name = "zipp" +version = "3.17.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] + [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "036ed7fb018709708f370a7646048203cf0fa92169d910f113cbe042cba147bf" +content-hash = "d713699b43a56662ac1f19fe035207a1e05aa6a89dadab78f0762f740e015ee1" diff --git a/pyproject.toml b/pyproject.toml index bb76ef54..372ffe68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.11" rdflib = "^7.0.0" +pyshacl = "^0.25.0" [build-system] From 2182ecc50c56f707651cd4051af0c32bf6081482 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Feb 2024 15:05:23 +0100 Subject: [PATCH 009/902] refactor: move code to the rocrate-validator folder --- {src => rocrate_validator}/shapes/rocrate-shapes.ttl | 0 {src => rocrate_validator}/utils.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {src => rocrate_validator}/shapes/rocrate-shapes.ttl (100%) rename {src => rocrate_validator}/utils.py (100%) diff --git a/src/shapes/rocrate-shapes.ttl b/rocrate_validator/shapes/rocrate-shapes.ttl similarity index 100% rename from src/shapes/rocrate-shapes.ttl rename to rocrate_validator/shapes/rocrate-shapes.ttl diff --git a/src/utils.py b/rocrate_validator/utils.py similarity index 100% rename from src/utils.py rename to rocrate_validator/utils.py From 465e889e986986dbe51f21d98d010dbbafafeed2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Feb 2024 15:35:00 +0100 Subject: [PATCH 010/902] build(dep): add click dependency --- poetry.lock | 27 ++++++++++++++++++++++++++- pyproject.toml | 4 ++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 5da1f65a..ca6494c4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,30 @@ # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.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"}, +] + [[package]] name = "html5lib" version = "1.1" @@ -211,4 +236,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "d713699b43a56662ac1f19fe035207a1e05aa6a89dadab78f0762f740e015ee1" +content-hash = "4598924cda4cfaa23b65650fe82b2a14400de1f1112e082745668cabe1463f97" diff --git a/pyproject.toml b/pyproject.toml index 372ffe68..66b1477a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,11 +4,15 @@ version = "0.1.0" description = "" authors = ["Marco Enrico Piras "] readme = "README.md" +packages = [ + { include = "rocrate_validator", from = "." } +] [tool.poetry.dependencies] python = "^3.11" rdflib = "^7.0.0" pyshacl = "^0.25.0" +click = "^8.1.7" [build-system] From 42d7128a9b9e95ddfd4d12bcdf0e90ea0788d187 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Feb 2024 17:26:59 +0100 Subject: [PATCH 011/902] refactor: move shape to shapes folder --- {rocrate_validator/shapes => shapes}/rocrate-shapes.ttl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) rename {rocrate_validator/shapes => shapes}/rocrate-shapes.ttl (77%) diff --git a/rocrate_validator/shapes/rocrate-shapes.ttl b/shapes/rocrate-shapes.ttl similarity index 77% rename from rocrate_validator/shapes/rocrate-shapes.ttl rename to shapes/rocrate-shapes.ttl index f83aa836..9db4460a 100644 --- a/rocrate_validator/shapes/rocrate-shapes.ttl +++ b/shapes/rocrate-shapes.ttl @@ -1,5 +1,4 @@ @prefix : <./> . -@prefix ex: . @prefix dct: . @prefix rdf: . @prefix schema_org: . @@ -16,14 +15,14 @@ schema_org:CreativeWork a sh:NodeShape ; sh:minCount 1 ; sh:nodeKind sh:IRI ; sh:path dct:conformsTo ; - sh:in ( ) + sh:in ( ) ] ; sh:property [ a sh:PropertyShape ; sh:name "about" ; sh:description "The main entity described in the RO-Crate" ; - sh:maxCount 2; - sh:minCount 2 ; + sh:maxCount 1; + sh:minCount 1 ; sh:nodeKind sh:IRI ; sh:path schema_org:about ; sh:hasValue <./> From 5be58c994cf02d6fc6484212494cbe8a1506c8cc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Feb 2024 17:27:39 +0100 Subject: [PATCH 012/902] feat: add models --- rocrate_validator/models.py | 198 ++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 rocrate_validator/models.py diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py new file mode 100644 index 00000000..587da156 --- /dev/null +++ b/rocrate_validator/models.py @@ -0,0 +1,198 @@ +import json +import logging +import os +from rdflib import Graph, URIRef +from rdflib.term import Node + +# set up logging +logger = logging.getLogger(__name__) + + +# Define SHACL namespace +SHACL_NS = "http://www.w3.org/ns/shacl#" + +# Define the rocrate-metadata.json file name +ROCRATE_METADATA_FILE = "ro-crate-metadata.json" + + +class Shape: + + def __init__(self, shape_node: Node, graph: Graph) -> None: + # check the input + assert isinstance(shape_node, Node), "Invalid shape node" + assert isinstance(graph, Graph), "Invalid graph" + + # store the input + self._shape_node = shape_node + self._graph = graph + + # create a graph for the shape + shape_graph = Graph() + shape_graph += graph.triples((shape_node, None, None)) + self.shape_graph = shape_graph + + # serialize the graph in json-ld + shape_json = shape_graph.serialize(format="json-ld") + shape_obj = json.loads(shape_json) + self.shape_json = shape_obj[0] + + @property + def node(self) -> Node: + return self._shape_node + + @property + def graph(self) -> Graph: + return self._graph + + @property + def name(self): + return self.shape_json[f'{SHACL_NS}name'][0]['@value'] + + @property + def description(self): + return self.shape_json[f'{SHACL_NS}description'][0]['@value'] + + @property + def path(self): + return self.shape_json[f'{SHACL_NS}path'][0]['@id'] + + @property + def nodeKind(self): + return self.shape_json[f'{SHACL_NS}nodeKind'][0]['@id'] + + +class Violation: + + def __init__(self, violation_node: Node, graph: Graph) -> None: + # check the input + assert isinstance(violation_node, Node), "Invalid violation node" + assert isinstance(graph, Graph), "Invalid graph" + + # store the input + self._violation_node = violation_node + self._graph = graph + + # create a graph for the violation + violation_graph = Graph() + violation_graph += graph.triples((violation_node, None, None)) + self.violation_graph = violation_graph + + # serialize the graph in json-ld + violation_json = violation_graph.serialize(format="json-ld") + violation_obj = json.loads(violation_json) + self.violation_json = violation_obj[0] + + # get the source shape + shapes = list(graph.triples( + (violation_node, URIRef(f"{SHACL_NS}sourceShape"), None))) + self.source_shape_node = shapes[0][2] + + @property + def node(self) -> Node: + return self._violation_node + + @property + def graph(self) -> Graph: + return self._graph + + @property + def resultSeverity(self): + return self.violation_json[f'{SHACL_NS}resultSeverity'][0]['@id'] + + @property + def focusNode(self): + return self.violation_json[f'{SHACL_NS}focusNode'][0]['@id'] + + @property + def resultPath(self): + return self.violation_json[f'{SHACL_NS}resultPath'][0]['@id'] + + @property + def value(self): + value = self.violation_json.get(f'{SHACL_NS}value', None) + if not value: + return None + return value[0]['@id'] + + @property + def resultMessage(self): + return self.violation_json[f'{SHACL_NS}resultMessage'][0]['@value'] + + @property + def sourceConstraintComponent(self): + return self.violation_json[f'{SHACL_NS}sourceConstraintComponent'][0]['@id'] + + @property + def sourceShape(self) -> Shape: + return Shape(self.source_shape_node, self._graph) + + +class ValidationResult: + + def __init__(self, results_graph: Graph, conforms: bool = None, results_text: str = None) -> None: + # validate the results graph input + assert results_graph is not None, "Invalid graph" + assert isinstance(results_graph, Graph), "Invalid graph type" + # check if the graph is valid ValidationReport + assert (None, URIRef(f"{SHACL_NS}conforms"), + None) in results_graph, "Invalid ValidationReport" + # store the input properties + self._conforms = conforms + self.results_graph = results_graph + self._text = results_text + # parse the results graph + self._violations = self.parse_results_graph(results_graph) + # initialize the conforms property + if conforms is not None: + self._conforms = len(self._violations) == 0 + else: + assert self._conforms == len( + self._violations) == 0, "Invalid validation result" + + @staticmethod + def parse_results_graph(results_graph: Graph): + # Query for validation results + query = """ + SELECT ?subject + WHERE {{ + ?subject a <{0}ValidationResult> . + }} + """.format(SHACL_NS) + + query_results = results_graph.query(query) + + violations = [] + for r in query_results: + violation_node = r[0] + violation = Violation(violation_node, results_graph) + violations.append(violation) + + return violations + + @property + def conforms(self) -> bool: + return self._conforms + + @property + def violations(self) -> list: + return self._violations + + @property + def text(self) -> str: + return self._text + + @staticmethod + def from_serialised_results_graph(file_path: str, format: str = 'turtle'): + # check the input + assert format in ['turtle', 'n3', 'nt', + 'xml', 'rdf', 'json-ld'], "Invalid format" + assert file_path, "Invalid file path" + assert os.path.exists(file_path), "File does not exist" + # Load the graph + logger.debug("Loading graph from file: %s" % file_path) + g = Graph() + g.parse(file_path, format=format) + logger.debug("Graph loaded from file: %s" % file_path) + + # return the validation result + return ValidationResult(g) From c9d7805a61645e170b2d91d8742c016a456bab35 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 20 Feb 2024 17:28:26 +0100 Subject: [PATCH 013/902] feat(srv): add validation services --- rocrate_validator/service.py | 129 +++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 rocrate_validator/service.py diff --git a/rocrate_validator/service.py b/rocrate_validator/service.py new file mode 100644 index 00000000..662f60a6 --- /dev/null +++ b/rocrate_validator/service.py @@ -0,0 +1,129 @@ +import logging +from typing import Optional, Union + +from rdflib import Graph + +import pyshacl +from pyshacl.pytypes import GraphLike + +from .models import ROCRATE_METADATA_FILE, ValidationResult +from .shapes import get_shapes_paths, get_shapes_graph + +# set up logging +logger = logging.getLogger(__name__) + + +class SHACLValidator: + + def __init__( + self, + shapes_graph: Optional[Union[GraphLike, str, bytes]], + ont_graph: Optional[Union[GraphLike, str, bytes]] = None, + ) -> None: + self._shapes_graph = shapes_graph + self._ont_graph = ont_graph + + @property + def shapes_graph(self) -> Optional[Union[GraphLike, str, bytes]]: + return self._shapes_graph + + @property + def ont_graph(self) -> Optional[Union[GraphLike, str, bytes]]: + return self._ont_graph + + def validate( + self, + data_graph: Union[GraphLike, str, bytes], + advanced: Optional[bool] = False, + inference: Optional[str] = None, + inplace: Optional[bool] = False, + abort_on_first: Optional[bool] = False, + allow_infos: Optional[bool] = False, + allow_warnings: Optional[bool] = False, + serialisation_output_path: str = None, + serialisation_output_format: str = "turtle", + **kwargs, + ) -> ValidationResult: + + # 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, + inplace=inplace, + abort_on_first=abort_on_first, + allow_infos=allow_infos, + allow_warnings=allow_warnings, + meta_shacl=False, + advanced=advanced, + js=False, + debug=False, + **kwargs, + ) + # log the validation results + logger.debug("Conforms: %r", conforms) + logger.debug("Results Graph: %r", results_graph) + logger.debug("Results Text: %r", results_text) + + # serialize the results graph + if serialisation_output_path: + assert serialisation_output_format in [ + "turtle", + "n3", + "nt", + "xml", + "rdf", + "json-ld", + ], "Invalid serialisation output format" + results_graph.serialize( + serialisation_output_path, format=serialisation_output_format + ) + # return the validation result + return ValidationResult(results_graph, conforms, results_text) + + +def validate( + rocrate_path: Union[GraphLike, str, bytes], + shapes_path: Union[GraphLike, str, bytes], + advanced: Optional[bool] = False, + inference: Optional[str] = None, + inplace: Optional[bool] = False, + abort_on_first: Optional[bool] = False, + allow_infos: Optional[bool] = False, + allow_warnings: Optional[bool] = False, + serialisation_output_path: str = None, + serialisation_output_format: str = "turtle", + **kwargs, +) -> ValidationResult: + """ + Validate a data graph using SHACL shapes as constraints + """ + # TODO: handle multiple shapes files and allow user to select one + shacl_graph = shapes_path + logger.debug("shacl_graph: %s", shacl_graph) + + # load the data graph + data_graph = Graph() + data_graph.parse(f"{rocrate_path}/{ROCRATE_METADATA_FILE}", + format="json-ld", publicID=rocrate_path) + + # load the shapes graph + shacl_graph = get_shapes_graph(shapes_path, publicID=rocrate_path) + + validator = SHACLValidator(shapes_graph=shacl_graph) + result = validator.validate( + data_graph=data_graph, + advanced=advanced, + inference=inference, + inplace=inplace, + abort_on_first=abort_on_first, + allow_infos=allow_infos, + allow_warnings=allow_warnings, + serialisation_output_path=serialisation_output_path, + serialisation_output_format=serialisation_output_format, + publicID=rocrate_path, + **kwargs, + ) + logger.debug("Validation conforms: %s", result.conforms) + return result From 6d4661792723e3a9c2ec299a97f8b6b4f96002a1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 4 Mar 2024 16:52:07 +0100 Subject: [PATCH 014/902] refactor(utils): :recycle: generic function to load graphs from paths --- rocrate_validator/utils.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 73a7be51..f111906f 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -1,4 +1,15 @@ +import logging import os +from typing import List + +from rdflib import Graph + +# current directory +CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) + + +# set up logging +logger = logging.getLogger(__name__) def get_all_files(directory: str = '.', extension: str = '.ttl'): @@ -14,3 +25,22 @@ def get_all_files(directory: str = '.', extension: str = '.ttl'): file_paths.append(os.path.join(root, file)) # return the list of file paths return file_paths + + +def get_graphs_paths(graphs_dir: str = CURRENT_DIR) -> List[str]: + """ + Get all the SHACL shapes files in the shapes directory + """ + return get_all_files(directory=graphs_dir, extension='.ttl') + + +def get_full_graph(graphs_dir: str, publicID: str = ".") -> Graph: + """ + Get the SHACL shapes graph + """ + full_graph = Graph() + graphs_paths = get_graphs_paths(graphs_dir) + for graph_path in graphs_paths: + full_graph.parse(graph_path, format="turtle", publicID=publicID) + logger.debug("Loaded triples from %s", graph_path) + return full_graph From 640646ed7d328040a37078f3e609675f44b6de84 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 4 Mar 2024 16:58:01 +0100 Subject: [PATCH 015/902] build(core): :package: Update Python packages --- poetry.lock | 200 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 10 ++- 2 files changed, 205 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index ca6494c4..9f654854 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. [[package]] name = "click" @@ -25,6 +25,70 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.4.3" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, + {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, + {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, + {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, + {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, + {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, + {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, + {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, + {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, + {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, + {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, + {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, + {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"}, + {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"}, + {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"}, + {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"}, + {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"}, + {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"}, + {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"}, + {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"}, + {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, + {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, +] + +[package.extras] +toml = ["tomli"] + [[package]] name = "html5lib" version = "1.1" @@ -65,6 +129,17 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "isodate" version = "0.6.1" @@ -79,6 +154,41 @@ files = [ [package.dependencies] six = "*" +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "owlrl" version = "6.0.2" @@ -104,6 +214,21 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "prettytable" version = "3.10.0" @@ -121,6 +246,21 @@ wcwidth = "*" [package.extras] tests = ["pytest", "pytest-cov", "pytest-lazy-fixtures"] +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pyparsing" version = "3.1.1" @@ -164,6 +304,44 @@ dev-type-checking = ["mypy (>=0.812,<0.900)", "mypy (>=0.900,<0.1000)", "platfor http = ["sanic (>=22.12,<23)", "sanic-cors (==2.2.0)", "sanic-ext (>=23.3,<23.6)"] js = ["pyduktape2 (>=0.4.6,<0.5.0)"] +[[package]] +name = "pytest" +version = "8.1.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.1.0-py3-none-any.whl", hash = "sha256:ee32db7af8de4629a455806befa90559f307424c07b8413ccfc30bf5b221dd7e"}, + {file = "pytest-8.1.0.tar.gz", hash = "sha256:f8fa04ab8f98d185113ae60ea6d79c22f8143b14bc1caeced44a0ab844928323"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.4,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + [[package]] name = "rdflib" version = "7.0.0" @@ -185,6 +363,24 @@ html = ["html5lib (>=1.0,<2.0)"] lxml = ["lxml (>=4.3.0,<5.0.0)"] networkx = ["networkx (>=2.0.0,<3.0.0)"] +[[package]] +name = "rich" +version = "13.7.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "six" version = "1.16.0" @@ -236,4 +432,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "4598924cda4cfaa23b65650fe82b2a14400de1f1112e082745668cabe1463f97" +content-hash = "3c5397b17913e101c33e68d8b2ecebbcddf14aef4f5e243e74464d22dcb5fdc9" diff --git a/pyproject.toml b/pyproject.toml index 66b1477a..89ebe83d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,15 +4,19 @@ version = "0.1.0" description = "" authors = ["Marco Enrico Piras "] readme = "README.md" -packages = [ - { include = "rocrate_validator", from = "." } -] +packages = [{ include = "rocrate_validator", from = "." }] [tool.poetry.dependencies] python = "^3.11" rdflib = "^7.0.0" pyshacl = "^0.25.0" click = "^8.1.7" +rich = "^13.7.1" + + +[tool.poetry.dev-dependencies] +pytest = "^8.1.0" +pytest-cov = "^4.1.0" [build-system] From f70b98b8d3c12958e5c29be07aa51f13fee64bad Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 4 Mar 2024 17:01:30 +0100 Subject: [PATCH 016/902] build(core): :rocket: configure the `rocrate-validator` script --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 89ebe83d..0618503f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,3 +22,6 @@ pytest-cov = "^4.1.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +rocrate-validator = "rocrate_validator.cli:cli" From dddca67abb612939e35f3394911e050facf0ff6e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 4 Mar 2024 17:03:56 +0100 Subject: [PATCH 017/902] build: :see_no_evil: Update ignore file --- .env | 1 + .gitignore | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 00000000..e86de2aa --- /dev/null +++ b/.env @@ -0,0 +1 @@ +PYTHONPATH=rocrate_validator \ No newline at end of file diff --git a/.gitignore b/.gitignore index d731cf2b..f049cc1b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,12 @@ .DS_Store -**/__pycache__ \ No newline at end of file +**/__pycache__ +**/*.pyc +**/.pytest_cache + +# ignore virtualenv +.venv + +# ignore coverage files +**/.coverage +**/.coverage.* +**/.report From 699ea1cc69b68d6b5be75082267da527a7579e53 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 14:20:52 +0100 Subject: [PATCH 018/902] feat: :sparkles: add minimal cli entrypoint --- rocrate_validator/cli.py | 134 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 rocrate_validator/cli.py diff --git a/rocrate_validator/cli.py b/rocrate_validator/cli.py new file mode 100644 index 00000000..eddc4adc --- /dev/null +++ b/rocrate_validator/cli.py @@ -0,0 +1,134 @@ +import logging +import os + +import click +from rich.console import Console +from rocrate_validator.errors import CheckValidationError, SHACLValidationError +from rocrate_validator.service import validate as validate_rocrate + +# set up logging +logger = logging.getLogger(__name__) + + +# Create a Rich Console instance for enhanced output +console = Console() + + +@click.group(invoke_without_command=True) +@click.option( + '--debug', + is_flag=True, + help="Enable debug logging", + default=False +) +@click.option( + "-s", + "--shapes-path", + type=click.Path(exists=True), + default="./shapes", + help="Path containing the shapes files", +) +@click.option( + "-o", + "--ontologies-path", + type=click.Path(exists=True), + default="./ontologies", + help="Path containing the ontology files", +) +@click.argument("rocrate-path", type=click.Path(exists=True), default=".") +@click.pass_context +def cli(ctx, debug, shapes_path, ontologies_path, rocrate_path): + # Set the log level + if debug: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.WARNING) + # If no subcommand is provided, invoke the default command + if ctx.invoked_subcommand is None: + # If no subcommand is provided, invoke the default command + ctx.invoke(validate, shapes_path=shapes_path, + ontologies_path=ontologies_path, + rocrate_path=rocrate_path) + + +@cli.command("validate") +def validate(shapes_path: str, ontologies_path: str, rocrate_path: str): + """Validate a RO-Crate using SHACL shapes as constraints.""" + + # Print the input parameters + logger.debug("shapes_path: %s", os.path.abspath(shapes_path)) + logger.debug("rocrate-path: %s", os.path.abspath(rocrate_path)) + logger.debug("ontologies_path: %s", os.path.abspath(ontologies_path)) + + try: + + # Validate the RO-Crate + result = validate_rocrate( + rocrate_path=os.path.abspath(rocrate_path), + shapes_path=os.path.abspath(shapes_path), + # ontologies_path=os.path.abspath(ontologies_path), + ) + + # Print the validation result + logger.debug("Validation conforms: %s" % result) + console.print( + "\n\n[bold]\[[green]OK[/green]] RO-Crate is valid!!![/bold]\n\n", + style="white", + ) + + except Exception as e: + console.print( + "\n\n[bold]\[[red]FAILED[/red]] RO-Crate is not valid!!![/bold]\n", + style="white", + ) + if logger.isEnabledFor(logging.DEBUG): + console.print_exception() + if isinstance(e, CheckValidationError): + console.print( + f"Check [bold][red]{e.check.name}[/red][/bold] failed: ") + console.print(f" -> {str(e)}\n\n", style="white") + elif isinstance(e, SHACLValidationError): + _log_validation_result_(e.result) + else: + console.print("Error: ", style="red", end="") + console.print(f" -> {str(e)}\n\n", style="white") + console.print("\n\n", style="white") + + +def _log_validation_result_(result: bool): + # Print the number of violations + logger.debug("* Number of violations: %s" % len(result.violations)) + + console.print("\n[bold]** %s validation errors: [/bold]" % + len(result.violations)) + + # Print the violations + count = 0 + for v in result.violations: + count += 1 + console.print( + "\n -> [red][bold]Violation " + f"{count}[/bold][/red]: {v.resultMessage}", + style="white", + ) + print(" - resultSeverity: %s" % v.resultSeverity) + print(" - focusNode: %s" % v.focusNode) + print(" - resultPath: %s" % v.resultPath) + print(" - value: %s" % v.value) + print(" - resultMessage: %s" % v.resultMessage) + print(" - sourceConstraintComponent: %s" % v.sourceConstraintComponent) + try: + if v.sourceShape: + print(" - sourceShape: %s" % v.sourceShape) + print(" - sourceShape.name: %s" % v.sourceShape.name) + print(" - sourceShape.description: %s" % + v.sourceShape.description) + print(" - sourceShape.path: %s" % v.sourceShape.path) + print(" - sourceShape.nodeKind: %s" % v.sourceShape.nodeKind) + except Exception as e: + print(f"Error getting source shape: {e}") + print("\n") + + +if __name__ == "__main__": + cli() From 79d8dd6d82d54ad9106ccf5e0e39a09db536e9a3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 14:23:00 +0100 Subject: [PATCH 019/902] refactor(cli): :fire: disable ontologies parameter --- rocrate_validator/cli.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/cli.py b/rocrate_validator/cli.py index eddc4adc..c2a12780 100644 --- a/rocrate_validator/cli.py +++ b/rocrate_validator/cli.py @@ -28,16 +28,16 @@ default="./shapes", help="Path containing the shapes files", ) -@click.option( - "-o", - "--ontologies-path", - type=click.Path(exists=True), - default="./ontologies", - help="Path containing the ontology files", -) +# @click.option( +# "-o", +# "--ontologies-path", +# type=click.Path(exists=True), +# default="./ontologies", +# help="Path containing the ontology files", +# ) @click.argument("rocrate-path", type=click.Path(exists=True), default=".") @click.pass_context -def cli(ctx, debug, shapes_path, ontologies_path, rocrate_path): +def cli(ctx, debug, shapes_path, ontologies_path=None, rocrate_path="."): # Set the log level if debug: logging.basicConfig(level=logging.DEBUG) From 779d9eed495114502d6188111aae3de6921e822e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 14:26:33 +0100 Subject: [PATCH 020/902] docs(cli): :memo: just one note about the CLI entrypoint --- rocrate_validator/cli.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli.py b/rocrate_validator/cli.py index c2a12780..db40356c 100644 --- a/rocrate_validator/cli.py +++ b/rocrate_validator/cli.py @@ -53,7 +53,11 @@ def cli(ctx, debug, shapes_path, ontologies_path=None, rocrate_path="."): @cli.command("validate") def validate(shapes_path: str, ontologies_path: str, rocrate_path: str): - """Validate a RO-Crate using SHACL shapes as constraints.""" + """ + Validate a RO-Crate using SHACL shapes as constraints. + * this command might be the only one needed for the CLI. + ??? merge this command with the main command ? + """ # Print the input parameters logger.debug("shapes_path: %s", os.path.abspath(shapes_path)) From 866509745f939e608f63a6c3ffb6a649f462b1cc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 14:31:36 +0100 Subject: [PATCH 021/902] build(core): :pushpin: update dependencies --- poetry.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9f654854..23283f6e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -112,22 +112,22 @@ lxml = ["lxml"] [[package]] name = "importlib-metadata" -version = "7.0.1" +version = "7.0.2" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, - {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, + {file = "importlib_metadata-7.0.2-py3-none-any.whl", hash = "sha256:f4bc4c0c070c490abf4ce96d715f68e95923320370efb66143df00199bb6c100"}, + {file = "importlib_metadata-7.0.2.tar.gz", hash = "sha256:198f568f3230878cb1b44fbd7975f87906c22336dba2e4a7f05278c281fbd792"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "iniconfig" @@ -263,13 +263,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyparsing" -version = "3.1.1" +version = "3.1.2" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.6.8" files = [ - {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, - {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, ] [package.extras] @@ -306,13 +306,13 @@ js = ["pyduktape2 (>=0.4.6,<0.5.0)"] [[package]] name = "pytest" -version = "8.1.0" +version = "8.1.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.1.0-py3-none-any.whl", hash = "sha256:ee32db7af8de4629a455806befa90559f307424c07b8413ccfc30bf5b221dd7e"}, - {file = "pytest-8.1.0.tar.gz", hash = "sha256:f8fa04ab8f98d185113ae60ea6d79c22f8143b14bc1caeced44a0ab844928323"}, + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, ] [package.dependencies] From bd4241927906a53bd70ad8bb9fc6063fe025b316 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 14:36:57 +0100 Subject: [PATCH 022/902] fix(cli): :bug: log info only when available --- rocrate_validator/cli.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/cli.py b/rocrate_validator/cli.py index db40356c..55d0755c 100644 --- a/rocrate_validator/cli.py +++ b/rocrate_validator/cli.py @@ -52,25 +52,29 @@ def cli(ctx, debug, shapes_path, ontologies_path=None, rocrate_path="."): @cli.command("validate") -def validate(shapes_path: str, ontologies_path: str, rocrate_path: str): +def validate(shapes_path: str, ontologies_path: str = None, rocrate_path: str = "."): """ Validate a RO-Crate using SHACL shapes as constraints. * this command might be the only one needed for the CLI. ??? merge this command with the main command ? """ - # Print the input parameters - logger.debug("shapes_path: %s", os.path.abspath(shapes_path)) - logger.debug("rocrate-path: %s", os.path.abspath(rocrate_path)) - logger.debug("ontologies_path: %s", os.path.abspath(ontologies_path)) + # Log the input parameters for debugging + if shapes_path: + logger.debug("shapes_path: %s", os.path.abspath(shapes_path)) + if ontologies_path: + logger.debug("ontologies_path: %s", os.path.abspath(ontologies_path)) + if rocrate_path: + logger.debug("ontologies_path: %s", os.path.abspath(rocrate_path)) try: # Validate the RO-Crate result = validate_rocrate( rocrate_path=os.path.abspath(rocrate_path), - shapes_path=os.path.abspath(shapes_path), - # ontologies_path=os.path.abspath(ontologies_path), + shapes_path=os.path.abspath(shapes_path) if shapes_path else None, + ontologies_path=os.path.abspath( + ontologies_path) if ontologies_path else None, ) # Print the validation result From 36b87c4b770a414889f92ffd1710e192d67a419d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 14:53:38 +0100 Subject: [PATCH 023/902] refactor(shacl): :truck: move SHACL validator to a dedicated package --- rocrate_validator/checks/__init__.py | 48 ++++++++ rocrate_validator/service.py | 144 +++++++++-------------- rocrate_validator/validators/__init__.py | 0 rocrate_validator/validators/shacl.py | 137 +++++++++++++++++++++ 4 files changed, 240 insertions(+), 89 deletions(-) create mode 100644 rocrate_validator/checks/__init__.py create mode 100644 rocrate_validator/validators/__init__.py create mode 100644 rocrate_validator/validators/shacl.py diff --git a/rocrate_validator/checks/__init__.py b/rocrate_validator/checks/__init__.py new file mode 100644 index 00000000..8e4e89de --- /dev/null +++ b/rocrate_validator/checks/__init__.py @@ -0,0 +1,48 @@ +from importlib import import_module +import inspect +import os +import re +import logging +from typing import List + +# set up logging +logger = logging.getLogger(__name__) + +# current directory +__CURRENT_DIR__ = os.path.dirname(os.path.realpath(__file__)) + + +def get_checks(directory: str = __CURRENT_DIR__, instaces: bool = True) -> List[str]: + """ + Load all the classes from the directory + """ + logger.debug("Loading checks from %s", directory) + # create an empty list to store the classes + classes = {} + # loop through the files in the directory + for root, dirs, files in os.walk(directory): + for file in files: + # check if the file is a python file + logger.debug("Checking file %s", file) + if file.endswith(".py") and not file.startswith("__init__"): + # get the file path + file_path = os.path.join(root, file) + # FIXME: works only on the main "general" general directory + m = '{}.{}'.format( + 'rocrate_validator.checks', os.path.basename(file_path)[:-3]) + logger.debug("Module: %r" % m) + # import the module + mod = import_module(m) + # loop through the objects in the module + # and store the classes + for _, obj in inspect.getmembers(mod): + logger.debug("Checking object %s", obj) + if inspect.isclass(obj) \ + and inspect.getmodule(obj) == mod \ + and obj.__name__.endswith('Check'): + classes[obj.__name__] = obj + logger.debug("Loaded class %s", obj.__name__) + return [v() if instaces else v for v in classes.values()] + + # return the list of classes + return classes diff --git a/rocrate_validator/service.py b/rocrate_validator/service.py index 662f60a6..9c046a76 100644 --- a/rocrate_validator/service.py +++ b/rocrate_validator/service.py @@ -1,117 +1,81 @@ import logging -from typing import Optional, Union +from typing import Literal, Optional, Union -from rdflib import Graph - -import pyshacl from pyshacl.pytypes import GraphLike +from rdflib import Graph -from .models import ROCRATE_METADATA_FILE, ValidationResult -from .shapes import get_shapes_paths, get_shapes_graph +from .errors import CheckValidationError, SHACLValidationError +from .models import ROCRATE_METADATA_FILE +from .utils import get_full_graph +from .validators.shacl import Validator # set up logging logger = logging.getLogger(__name__) -class SHACLValidator: - - def __init__( - self, - shapes_graph: Optional[Union[GraphLike, str, bytes]], - ont_graph: Optional[Union[GraphLike, str, bytes]] = None, - ) -> None: - self._shapes_graph = shapes_graph - self._ont_graph = ont_graph - - @property - def shapes_graph(self) -> Optional[Union[GraphLike, str, bytes]]: - return self._shapes_graph - - @property - def ont_graph(self) -> Optional[Union[GraphLike, str, bytes]]: - return self._ont_graph - - def validate( - self, - data_graph: Union[GraphLike, str, bytes], - advanced: Optional[bool] = False, - inference: Optional[str] = None, - inplace: Optional[bool] = False, - abort_on_first: Optional[bool] = False, - allow_infos: Optional[bool] = False, - allow_warnings: Optional[bool] = False, - serialisation_output_path: str = None, - serialisation_output_format: str = "turtle", - **kwargs, - ) -> ValidationResult: - - # 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, - inplace=inplace, - abort_on_first=abort_on_first, - allow_infos=allow_infos, - allow_warnings=allow_warnings, - meta_shacl=False, - advanced=advanced, - js=False, - debug=False, - **kwargs, - ) - # log the validation results - logger.debug("Conforms: %r", conforms) - logger.debug("Results Graph: %r", results_graph) - logger.debug("Results Text: %r", results_text) - - # serialize the results graph - if serialisation_output_path: - assert serialisation_output_format in [ - "turtle", - "n3", - "nt", - "xml", - "rdf", - "json-ld", - ], "Invalid serialisation output format" - results_graph.serialize( - serialisation_output_path, format=serialisation_output_format - ) - # return the validation result - return ValidationResult(results_graph, conforms, results_text) - - def validate( rocrate_path: Union[GraphLike, str, bytes], shapes_path: Union[GraphLike, str, bytes], + ontologies_path: Union[GraphLike, str, bytes] = None, advanced: Optional[bool] = False, - inference: Optional[str] = None, + inference: Optional[Literal["owl", "rdfs"]] = False, inplace: Optional[bool] = False, abort_on_first: Optional[bool] = False, allow_infos: Optional[bool] = False, allow_warnings: Optional[bool] = False, - serialisation_output_path: str = None, - serialisation_output_format: str = "turtle", + serialization_output_path: str = None, + serialization_output_format: str = "turtle", **kwargs, -) -> ValidationResult: +) -> bool: """ Validate a data graph using SHACL shapes as constraints + + :param rocrate_path: The path to the RO-Crate metadata file + :param shapes_path: The path to the SHACL shapes file + + :return: True if the data graph conforms to the SHACL shapes + :raises SHACLValidationError: If the data graph does not conform to the SHACL shapes + :raises CheckValidationError: If a check fails """ - # TODO: handle multiple shapes files and allow user to select one - shacl_graph = shapes_path - logger.debug("shacl_graph: %s", shacl_graph) + + # set the RO-Crate metadata file + rocrate_metadata_path = f"{rocrate_path}/{ROCRATE_METADATA_FILE}" + logger.debug("rocrate_metadata_path: %s", rocrate_metadata_path) + + from .checks import get_checks + + # get the checks + for check_instance in get_checks(): + logger.debug("Loaded check: %s", check_instance) + result = check_instance.check(rocrate_path) + if result[0] == 0: + logger.debug("Check passed: %s", check_instance.name) + else: + logger.debug( + f"Check {check_instance.name} failed: " + f"{result[1]}", check_instance.name + ) + raise CheckValidationError( + check_instance, result[1], rocrate_path, result[0] + ) # load the data graph data_graph = Graph() - data_graph.parse(f"{rocrate_path}/{ROCRATE_METADATA_FILE}", + data_graph.parse(rocrate_metadata_path, format="json-ld", publicID=rocrate_path) # load the shapes graph - shacl_graph = get_shapes_graph(shapes_path, publicID=rocrate_path) + shacl_graph = None + if shapes_path: + shacl_graph = get_full_graph(shapes_path, publicID=rocrate_path) + + # load the ontology graph + ontology_graph = None + if ontologies_path: + ontology_graph = get_full_graph(ontologies_path) - validator = SHACLValidator(shapes_graph=shacl_graph) + validator = Validator( + shapes_graph=shacl_graph, ont_graph=ontology_graph) result = validator.validate( data_graph=data_graph, advanced=advanced, @@ -120,10 +84,12 @@ def validate( abort_on_first=abort_on_first, allow_infos=allow_infos, allow_warnings=allow_warnings, - serialisation_output_path=serialisation_output_path, - serialisation_output_format=serialisation_output_format, + serialization_output_path=serialization_output_path, + serialization_output_format=serialization_output_format, publicID=rocrate_path, **kwargs, ) logger.debug("Validation conforms: %s", result.conforms) - return result + if not result.conforms: + raise SHACLValidationError(result, rocrate_path) + return True diff --git a/rocrate_validator/validators/__init__.py b/rocrate_validator/validators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rocrate_validator/validators/shacl.py b/rocrate_validator/validators/shacl.py new file mode 100644 index 00000000..8c74c331 --- /dev/null +++ b/rocrate_validator/validators/shacl.py @@ -0,0 +1,137 @@ +import logging +from typing import Literal, Optional, Union + +import pyshacl +from pyshacl.pytypes import GraphLike +from rdflib import Graph + +from ..constants import RDF_SERIALIZATION_FORMATS, VALID_INFERENCE_OPTIONS +from ..models import ValidationResult + +# set up logging +logger = logging.getLogger(__name__) + + +class Validator: + + def __init__( + self, + shapes_graph: Optional[Union[GraphLike, str, bytes]], + ont_graph: Optional[Union[GraphLike, str, bytes]] = None, + ) -> None: + """ + Create a new SHACLValidator instance. + + :param shacl_graph: rdflib.Graph or file path or web url + of the SHACL Shapes graph to use to + validate the data graph + :type shacl_graph: rdflib.Graph | str | bytes + :param ont_graph: rdflib.Graph or file path or web url + of an extra ontology document to mix into the data graph + :type ont_graph: rdflib.Graph | str | bytes + """ + self._shapes_graph = shapes_graph + self._ont_graph = ont_graph + + @property + def shapes_graph(self) -> Optional[Union[GraphLike, str, bytes]]: + return self._shapes_graph + + @property + def ont_graph(self) -> Optional[Union[GraphLike, str, bytes]]: + return self._ont_graph + + def validate( + self, + data_graph: Union[GraphLike, str, bytes], + advanced: Optional[bool] = False, + inference: Optional[Literal["owl", "rdfs"]] = False, + inplace: Optional[bool] = False, + abort_on_first: Optional[bool] = False, + allow_infos: Optional[bool] = False, + allow_warnings: Optional[bool] = False, + serialization_output_path: Optional[str] = None, + serialization_output_format: Optional[str] = "turtle", + **kwargs, + ) -> ValidationResult: + f""" + Validate a data graph using SHACL shapes as constraints + + :param data_graph: rdflib.Graph or file path or web url + of the data to validate + :type data_graph: rdflib.Graph | str | bytes + :param advanced: Enable advanced SHACL features, default=False + :type advanced: bool | None + :param inference: One of "rdfs", "owlrl", "both", "none", or None + :type inference: str | None + :param inplace: If this is enabled, do not clone the datagraph, + manipulate it inplace + :type inplace: bool + :param abort_on_first: Stop evaluating constraints after first + violation is found + :type abort_on_first: bool | None + :param allow_infos: Shapes marked with severity of sh:Info + will not cause result to be invalid. + :type allow_infos: bool | None + :param allow_warnings: Shapes marked with severity of sh:Warning + or sh:Info will not cause result to be invalid. + :type allow_warnings: bool | None + :param serialization_output_format: Literal[ + {RDF_SERIALIZATION_FORMATS} + ] + :param kwargs: Additional keyword arguments to pass to pyshacl.validate + """ + + # Validate data_graph + if not isinstance(data_graph, (Graph, str, bytes)): + raise ValueError( + "data_graph must be an instance of Graph, str, or bytes") + + # Validate inference + if inference and inference not in VALID_INFERENCE_OPTIONS: + raise ValueError( + f"inference must be one of {VALID_INFERENCE_OPTIONS}") + + # Validate serialization_output_format + if serialization_output_format and \ + serialization_output_format not in RDF_SERIALIZATION_FORMATS: + raise ValueError( + "serialization_output_format must be one of " + f"{RDF_SERIALIZATION_FORMATS}") + + # 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, + inplace=inplace, + abort_on_first=abort_on_first, + allow_infos=allow_infos, + allow_warnings=allow_warnings, + meta_shacl=False, + advanced=advanced, + js=False, + debug=False, + **kwargs, + ) + # log the validation results + logger.debug("Conforms: %r", conforms) + logger.debug("Results Graph: %r", results_graph) + logger.debug("Results Text: %r", results_text) + + # serialize the results graph + if serialization_output_path: + assert serialization_output_format in [ + "turtle", + "n3", + "nt", + "xml", + "rdf", + "json-ld", + ], "Invalid serialization output format" + results_graph.serialize( + serialization_output_path, format=serialization_output_format + ) + # return the validation result + return ValidationResult(results_graph, conforms, results_text) From a754bbe5187e02b0bae27bc77c0401ec03a1153c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 15:05:32 +0100 Subject: [PATCH 024/902] refactor(shacl): :recycle: use contant module to define allowed values of params --- rocrate_validator/constants.py | 24 ++++++++++++++++++++++++ rocrate_validator/validators/shacl.py | 9 +++++---- 2 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 rocrate_validator/constants.py diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py new file mode 100644 index 00000000..5a86a72e --- /dev/null +++ b/rocrate_validator/constants.py @@ -0,0 +1,24 @@ +# Define allowed RDF extensions and serialization formats as map +import typing + + +# Define allowed RDF extensions and serialization formats as map +RDF_SERIALIZATION_FILE_FORMAT_MAP = { + "xml": "xml", + "pretty-xml": "pretty-xml", + "trig": "trig", + "n3": "n3", + "turtle": "ttl", + "nt": "nt", + "json-ld": "json-ld" +} + +# Define allowed RDF serialization formats +RDF_SERIALIZATION_FORMATS_TYPES = typing.Literal[ + "xml", "pretty-xml", "trig", "n3", "turtle", "nt", "json-ld" +] +RDF_SERIALIZATION_FORMATS = typing.get_args(RDF_SERIALIZATION_FORMATS_TYPES) + +# Define allowed inference options +VALID_INFERENCE_OPTIONS_TYPES = typing.Literal["owl", "rdfs", "both", None] +VALID_INFERENCE_OPTIONS = typing.get_args(VALID_INFERENCE_OPTIONS_TYPES) diff --git a/rocrate_validator/validators/shacl.py b/rocrate_validator/validators/shacl.py index 8c74c331..b9db7788 100644 --- a/rocrate_validator/validators/shacl.py +++ b/rocrate_validator/validators/shacl.py @@ -5,7 +5,7 @@ from pyshacl.pytypes import GraphLike from rdflib import Graph -from ..constants import RDF_SERIALIZATION_FORMATS, VALID_INFERENCE_OPTIONS +from ..constants import RDF_SERIALIZATION_FORMATS, RDF_SERIALIZATION_FORMATS_TYPES, VALID_INFERENCE_OPTIONS, VALID_INFERENCE_OPTIONS_TYPES from ..models import ValidationResult # set up logging @@ -45,13 +45,14 @@ def validate( self, data_graph: Union[GraphLike, str, bytes], advanced: Optional[bool] = False, - inference: Optional[Literal["owl", "rdfs"]] = False, + inference: Optional[VALID_INFERENCE_OPTIONS_TYPES] = None, inplace: Optional[bool] = False, abort_on_first: Optional[bool] = False, allow_infos: Optional[bool] = False, allow_warnings: Optional[bool] = False, serialization_output_path: Optional[str] = None, - serialization_output_format: Optional[str] = "turtle", + serialization_output_format: + Optional[RDF_SERIALIZATION_FORMATS_TYPES] = "turtle", **kwargs, ) -> ValidationResult: f""" @@ -62,7 +63,7 @@ def validate( :type data_graph: rdflib.Graph | str | bytes :param advanced: Enable advanced SHACL features, default=False :type advanced: bool | None - :param inference: One of "rdfs", "owlrl", "both", "none", or None + :param inference: One of {VALID_INFERENCE_OPTIONS} :type inference: str | None :param inplace: If this is enabled, do not clone the datagraph, manipulate it inplace From 4ef54fec13c80e7e9718ba50a9901d324a12dc1e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 15:31:53 +0100 Subject: [PATCH 025/902] refactor(utils): :recycle: use format to denote graph types --- rocrate_validator/errors.py | 76 +++++++++++++++++++++++++++++++++++++ rocrate_validator/utils.py | 28 ++++++++++++-- 2 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 rocrate_validator/errors.py diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py new file mode 100644 index 00000000..8ed6fd75 --- /dev/null +++ b/rocrate_validator/errors.py @@ -0,0 +1,76 @@ +from .models import ValidationResult + + +class InvalidSerializationFormat(Exception): + def __init__(self, format: str = None): + self._format = format + + @property + def format(self): + return self._format + + def __str__(self): + return f"Invalid serialization format: {self._format!r}" + + def __repr__(self): + return f"InvalidSerializationFormat({self._format!r})" + + +class ValidationError(Exception): + def __init__(self, message, path: str = ".", code: int = -1): + self._message = message + self._path = path + self._code = code + + @property + def message(self) -> str: + return self._message + + @property + def path(self) -> str: + return self._path + + @property + def code(self) -> int: + return self._code + + def __str__(self): + return self._message + + def __repr__(self): + return f"ValidationError({self._message!r}, {self._path!r})" + + +class CheckValidationError(ValidationError): + def __init__(self, check, message, path: str = ".", code: int = -1): + super().__init__(message, path, code) + self._check = check + + @property + def check(self): + return self._check + + def __repr__(self): + return f"CheckValidationError({self._check!r}, {self._message!r}, {self._path!r})" + + +class SHACLValidationError(ValidationError): + + def __init__( + self, + result: ValidationResult = None, + message: str = "Document does not conform to SHACL shapes.", + path: str = ".", + code: int = 500, + ): + super().__init__(message, path, code) + self._result = result + + @property + def result(self) -> ValidationResult: + return self._result + + def __repr__(self): + return ( + f"SHACLValidationError({self._message!r}, {self._path!r}, {self.result!r})" + ) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index f111906f..daf501c4 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -4,6 +4,8 @@ from rdflib import Graph +from . import constants, errors + # current directory CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) @@ -12,9 +14,23 @@ logger = logging.getLogger(__name__) -def get_all_files(directory: str = '.', extension: str = '.ttl'): +def get_format_extension(format: constants.RDF_SERIALIZATION_FORMATS_TYPES) -> str: + """ + try: + return constants.RDF_SERIALIZATION_FILE_FORMAT_MAP[format] + except KeyError: + logger.error("Invalid RDF serialization format: %s", format) + raise errors.InvalidSerializationFormat(format) + +def get_all_files( + directory: str = '.', + format: constants.RDF_SERIALIZATION_FORMATS_TYPES = "turtle") -> List[str]: # initialize an empty list to store the file paths file_paths = [] + + # extension + extension = get_format_extension(format) + # iterate through the directory and subdirectories for root, dirs, files in os.walk(directory): # iterate through the files @@ -27,19 +43,23 @@ def get_all_files(directory: str = '.', extension: str = '.ttl'): return file_paths -def get_graphs_paths(graphs_dir: str = CURRENT_DIR) -> List[str]: +def get_graphs_paths( + graphs_dir: str = CURRENT_DIR, format="turtle") -> List[str]: """ Get all the SHACL shapes files in the shapes directory """ return get_all_files(directory=graphs_dir, extension='.ttl') -def get_full_graph(graphs_dir: str, publicID: str = ".") -> Graph: +def get_full_graph( + graphs_dir: str, + format: constants.RDF_SERIALIZATION_FORMATS_TYPES = "turtle", + publicID: str = ".") -> Graph: """ Get the SHACL shapes graph """ full_graph = Graph() - graphs_paths = get_graphs_paths(graphs_dir) + graphs_paths = get_graphs_paths(graphs_dir, format=format) for graph_path in graphs_paths: full_graph.parse(graph_path, format="turtle", publicID=publicID) logger.debug("Loaded triples from %s", graph_path) From 877c7fb00ca1b5cd72a2784a576855593a103afb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 15:32:35 +0100 Subject: [PATCH 026/902] docs(utils): :memo: add description of utility methods --- rocrate_validator/utils.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index daf501c4..e33cb5c3 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -15,6 +15,13 @@ def get_format_extension(format: constants.RDF_SERIALIZATION_FORMATS_TYPES) -> str: + """ + Get the file extension for the RDF serialization format + + :param format: The RDF serialization format + :return: The file extension + + :raises InvalidSerializationFormat: If the format is not valid """ try: return constants.RDF_SERIALIZATION_FILE_FORMAT_MAP[format] @@ -22,9 +29,17 @@ def get_format_extension(format: constants.RDF_SERIALIZATION_FORMATS_TYPES) -> s logger.error("Invalid RDF serialization format: %s", format) raise errors.InvalidSerializationFormat(format) + def get_all_files( directory: str = '.', format: constants.RDF_SERIALIZATION_FORMATS_TYPES = "turtle") -> List[str]: + """ + Get all the files in the directory matching the format. + + :param directory: The directory to search + :param format: The RDF serialization format + :return: A list of file paths + """ # initialize an empty list to store the file paths file_paths = [] @@ -46,9 +61,13 @@ def get_all_files( def get_graphs_paths( graphs_dir: str = CURRENT_DIR, format="turtle") -> List[str]: """ - Get all the SHACL shapes files in the shapes directory + Get the paths to all the graphs in the directory + + :param graphs_dir: The directory containing the graphs + :param format: The RDF serialization format + :return: A list of graph paths """ - return get_all_files(directory=graphs_dir, extension='.ttl') + return get_all_files(directory=graphs_dir, format=format) def get_full_graph( @@ -56,7 +75,12 @@ def get_full_graph( format: constants.RDF_SERIALIZATION_FORMATS_TYPES = "turtle", publicID: str = ".") -> Graph: """ - Get the SHACL shapes graph + Get the full graph from the directory + + :param graphs_dir: The directory containing the graphs + :param format: The RDF serialization format + :param publicID: The public ID + :return: The full graph """ full_graph = Graph() graphs_paths = get_graphs_paths(graphs_dir, format=format) From 730c3064345f0916bed55ebd4f929a07f3dd9460 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 15:39:07 +0100 Subject: [PATCH 027/902] refactor(core): :recycle: move constants to the constants module --- rocrate_validator/constants.py | 5 +++++ rocrate_validator/models.py | 10 +++------- rocrate_validator/service.py | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index 5a86a72e..a28c50b4 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -1,6 +1,11 @@ # Define allowed RDF extensions and serialization formats as map import typing +# Define SHACL namespace +SHACL_NS = "http://www.w3.org/ns/shacl#" +# Define the rocrate-metadata.json file name +ROCRATE_METADATA_FILE = "ro-crate-metadata.json" + # Define allowed RDF extensions and serialization formats as map RDF_SERIALIZATION_FILE_FORMAT_MAP = { diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 587da156..1ac68605 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1,20 +1,16 @@ import json import logging import os + from rdflib import Graph, URIRef from rdflib.term import Node +from .constants import SHACL_NS + # set up logging logger = logging.getLogger(__name__) -# Define SHACL namespace -SHACL_NS = "http://www.w3.org/ns/shacl#" - -# Define the rocrate-metadata.json file name -ROCRATE_METADATA_FILE = "ro-crate-metadata.json" - - class Shape: def __init__(self, shape_node: Node, graph: Graph) -> None: diff --git a/rocrate_validator/service.py b/rocrate_validator/service.py index 9c046a76..57213b5b 100644 --- a/rocrate_validator/service.py +++ b/rocrate_validator/service.py @@ -4,8 +4,8 @@ from pyshacl.pytypes import GraphLike from rdflib import Graph +from .constants import ROCRATE_METADATA_FILE from .errors import CheckValidationError, SHACLValidationError -from .models import ROCRATE_METADATA_FILE from .utils import get_full_graph from .validators.shacl import Validator From 14b729c699e60f678c4311f4b29ac4c549f02658 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 15:41:37 +0100 Subject: [PATCH 028/902] fix(core): :pencil2: instances --- rocrate_validator/checks/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/checks/__init__.py b/rocrate_validator/checks/__init__.py index 8e4e89de..940fd2da 100644 --- a/rocrate_validator/checks/__init__.py +++ b/rocrate_validator/checks/__init__.py @@ -12,7 +12,7 @@ __CURRENT_DIR__ = os.path.dirname(os.path.realpath(__file__)) -def get_checks(directory: str = __CURRENT_DIR__, instaces: bool = True) -> List[str]: +def get_checks(directory: str = __CURRENT_DIR__, instances: bool = True) -> List[str]: """ Load all the classes from the directory """ @@ -42,7 +42,7 @@ def get_checks(directory: str = __CURRENT_DIR__, instaces: bool = True) -> List[ and obj.__name__.endswith('Check'): classes[obj.__name__] = obj logger.debug("Loaded class %s", obj.__name__) - return [v() if instaces else v for v in classes.values()] + return [v() if instances else v for v in classes.values()] # return the list of classes return classes From 4bf62c9276833be9f68edf9b35b8acbde4ed74b5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 15:42:34 +0100 Subject: [PATCH 029/902] style(core): :recycle: sort imports --- rocrate_validator/checks/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/checks/__init__.py b/rocrate_validator/checks/__init__.py index 940fd2da..ec7afb8e 100644 --- a/rocrate_validator/checks/__init__.py +++ b/rocrate_validator/checks/__init__.py @@ -1,8 +1,7 @@ -from importlib import import_module import inspect -import os -import re import logging +import os +from importlib import import_module from typing import List # set up logging From a7bd83b2091ccb2eb9775a152c58133950200da9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 15:55:39 +0100 Subject: [PATCH 030/902] build(core): :package: add pyproject-flake8 --- poetry.lock | 65 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 6 +++-- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 23283f6e..55b9e157 100644 --- a/poetry.lock +++ b/poetry.lock @@ -89,6 +89,22 @@ files = [ [package.extras] toml = ["tomli"] +[[package]] +name = "flake8" +version = "6.1.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, + {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.1.0,<3.2.0" + [[package]] name = "html5lib" version = "1.1" @@ -178,6 +194,17 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -246,6 +273,28 @@ wcwidth = "*" [package.extras] tests = ["pytest", "pytest-cov", "pytest-lazy-fixtures"] +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[package]] +name = "pyflakes" +version = "3.1.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, + {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, +] + [[package]] name = "pygments" version = "2.17.2" @@ -275,6 +324,20 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pyproject-flake8" +version = "6.1.0" +description = "pyproject-flake8 (`pflake8`), a monkey patching wrapper to connect flake8 with pyproject.toml configuration" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "pyproject_flake8-6.1.0-py3-none-any.whl", hash = "sha256:86ea5559263c098e1aa4f866776aa2cf45362fd91a576b9fd8fbbbb55db12c4e"}, + {file = "pyproject_flake8-6.1.0.tar.gz", hash = "sha256:6da8e5a264395e0148bc11844c6fb50546f1fac83ac9210f7328664135f9e70f"}, +] + +[package.dependencies] +flake8 = "6.1.0" + [[package]] name = "pyshacl" version = "0.25.0" @@ -432,4 +495,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "3c5397b17913e101c33e68d8b2ecebbcddf14aef4f5e243e74464d22dcb5fdc9" +content-hash = "fbc31471c47585dbff8af7cfc82af6f689c1a7306fc0259594acc5ed63c7a096" diff --git a/pyproject.toml b/pyproject.toml index 0618503f..8f545ade 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,11 +13,13 @@ pyshacl = "^0.25.0" click = "^8.1.7" rich = "^13.7.1" - -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] +pyproject-flake8 = "^6.1.0" pytest = "^8.1.0" pytest-cov = "^4.1.0" +[tool.flake8] +max-line-length = 120 [build-system] requires = ["poetry-core"] From 0bca4ff67164caf659afbaeb7ef6cdc186c0080c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 18:50:33 +0100 Subject: [PATCH 031/902] feat(utils): :sparkles: add function to calculate the path of the rocratre descriptor --- rocrate_validator/utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index e33cb5c3..2d60a33d 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -1,5 +1,6 @@ import logging import os +from pathlib import Path from typing import List from rdflib import Graph @@ -14,6 +15,16 @@ logger = logging.getLogger(__name__) +def get_file_descriptor_path(rocrate_path: Path) -> Path: + """ + Get the path to the metadata file in the RO-Crate + + :param rocrate_path: The path to the RO-Crate + :return: The path to the metadata file + """ + return Path(rocrate_path) / constants.ROCRATE_METADATA_FILE + + def get_format_extension(format: constants.RDF_SERIALIZATION_FORMATS_TYPES) -> str: """ Get the file extension for the RDF serialization format From a674658ef4c61eb07f639f786c6b0cbcabcc2456 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 19:04:19 +0100 Subject: [PATCH 032/902] feat(core): :sparkles: re-implement base classes to represent checks --- rocrate_validator/checks/__init__.py | 205 ++++++++++++++++++++++++++- 1 file changed, 203 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/checks/__init__.py b/rocrate_validator/checks/__init__.py index ec7afb8e..99b65c3e 100644 --- a/rocrate_validator/checks/__init__.py +++ b/rocrate_validator/checks/__init__.py @@ -1,8 +1,15 @@ +from __future__ import annotations + import inspect import logging import os +from abc import ABC, abstractmethod +from enum import Enum, auto from importlib import import_module -from typing import List +from pathlib import Path +from typing import List, Optional, Type + +from ..utils import get_config, get_file_descriptor_path # set up logging logger = logging.getLogger(__name__) @@ -11,7 +18,201 @@ __CURRENT_DIR__ = os.path.dirname(os.path.realpath(__file__)) -def get_checks(directory: str = __CURRENT_DIR__, instances: bool = True) -> List[str]: +def issue_types(issues: List[Type[CheckIssue]]) -> Type[Check]: + def class_decorator(cls): + cls.issue_types = issues + return cls + return class_decorator + + +class CheckIssue: + """ + Class to store an issue found during a check + + Attributes: + severity (CheckIssue.IssueSeverity): The severity of the issue + message (str): The message + code (int): The code + """ + class IssueSeverity(Enum): + INFO = auto() + MAY = auto() + SHOULD = auto() + SHOULD_NOT = auto() + WARNING = auto() + MUST = auto() + MUST_NOT = auto() + ERROR = auto() + + def __init__(self, severity: IssueSeverity, message: Optional[str] = None, code: int = None): + self._severity = severity + self._message = message + self._code = code + + @property + def message(self) -> str: + """The message associated with the issue""" + return self._message + + @property + def severity(self) -> str: + """The severity of the issue""" + return self._severity + + @property + def code(self) -> int: + # If the code has not been set, calculate it + if not self._code: + """ + Calculate the code based on the severity, the class name and the message. + - All issues with the same severity, class name and message will have the same code. + - All issues with the same severity and class name but different message will have different codes. + - All issues with the same severity but different class name and message will have different codes. + - All issues with the same severity should start with the same number. + - All codes should be positive numbers. + """ + # Concatenate the severity, class name and message into a single string + issue_string = str(self._severity.value) + self.__class__.__name__ + str(self._message) + + # Use the built-in hash function to generate a unique code for this string + # The modulo operation ensures that the code is a positive number + self._code = hash(issue_string) % ((1 << 31) - 1) + # Return the code + return self._code + + +class Check(ABC): + """ + Base class for checks + """ + + def __init__(self, ro_crate_path: Path) -> None: + self._ro_crate_path = ro_crate_path + # create a result object for the check + self._result: CheckResult = CheckResult(self) + + @property + def name(self) -> str: + return self.__class__.__name__.replace("Check", "") + + @property + def description(self) -> str: + return self.__doc__.strip() + + @property + def ro_crate_path(self) -> Path: + return self._ro_crate_path + + @property + def file_descriptor_path(self) -> Path: + return get_file_descriptor_path(self.ro_crate_path) + + @property + def result(self) -> CheckResult: + return self._result + + def __do_check__(self) -> bool: + """ + Internal method to perform the check + """ + # Check if the check has issue types defined + assert self.issue_types, "Check must have issue types defined in the decorator" + # Perform the check + try: + return self.check() + except Exception as e: + self.check.result.add_error(str(e)) + logger.error("Unexpected error during check: %s", e) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return False + + @abstractmethod + def check(self) -> bool: + raise NotImplementedError("Check not implemented") + + def passed(self) -> bool: + return self.result.passed() + + def get_issues(self, severity: CheckIssue.IssueSeverity = CheckIssue.IssueSeverity.WARNING) -> List[CheckIssue]: + return self.result.get_issues(severity) + + def get_issues_by_severity(self, severity: CheckIssue.IssueSeverity) -> List[CheckIssue]: + return self.result.get_issues_by_severity(severity) + + def __str__(self) -> str: + return self.name + + def __repr__(self) -> str: + return f"{self.name}Check()" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Check): + return False + return self.name == other.name + + def __hash__(self) -> int: + return hash(self.name) + + +class CheckResult: + """ + Class to store the result of a check + + Attributes: + check (Check): The check that was performed + code (int): The result code + message (str): The message + """ + + def __init__(self, check: Check): + self.check = check + self._issues: List[CheckIssue] = [] + + @property + def issues(self) -> List[CheckIssue]: + return self._issues + + def add_issue(self, issue: CheckIssue): + self._issues.append(issue) + + def add_error(self, message: str, code: int = None): + self._issues.append(CheckIssue(CheckIssue.IssueSeverity.ERROR, message, code)) + + def add_warning(self, message: str, code: int = None): + self._issues.append(CheckIssue(CheckIssue.IssueSeverity.WARNING, message, code)) + + def add_info(self, message: str, code: int = None): + self._issues.append(CheckIssue(CheckIssue.IssueSeverity.INFO, message, code)) + + def add_optional(self, message: str, code: int = None): + self._issues.append(CheckIssue(CheckIssue.IssueSeverity.OPTIONAL, message, code)) + + def add_may(self, message: str, code: int = None): + self._issues.append(CheckIssue(CheckIssue.IssueSeverity.MAY, message, code)) + + def add_should(self, message: str, code: int = None): + self._issues.append(CheckIssue(CheckIssue.IssueSeverity.SHOULD, message, code)) + + def add_should_not(self, message: str, code: int = None): + self._issues.append(CheckIssue(CheckIssue.IssueSeverity.SHOULD_NOT, message, code)) + + def add_must(self, message: str, code: int = None): + self._issues.append(CheckIssue(CheckIssue.IssueSeverity.MUST, message, code)) + + def add_must_not(self, message: str, code: int = None): + self._issues.append(CheckIssue(CheckIssue.IssueSeverity.MUST_NOT, message, code)) + + def get_issues(self, severity: CheckIssue.IssueSeverity = CheckIssue.IssueSeverity.WARNING) -> List[CheckIssue]: + return [issue for issue in self.issues if issue.severity.value >= severity.value] + + def get_issues_by_severity(self, severity: CheckIssue.IssueSeverity) -> List[CheckIssue]: + return [issue for issue in self.issues if issue.severity == severity] + + def passed(self, severity: CheckIssue.IssueSeverity = CheckIssue.IssueSeverity.WARNING) -> bool: + return not any(issue.severity.value >= severity.value for issue in self.issues) + + """ Load all the classes from the directory """ From 2496e09a00c31d0bc7aba24709e723d6f582d854 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 19:05:53 +0100 Subject: [PATCH 033/902] feat(core): :sparkles: allow confiruging folders to skip --- pyproject.toml | 3 +++ rocrate_validator/checks/__init__.py | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8f545ade..40ce52f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,3 +27,6 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] rocrate-validator = "rocrate_validator.cli:cli" + +[tool.rocrate_validator] +skip_dirs = [".git", ".github", ".vscode"] diff --git a/rocrate_validator/checks/__init__.py b/rocrate_validator/checks/__init__.py index 99b65c3e..6a9f28d0 100644 --- a/rocrate_validator/checks/__init__.py +++ b/rocrate_validator/checks/__init__.py @@ -213,17 +213,28 @@ def passed(self, severity: CheckIssue.IssueSeverity = CheckIssue.IssueSeverity.W return not any(issue.severity.value >= severity.value for issue in self.issues) +def get_checks(directory: str = __CURRENT_DIR__, + rocrate_path: Path = ".", + instances: bool = True, + skip_dirs: List[str] = None) -> List[Type[Check]]: """ Load all the classes from the directory """ logger.debug("Loading checks from %s", directory) # create an empty list to store the classes classes = {} + # skip directories that start with a dot + skip_dirs = skip_dirs or [] + skip_dirs.extend(get_config(property="skip_dirs")) + # loop through the files in the directory for root, dirs, files in os.walk(directory): + # skip directories that start with a dot + dirs[:] = [d for d in dirs if not d.startswith('.')] + # loop through the files for file in files: # check if the file is a python file - logger.debug("Checking file %s", file) + logger.debug("Checking file %s %s %s", root, dirs, file) if file.endswith(".py") and not file.startswith("__init__"): # get the file path file_path = os.path.join(root, file) @@ -239,10 +250,11 @@ def passed(self, severity: CheckIssue.IssueSeverity = CheckIssue.IssueSeverity.W logger.debug("Checking object %s", obj) if inspect.isclass(obj) \ and inspect.getmodule(obj) == mod \ + and issubclass(obj, Check) \ and obj.__name__.endswith('Check'): classes[obj.__name__] = obj logger.debug("Loaded class %s", obj.__name__) - return [v() if instances else v for v in classes.values()] + return [v(rocrate_path) if instances else v for v in classes.values()] # return the list of classes return classes From 905dad3719e5eba8da6a1f513536c999838920b0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 9 Mar 2024 19:06:22 +0100 Subject: [PATCH 034/902] build(core): :package: add toml package --- poetry.lock | 13 ++++++++++++- pyproject.toml | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 55b9e157..017d7b1a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -455,6 +455,17 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + [[package]] name = "wcwidth" version = "0.2.13" @@ -495,4 +506,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "fbc31471c47585dbff8af7cfc82af6f689c1a7306fc0259594acc5ed63c7a096" +content-hash = "2ab5150ef75873dd9c5023cc9e4a10b43c918586a8014930434b0a6d18bbdc9a" diff --git a/pyproject.toml b/pyproject.toml index 40ce52f6..811cef32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ rdflib = "^7.0.0" pyshacl = "^0.25.0" click = "^8.1.7" rich = "^13.7.1" +toml = "^0.10.2" [tool.poetry.group.dev.dependencies] pyproject-flake8 = "^6.1.0" From 2e7143986821bce536ff4edc7122ecb445e6478c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 10 Mar 2024 09:32:46 +0100 Subject: [PATCH 035/902] feat(core): :sparkles: add model to represent the global validation result --- rocrate_validator/checks/__init__.py | 50 +++---- rocrate_validator/models.py | 208 ++++----------------------- rocrate_validator/service.py | 53 +++++-- 3 files changed, 94 insertions(+), 217 deletions(-) diff --git a/rocrate_validator/checks/__init__.py b/rocrate_validator/checks/__init__.py index 6a9f28d0..62336d9d 100644 --- a/rocrate_validator/checks/__init__.py +++ b/rocrate_validator/checks/__init__.py @@ -25,24 +25,26 @@ def class_decorator(cls): return class_decorator +class IssueSeverity(Enum): + INFO = auto() + MAY = auto() + SHOULD = auto() + SHOULD_NOT = auto() + WARNING = auto() + MUST = auto() + MUST_NOT = auto() + ERROR = auto() + + class CheckIssue: """ Class to store an issue found during a check Attributes: - severity (CheckIssue.IssueSeverity): The severity of the issue + severity (IssueSeverity): The severity of the issue message (str): The message code (int): The code """ - class IssueSeverity(Enum): - INFO = auto() - MAY = auto() - SHOULD = auto() - SHOULD_NOT = auto() - WARNING = auto() - MUST = auto() - MUST_NOT = auto() - ERROR = auto() def __init__(self, severity: IssueSeverity, message: Optional[str] = None, code: int = None): self._severity = severity @@ -134,10 +136,10 @@ def check(self) -> bool: def passed(self) -> bool: return self.result.passed() - def get_issues(self, severity: CheckIssue.IssueSeverity = CheckIssue.IssueSeverity.WARNING) -> List[CheckIssue]: + def get_issues(self, severity: IssueSeverity = IssueSeverity.WARNING) -> List[CheckIssue]: return self.result.get_issues(severity) - def get_issues_by_severity(self, severity: CheckIssue.IssueSeverity) -> List[CheckIssue]: + def get_issues_by_severity(self, severity: IssueSeverity) -> List[CheckIssue]: return self.result.get_issues_by_severity(severity) def __str__(self) -> str: @@ -177,39 +179,39 @@ def add_issue(self, issue: CheckIssue): self._issues.append(issue) def add_error(self, message: str, code: int = None): - self._issues.append(CheckIssue(CheckIssue.IssueSeverity.ERROR, message, code)) + self._issues.append(CheckIssue(IssueSeverity.ERROR, message, code)) def add_warning(self, message: str, code: int = None): - self._issues.append(CheckIssue(CheckIssue.IssueSeverity.WARNING, message, code)) + self._issues.append(CheckIssue(IssueSeverity.WARNING, message, code)) def add_info(self, message: str, code: int = None): - self._issues.append(CheckIssue(CheckIssue.IssueSeverity.INFO, message, code)) + self._issues.append(CheckIssue(IssueSeverity.INFO, message, code)) def add_optional(self, message: str, code: int = None): - self._issues.append(CheckIssue(CheckIssue.IssueSeverity.OPTIONAL, message, code)) + self._issues.append(CheckIssue(IssueSeverity.OPTIONAL, message, code)) def add_may(self, message: str, code: int = None): - self._issues.append(CheckIssue(CheckIssue.IssueSeverity.MAY, message, code)) + self._issues.append(CheckIssue(IssueSeverity.MAY, message, code)) def add_should(self, message: str, code: int = None): - self._issues.append(CheckIssue(CheckIssue.IssueSeverity.SHOULD, message, code)) + self._issues.append(CheckIssue(IssueSeverity.SHOULD, message, code)) def add_should_not(self, message: str, code: int = None): - self._issues.append(CheckIssue(CheckIssue.IssueSeverity.SHOULD_NOT, message, code)) + self._issues.append(CheckIssue(IssueSeverity.SHOULD_NOT, message, code)) def add_must(self, message: str, code: int = None): - self._issues.append(CheckIssue(CheckIssue.IssueSeverity.MUST, message, code)) + self._issues.append(CheckIssue(IssueSeverity.MUST, message, code)) def add_must_not(self, message: str, code: int = None): - self._issues.append(CheckIssue(CheckIssue.IssueSeverity.MUST_NOT, message, code)) + self._issues.append(CheckIssue(IssueSeverity.MUST_NOT, message, code)) - def get_issues(self, severity: CheckIssue.IssueSeverity = CheckIssue.IssueSeverity.WARNING) -> List[CheckIssue]: + def get_issues(self, severity: IssueSeverity = IssueSeverity.WARNING) -> List[CheckIssue]: return [issue for issue in self.issues if issue.severity.value >= severity.value] - def get_issues_by_severity(self, severity: CheckIssue.IssueSeverity) -> List[CheckIssue]: + def get_issues_by_severity(self, severity: IssueSeverity) -> List[CheckIssue]: return [issue for issue in self.issues if issue.severity == severity] - def passed(self, severity: CheckIssue.IssueSeverity = CheckIssue.IssueSeverity.WARNING) -> bool: + def passed(self, severity: IssueSeverity = IssueSeverity.WARNING) -> bool: return not any(issue.severity.value >= severity.value for issue in self.issues) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 1ac68605..b9984b09 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1,194 +1,42 @@ -import json -import logging -import os +from pathlib import Path +from typing import Dict, List -from rdflib import Graph, URIRef -from rdflib.term import Node - -from .constants import SHACL_NS - -# set up logging -logger = logging.getLogger(__name__) - - -class Shape: - - def __init__(self, shape_node: Node, graph: Graph) -> None: - # check the input - assert isinstance(shape_node, Node), "Invalid shape node" - assert isinstance(graph, Graph), "Invalid graph" - - # store the input - self._shape_node = shape_node - self._graph = graph - - # create a graph for the shape - shape_graph = Graph() - shape_graph += graph.triples((shape_node, None, None)) - self.shape_graph = shape_graph - - # serialize the graph in json-ld - shape_json = shape_graph.serialize(format="json-ld") - shape_obj = json.loads(shape_json) - self.shape_json = shape_obj[0] - - @property - def node(self) -> Node: - return self._shape_node - - @property - def graph(self) -> Graph: - return self._graph - - @property - def name(self): - return self.shape_json[f'{SHACL_NS}name'][0]['@value'] - - @property - def description(self): - return self.shape_json[f'{SHACL_NS}description'][0]['@value'] - - @property - def path(self): - return self.shape_json[f'{SHACL_NS}path'][0]['@id'] - - @property - def nodeKind(self): - return self.shape_json[f'{SHACL_NS}nodeKind'][0]['@id'] - - -class Violation: - - def __init__(self, violation_node: Node, graph: Graph) -> None: - # check the input - assert isinstance(violation_node, Node), "Invalid violation node" - assert isinstance(graph, Graph), "Invalid graph" - - # store the input - self._violation_node = violation_node - self._graph = graph - - # create a graph for the violation - violation_graph = Graph() - violation_graph += graph.triples((violation_node, None, None)) - self.violation_graph = violation_graph - - # serialize the graph in json-ld - violation_json = violation_graph.serialize(format="json-ld") - violation_obj = json.loads(violation_json) - self.violation_json = violation_obj[0] - - # get the source shape - shapes = list(graph.triples( - (violation_node, URIRef(f"{SHACL_NS}sourceShape"), None))) - self.source_shape_node = shapes[0][2] - - @property - def node(self) -> Node: - return self._violation_node - - @property - def graph(self) -> Graph: - return self._graph - - @property - def resultSeverity(self): - return self.violation_json[f'{SHACL_NS}resultSeverity'][0]['@id'] - - @property - def focusNode(self): - return self.violation_json[f'{SHACL_NS}focusNode'][0]['@id'] - - @property - def resultPath(self): - return self.violation_json[f'{SHACL_NS}resultPath'][0]['@id'] - - @property - def value(self): - value = self.violation_json.get(f'{SHACL_NS}value', None) - if not value: - return None - return value[0]['@id'] - - @property - def resultMessage(self): - return self.violation_json[f'{SHACL_NS}resultMessage'][0]['@value'] - - @property - def sourceConstraintComponent(self): - return self.violation_json[f'{SHACL_NS}sourceConstraintComponent'][0]['@id'] - - @property - def sourceShape(self) -> Shape: - return Shape(self.source_shape_node, self._graph) +from .checks import CheckIssue, IssueSeverity class ValidationResult: - def __init__(self, results_graph: Graph, conforms: bool = None, results_text: str = None) -> None: - # validate the results graph input - assert results_graph is not None, "Invalid graph" - assert isinstance(results_graph, Graph), "Invalid graph type" - # check if the graph is valid ValidationReport - assert (None, URIRef(f"{SHACL_NS}conforms"), - None) in results_graph, "Invalid ValidationReport" - # store the input properties - self._conforms = conforms - self.results_graph = results_graph - self._text = results_text - # parse the results graph - self._violations = self.parse_results_graph(results_graph) - # initialize the conforms property - if conforms is not None: - self._conforms = len(self._violations) == 0 - else: - assert self._conforms == len( - self._violations) == 0, "Invalid validation result" + def __init__(self, rocrate_path: Path, validation_settings: Dict = None): + self._issues: List[CheckIssue] = [] + self._rocrate_path = rocrate_path + self._validation_settings = validation_settings + + def get_rocrate_path(self): + return self._rocrate_path - @staticmethod - def parse_results_graph(results_graph: Graph): - # Query for validation results - query = """ - SELECT ?subject - WHERE {{ - ?subject a <{0}ValidationResult> . - }} - """.format(SHACL_NS) + def get_validation_settings(self): + return self._validation_settings - query_results = results_graph.query(query) + def add_issue(self, issue: CheckIssue): + self._issues.append(issue) - violations = [] - for r in query_results: - violation_node = r[0] - violation = Violation(violation_node, results_graph) - violations.append(violation) + def add_issues(self, issues: List[CheckIssue]): + self._issues.extend(issues) - return violations + def get_issues(self, severity: IssueSeverity = IssueSeverity.WARNING) -> List[CheckIssue]: + return [issue for issue in self._issues if issue.severity.value >= severity.value] - @property - def conforms(self) -> bool: - return self._conforms + def get_issues_by_severity(self, severity: IssueSeverity) -> List[CheckIssue]: + return [issue for issue in self._issues if issue.severity == severity] - @property - def violations(self) -> list: - return self._violations + def has_issues(self, severity: IssueSeverity = IssueSeverity.WARNING) -> bool: + return any(issue.severity.value >= severity.value for issue in self._issues) - @property - def text(self) -> str: - return self._text + def passed(self, severity: IssueSeverity = IssueSeverity.WARNING) -> bool: + return not any(issue.severity.value >= severity.value for issue in self._issues) - @staticmethod - def from_serialised_results_graph(file_path: str, format: str = 'turtle'): - # check the input - assert format in ['turtle', 'n3', 'nt', - 'xml', 'rdf', 'json-ld'], "Invalid format" - assert file_path, "Invalid file path" - assert os.path.exists(file_path), "File does not exist" - # Load the graph - logger.debug("Loading graph from file: %s" % file_path) - g = Graph() - g.parse(file_path, format=format) - logger.debug("Graph loaded from file: %s" % file_path) + def __str__(self): + return f"Validation result: {len(self._issues)} issues" - # return the validation result - return ValidationResult(g) + def __repr__(self): + return f"ValidationResult(issues={self._issues})" diff --git a/rocrate_validator/service.py b/rocrate_validator/service.py index 57213b5b..d49cdd4e 100644 --- a/rocrate_validator/service.py +++ b/rocrate_validator/service.py @@ -8,6 +8,7 @@ from .errors import CheckValidationError, SHACLValidationError from .utils import get_full_graph from .validators.shacl import Validator +from .models import ValidationResult # set up logging logger = logging.getLogger(__name__) @@ -25,8 +26,9 @@ def validate( allow_warnings: Optional[bool] = False, serialization_output_path: str = None, serialization_output_format: str = "turtle", + fail_fast: Optional[bool] = True, **kwargs, -) -> bool: +) -> ValidationResult: """ Validate a data graph using SHACL shapes as constraints @@ -44,35 +46,58 @@ def validate( from .checks import get_checks + # keep track of the validation settings + validation_settings = { + "shapes_path": shapes_path, + "ontologies_path": ontologies_path, + "advanced": advanced, + "inference": inference, + "inplace": inplace, + "abort_on_first": abort_on_first, + "allow_infos": allow_infos, + "allow_warnings": allow_warnings, + "serialization_output_path": serialization_output_path, + "serialization_output_format": serialization_output_format, + **kwargs, + } + + # initialize the validation result + validation_result = ValidationResult(rocrate_path=rocrate_path, validation_settings=validation_settings) + # get the checks - for check_instance in get_checks(): + for check_instance in get_checks(rocrate_path=rocrate_path): logger.debug("Loaded check: %s", check_instance) - result = check_instance.check(rocrate_path) - if result[0] == 0: + result = check_instance.check() + if result: logger.debug("Check passed: %s", check_instance.name) else: - logger.debug( - f"Check {check_instance.name} failed: " - f"{result[1]}", check_instance.name - ) - raise CheckValidationError( - check_instance, result[1], rocrate_path, result[0] - ) + logger.debug(f"Check {check_instance.name} failed ") + validation_result.add_issues(check_instance.get_issues()) + if fail_fast: + logger.debug("Failing fast") + return validation_result + # issues = check_instance.get_issues() + # raise CheckValidationError( + # check_instance, issues[0].message, rocrate_path, issues[0].code + # ) # load the data graph data_graph = Graph() data_graph.parse(rocrate_metadata_path, format="json-ld", publicID=rocrate_path) + validation_settings["data_graph"] = data_graph # load the shapes graph shacl_graph = None if shapes_path: shacl_graph = get_full_graph(shapes_path, publicID=rocrate_path) + validation_settings["shapes_graph"] = shacl_graph # load the ontology graph ontology_graph = None if ontologies_path: ontology_graph = get_full_graph(ontologies_path) + validation_settings["ont_graph"] = ontology_graph validator = Validator( shapes_graph=shacl_graph, ont_graph=ontology_graph) @@ -91,5 +116,7 @@ def validate( ) logger.debug("Validation conforms: %s", result.conforms) if not result.conforms: - raise SHACLValidationError(result, rocrate_path) - return True + logger.error("Validation failed") + # TODO: wrap the validation error as issue + + return validation_result From f223ec8e7c155e867963f872e63d2121a32d5658 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 10 Mar 2024 09:33:19 +0100 Subject: [PATCH 036/902] feat(utils): :sparkles: allow to read config options --- rocrate_validator/utils.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 2d60a33d..4110e3c9 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -3,6 +3,7 @@ from pathlib import Path from typing import List +import toml from rdflib import Graph from . import constants, errors @@ -14,6 +15,30 @@ # set up logging logger = logging.getLogger(__name__) +# Read the pyproject.toml file +config = toml.load("pyproject.toml") + + +def get_version() -> str: + """ + Get the version of the package + + :return: The version + """ + return config["tool"]["poetry"]["version"] + + +def get_config(property: str = None) -> dict: + """ + Get the configuration for the package or a specific property + + :param property_name: The property name + :return: The configuration + """ + if property: + return config["tool"]["rocrate_validator"][property] + return config["tool"]["rocrate_validator"] + def get_file_descriptor_path(rocrate_path: Path) -> Path: """ From 1a9cbe2cd2cb110549aa094e66ab7066b9ae464e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 10 Mar 2024 09:35:41 +0100 Subject: [PATCH 037/902] style(cli): :lipstick: update layout of CLI output messages --- rocrate_validator/cli.py | 77 ++++++++++++++++++++++++++++--------- rocrate_validator/colors.py | 28 ++++++++++++++ 2 files changed, 87 insertions(+), 18 deletions(-) create mode 100644 rocrate_validator/colors.py diff --git a/rocrate_validator/cli.py b/rocrate_validator/cli.py index 55d0755c..81e63167 100644 --- a/rocrate_validator/cli.py +++ b/rocrate_validator/cli.py @@ -3,9 +3,14 @@ import click from rich.console import Console + from rocrate_validator.errors import CheckValidationError, SHACLValidationError from rocrate_validator.service import validate as validate_rocrate +from .checks import IssueSeverity +from .colors import get_severity_color +from .models import ValidationResult + # set up logging logger = logging.getLogger(__name__) @@ -52,7 +57,16 @@ def cli(ctx, debug, shapes_path, ontologies_path=None, rocrate_path="."): @cli.command("validate") -def validate(shapes_path: str, ontologies_path: str = None, rocrate_path: str = "."): +# ??? is it relevant ? +@click.option( + '-x', + '--fail-fast', + is_flag=True, + help="Fail fast", + default=True +) +def validate(shapes_path: str, ontologies_path: str = None, + rocrate_path: str = ".", fail_fast: bool = True): """ Validate a RO-Crate using SHACL shapes as constraints. * this command might be the only one needed for the CLI. @@ -70,7 +84,7 @@ def validate(shapes_path: str, ontologies_path: str = None, rocrate_path: str = try: # Validate the RO-Crate - result = validate_rocrate( + result: ValidationResult = validate_rocrate( rocrate_path=os.path.abspath(rocrate_path), shapes_path=os.path.abspath(shapes_path) if shapes_path else None, ontologies_path=os.path.abspath( @@ -78,29 +92,56 @@ def validate(shapes_path: str, ontologies_path: str = None, rocrate_path: str = ) # Print the validation result - logger.debug("Validation conforms: %s" % result) - console.print( - "\n\n[bold]\[[green]OK[/green]] RO-Crate is valid!!![/bold]\n\n", - style="white", - ) + __print_validation_result__(result) except Exception as e: console.print( - "\n\n[bold]\[[red]FAILED[/red]] RO-Crate is not valid!!![/bold]\n", + "\n\n[bold]\[[red]FAILED[/red]] Unexpected error !!![/bold]\n", style="white", ) if logger.isEnabledFor(logging.DEBUG): - console.print_exception() - if isinstance(e, CheckValidationError): + console.print_exception(e) + # if isinstance(e, CheckValidationError): + # console.print( + # f"Check [bold][red]{e.check.name}[/red][/bold] failed: ") + # console.print(f" -> {str(e)}\n\n", style="white") + # elif isinstance(e, SHACLValidationError): + # _log_validation_result_(e.result) + # else: + # console.print("Error: ", style="red", end="") + # console.print(f" -> {str(e)}\n\n", style="white") + # console.print("\n\n", style="white") + + +def __print_validation_result__( + result: ValidationResult, + severity: IssueSeverity = IssueSeverity.WARNING): + """ + Print the validation result + """ + if result.passed(severity=severity): + console.print( + "\n\n[bold]\[[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", + style="white", + ) + else: + console.print( + "\n\n[bold]\[[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", + style="white", + ) + + for issue in result.get_issues(severity=severity): + issue_color = get_severity_color(issue.severity) + console.print( + f" -> [bold][magenta]{issue.check.name}[/magenta] check [red]failed[/red][/bold]", + style="white", + ) + console.print(f"{' '*4}{issue.check.description}\n", style="white italic") + console.print(f"{' '*4}Detected issues:", style="white bold") console.print( - f"Check [bold][red]{e.check.name}[/red][/bold] failed: ") - console.print(f" -> {str(e)}\n\n", style="white") - elif isinstance(e, SHACLValidationError): - _log_validation_result_(e.result) - else: - console.print("Error: ", style="red", end="") - console.print(f" -> {str(e)}\n\n", style="white") - console.print("\n\n", style="white") + f"{' '*4}- [[{issue_color}]{issue.severity.name}[/{issue_color}] " + f"[magenta]{issue.code}[/magenta]]: {issue.message}\n\n", + style="white") def _log_validation_result_(result: bool): diff --git a/rocrate_validator/colors.py b/rocrate_validator/colors.py new file mode 100644 index 00000000..1613cdde --- /dev/null +++ b/rocrate_validator/colors.py @@ -0,0 +1,28 @@ +from .checks import IssueSeverity + + +def get_severity_color(severity: IssueSeverity) -> str: + """ + Get the color for the severity + + :param severity: The severity + :return: The color + """ + if severity == IssueSeverity.ERROR: + return "red" + elif severity == IssueSeverity.MUST: + return "red" + elif severity == IssueSeverity.MUST_NOT: + return "purple" + elif severity == IssueSeverity.SHOULD: + return "yellow" + elif severity == IssueSeverity.SHOULD_NOT: + return "lightyellow" + elif severity == IssueSeverity.MAY: + return "orange" + elif severity == IssueSeverity.INFO: + return "lightblue" + elif severity == IssueSeverity.WARNING: + return "yellow green" + else: + return "white" From ad5c02b97d965d86b054d8d09f5ba1af24aca93a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 10 Mar 2024 10:44:19 +0100 Subject: [PATCH 038/902] feat(core): :sparkles: extend the issue model with the link to the related check --- rocrate_validator/checks/__init__.py | 30 ++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/rocrate_validator/checks/__init__.py b/rocrate_validator/checks/__init__.py index 62336d9d..e235031f 100644 --- a/rocrate_validator/checks/__init__.py +++ b/rocrate_validator/checks/__init__.py @@ -46,10 +46,14 @@ class CheckIssue: code (int): The code """ - def __init__(self, severity: IssueSeverity, message: Optional[str] = None, code: int = None): + def __init__(self, severity: IssueSeverity, + message: Optional[str] = None, + code: int = None, + check: Check = None): self._severity = severity self._message = message self._code = code + self._check = check @property def message(self) -> str: @@ -61,6 +65,11 @@ def severity(self) -> str: """The severity of the issue""" return self._severity + @property + def check(self) -> Check: + """The check that generated the issue""" + return self._check + @property def code(self) -> int: # If the code has not been set, calculate it @@ -176,34 +185,35 @@ def issues(self) -> List[CheckIssue]: return self._issues def add_issue(self, issue: CheckIssue): + issue._check = self.check self._issues.append(issue) def add_error(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.ERROR, message, code)) + self._issues.append(CheckIssue(IssueSeverity.ERROR, message, code, self.check)) def add_warning(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.WARNING, message, code)) + self._issues.append(CheckIssue(IssueSeverity.WARNING, message, code, self.check)) def add_info(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.INFO, message, code)) + self._issues.append(CheckIssue(IssueSeverity.INFO, message, code, self.check)) def add_optional(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.OPTIONAL, message, code)) + self._issues.append(CheckIssue(IssueSeverity.OPTIONAL, message, code, self.check)) def add_may(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.MAY, message, code)) + self._issues.append(CheckIssue(IssueSeverity.MAY, message, code, self.check)) def add_should(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.SHOULD, message, code)) + self._issues.append(CheckIssue(IssueSeverity.SHOULD, message, code, self.check)) def add_should_not(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.SHOULD_NOT, message, code)) + self._issues.append(CheckIssue(IssueSeverity.SHOULD_NOT, message, code, self.check)) def add_must(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.MUST, message, code)) + self._issues.append(CheckIssue(IssueSeverity.MUST, message, code, self.check)) def add_must_not(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.MUST_NOT, message, code)) + self._issues.append(CheckIssue(IssueSeverity.MUST_NOT, message, code, self.check)) def get_issues(self, severity: IssueSeverity = IssueSeverity.WARNING) -> List[CheckIssue]: return [issue for issue in self.issues if issue.severity.value >= severity.value] From 8f28d0b705b09db08d9daa9c0798597425b711da Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 10 Mar 2024 10:48:10 +0100 Subject: [PATCH 039/902] refactor(core): :recycle: rename enum to represent the severity level --- rocrate_validator/checks/__init__.py | 32 ++++++++++++++-------------- rocrate_validator/cli.py | 4 ++-- rocrate_validator/colors.py | 20 ++++++++--------- rocrate_validator/models.py | 10 ++++----- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/rocrate_validator/checks/__init__.py b/rocrate_validator/checks/__init__.py index e235031f..02425362 100644 --- a/rocrate_validator/checks/__init__.py +++ b/rocrate_validator/checks/__init__.py @@ -25,7 +25,7 @@ def class_decorator(cls): return class_decorator -class IssueSeverity(Enum): +class Severity(Enum): INFO = auto() MAY = auto() SHOULD = auto() @@ -46,7 +46,7 @@ class CheckIssue: code (int): The code """ - def __init__(self, severity: IssueSeverity, + def __init__(self, severity: Severity, message: Optional[str] = None, code: int = None, check: Check = None): @@ -145,10 +145,10 @@ def check(self) -> bool: def passed(self) -> bool: return self.result.passed() - def get_issues(self, severity: IssueSeverity = IssueSeverity.WARNING) -> List[CheckIssue]: + def get_issues(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: return self.result.get_issues(severity) - def get_issues_by_severity(self, severity: IssueSeverity) -> List[CheckIssue]: + def get_issues_by_severity(self, severity: Severity) -> List[CheckIssue]: return self.result.get_issues_by_severity(severity) def __str__(self) -> str: @@ -189,39 +189,39 @@ def add_issue(self, issue: CheckIssue): self._issues.append(issue) def add_error(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.ERROR, message, code, self.check)) + self._issues.append(CheckIssue(Severity.ERROR, message, code, self.check)) def add_warning(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.WARNING, message, code, self.check)) + self._issues.append(CheckIssue(Severity.WARNING, message, code, self.check)) def add_info(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.INFO, message, code, self.check)) + self._issues.append(CheckIssue(Severity.INFO, message, code, self.check)) def add_optional(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.OPTIONAL, message, code, self.check)) + self._issues.append(CheckIssue(Severity.OPTIONAL, message, code, self.check)) def add_may(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.MAY, message, code, self.check)) + self._issues.append(CheckIssue(Severity.MAY, message, code, self.check)) def add_should(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.SHOULD, message, code, self.check)) + self._issues.append(CheckIssue(Severity.SHOULD, message, code, self.check)) def add_should_not(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.SHOULD_NOT, message, code, self.check)) + self._issues.append(CheckIssue(Severity.SHOULD_NOT, message, code, self.check)) def add_must(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.MUST, message, code, self.check)) + self._issues.append(CheckIssue(Severity.MUST, message, code, self.check)) def add_must_not(self, message: str, code: int = None): - self._issues.append(CheckIssue(IssueSeverity.MUST_NOT, message, code, self.check)) + self._issues.append(CheckIssue(Severity.MUST_NOT, message, code, self.check)) - def get_issues(self, severity: IssueSeverity = IssueSeverity.WARNING) -> List[CheckIssue]: + def get_issues(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: return [issue for issue in self.issues if issue.severity.value >= severity.value] - def get_issues_by_severity(self, severity: IssueSeverity) -> List[CheckIssue]: + def get_issues_by_severity(self, severity: Severity) -> List[CheckIssue]: return [issue for issue in self.issues if issue.severity == severity] - def passed(self, severity: IssueSeverity = IssueSeverity.WARNING) -> bool: + def passed(self, severity: Severity = Severity.WARNING) -> bool: return not any(issue.severity.value >= severity.value for issue in self.issues) diff --git a/rocrate_validator/cli.py b/rocrate_validator/cli.py index 81e63167..d1708553 100644 --- a/rocrate_validator/cli.py +++ b/rocrate_validator/cli.py @@ -7,7 +7,7 @@ from rocrate_validator.errors import CheckValidationError, SHACLValidationError from rocrate_validator.service import validate as validate_rocrate -from .checks import IssueSeverity +from .checks import Severity from .colors import get_severity_color from .models import ValidationResult @@ -115,7 +115,7 @@ def validate(shapes_path: str, ontologies_path: str = None, def __print_validation_result__( result: ValidationResult, - severity: IssueSeverity = IssueSeverity.WARNING): + severity: Severity = Severity.WARNING): """ Print the validation result """ diff --git a/rocrate_validator/colors.py b/rocrate_validator/colors.py index 1613cdde..eb6a36cc 100644 --- a/rocrate_validator/colors.py +++ b/rocrate_validator/colors.py @@ -1,28 +1,28 @@ -from .checks import IssueSeverity +from .checks import Severity -def get_severity_color(severity: IssueSeverity) -> str: +def get_severity_color(severity: Severity) -> str: """ Get the color for the severity :param severity: The severity :return: The color """ - if severity == IssueSeverity.ERROR: + if severity == Severity.ERROR: return "red" - elif severity == IssueSeverity.MUST: + elif severity == Severity.MUST: return "red" - elif severity == IssueSeverity.MUST_NOT: + elif severity == Severity.MUST_NOT: return "purple" - elif severity == IssueSeverity.SHOULD: + elif severity == Severity.SHOULD: return "yellow" - elif severity == IssueSeverity.SHOULD_NOT: + elif severity == Severity.SHOULD_NOT: return "lightyellow" - elif severity == IssueSeverity.MAY: + elif severity == Severity.MAY: return "orange" - elif severity == IssueSeverity.INFO: + elif severity == Severity.INFO: return "lightblue" - elif severity == IssueSeverity.WARNING: + elif severity == Severity.WARNING: return "yellow green" else: return "white" diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index b9984b09..28953533 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1,7 +1,7 @@ from pathlib import Path from typing import Dict, List -from .checks import CheckIssue, IssueSeverity +from .checks import CheckIssue, Severity class ValidationResult: @@ -23,16 +23,16 @@ def add_issue(self, issue: CheckIssue): def add_issues(self, issues: List[CheckIssue]): self._issues.extend(issues) - def get_issues(self, severity: IssueSeverity = IssueSeverity.WARNING) -> List[CheckIssue]: + def get_issues(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: return [issue for issue in self._issues if issue.severity.value >= severity.value] - def get_issues_by_severity(self, severity: IssueSeverity) -> List[CheckIssue]: + def get_issues_by_severity(self, severity: Severity) -> List[CheckIssue]: return [issue for issue in self._issues if issue.severity == severity] - def has_issues(self, severity: IssueSeverity = IssueSeverity.WARNING) -> bool: + def has_issues(self, severity: Severity = Severity.WARNING) -> bool: return any(issue.severity.value >= severity.value for issue in self._issues) - def passed(self, severity: IssueSeverity = IssueSeverity.WARNING) -> bool: + def passed(self, severity: Severity = Severity.WARNING) -> bool: return not any(issue.severity.value >= severity.value for issue in self._issues) def __str__(self): From eb74a017197d23cb21ad3fb3327d160a3845712c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 10 Mar 2024 11:05:23 +0100 Subject: [PATCH 040/902] feat(core): :sparkles: extend list of Requirement Levels --- rocrate_validator/checks/__init__.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/checks/__init__.py b/rocrate_validator/checks/__init__.py index 02425362..119ca4c4 100644 --- a/rocrate_validator/checks/__init__.py +++ b/rocrate_validator/checks/__init__.py @@ -26,14 +26,25 @@ def class_decorator(cls): class Severity(Enum): + """ + * The key words MUST, MUST NOT, REQUIRED, + * SHALL, SHALL NOT, SHOULD, SHOULD NOT, + * RECOMMENDED, MAY, and OPTIONAL in this document + * are to be interpreted as described in RFC 2119. + """ INFO = auto() MAY = auto() + OPTIONAL = auto() SHOULD = auto() SHOULD_NOT = auto() WARNING = auto() + ERROR = auto() + REQUIRED = auto() MUST = auto() MUST_NOT = auto() - ERROR = auto() + SHALL = auto() + SHALL_NOT = auto() + RECOMMENDED = auto() class CheckIssue: From d53e6df218d4df5cc1c786e64c0ee0a31bdf5845 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 10 Mar 2024 11:21:01 +0100 Subject: [PATCH 041/902] fix(core): :alembic: try to assign weight to severity levels --- rocrate_validator/checks/__init__.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/rocrate_validator/checks/__init__.py b/rocrate_validator/checks/__init__.py index 119ca4c4..840c8a6f 100644 --- a/rocrate_validator/checks/__init__.py +++ b/rocrate_validator/checks/__init__.py @@ -32,19 +32,19 @@ class Severity(Enum): * RECOMMENDED, MAY, and OPTIONAL in this document * are to be interpreted as described in RFC 2119. """ - INFO = auto() - MAY = auto() - OPTIONAL = auto() - SHOULD = auto() - SHOULD_NOT = auto() - WARNING = auto() - ERROR = auto() - REQUIRED = auto() - MUST = auto() - MUST_NOT = auto() - SHALL = auto() - SHALL_NOT = auto() - RECOMMENDED = auto() + INFO = 0 + MAY = 1 + OPTIONAL = 1 + SHOULD = 2 + SHOULD_NOT = 2 + WARNING = 2 + REQUIRED = 3 + MUST = 3 + MUST_NOT = 3 + SHALL = 3 + SHALL_NOT = 3 + RECOMMENDED = 3 + ERROR = 4 class CheckIssue: From 21a1e30bbe0d1f70695c9ce98df69567c3a414c8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 10 Mar 2024 11:59:08 +0100 Subject: [PATCH 042/902] feat(core): :sparkles: better representation of requirement levels --- rocrate_validator/checks/__init__.py | 28 ++++----------- rocrate_validator/profiles.py | 51 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 21 deletions(-) create mode 100644 rocrate_validator/profiles.py diff --git a/rocrate_validator/checks/__init__.py b/rocrate_validator/checks/__init__.py index 840c8a6f..1244020c 100644 --- a/rocrate_validator/checks/__init__.py +++ b/rocrate_validator/checks/__init__.py @@ -4,11 +4,12 @@ import logging import os from abc import ABC, abstractmethod -from enum import Enum, auto from importlib import import_module from pathlib import Path from typing import List, Optional, Type +from ..profiles import RequirementLevels, RequirementType + from ..utils import get_config, get_file_descriptor_path # set up logging @@ -25,26 +26,11 @@ def class_decorator(cls): return class_decorator -class Severity(Enum): - """ - * The key words MUST, MUST NOT, REQUIRED, - * SHALL, SHALL NOT, SHOULD, SHOULD NOT, - * RECOMMENDED, MAY, and OPTIONAL in this document - * are to be interpreted as described in RFC 2119. - """ - INFO = 0 - MAY = 1 - OPTIONAL = 1 - SHOULD = 2 - SHOULD_NOT = 2 - WARNING = 2 - REQUIRED = 3 - MUST = 3 - MUST_NOT = 3 - SHALL = 3 - SHALL_NOT = 3 - RECOMMENDED = 3 - ERROR = 4 +class Severity(RequirementLevels): + """Extends the RequirementLevels enum with additional values""" + INFO = RequirementType('INFO', 0) + WARNING = RequirementType('WARNING', 2) + ERROR = RequirementType('ERROR', 4) class CheckIssue: diff --git a/rocrate_validator/profiles.py b/rocrate_validator/profiles.py new file mode 100644 index 00000000..806e4ed6 --- /dev/null +++ b/rocrate_validator/profiles.py @@ -0,0 +1,51 @@ +from dataclasses import dataclass + + +@dataclass +class RequirementType: + name: str + value: int + + def __eq__(self, other): + return self.name == other.name and self.value == other.severity + + def __ne__(self, other): + return self.name != other.name or self.value != other.severity + + def __lt__(self, other): + return self.value < other.severity + + def __le__(self, other): + return self.value <= other.severity + + def __gt__(self, other): + return self.value > other.severity + + def __ge__(self, other): + return self.value >= other.severity + + def __hash__(self): + return hash((self.name, self.value)) + + def __repr__(self): + return f'RequirementType(name={self.name}, severity={self.value})' + + +class RequirementLevels: + """ + * The key words MUST, MUST NOT, REQUIRED, + * SHALL, SHALL NOT, SHOULD, SHOULD NOT, + * RECOMMENDED, MAY, and OPTIONAL in this document + * are to be interpreted as described in RFC 2119. + """ + MAY = RequirementType('MAY', 1) + OPTIONAL = RequirementType('OPTIONAL', 1) + SHOULD = RequirementType('SHOULD', 2) + SHOULD_NOT = RequirementType('SHOULD_NOT', 2) + REQUIRED = RequirementType('REQUIRED', 3) + MUST = RequirementType('MUST', 3) + MUST_NOT = RequirementType('MUST_NOT', 3) + SHALL = RequirementType('SHALL', 3) + SHALL_NOT = RequirementType('SHALL_NOT', 3) + RECOMMENDED = RequirementType('RECOMMENDED', 3) + From f2d59042beb004d9887c53a6e208e8ea06328d65 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 10 Mar 2024 13:03:43 +0100 Subject: [PATCH 043/902] feat(core): :sparkles: add method to expose all requirements levels --- rocrate_validator/profiles.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rocrate_validator/profiles.py b/rocrate_validator/profiles.py index 806e4ed6..279fe475 100644 --- a/rocrate_validator/profiles.py +++ b/rocrate_validator/profiles.py @@ -1,4 +1,8 @@ from dataclasses import dataclass +import inspect +import os +from pathlib import Path +from typing import Dict, List, Set @dataclass @@ -49,3 +53,9 @@ class RequirementLevels: SHALL_NOT = RequirementType('SHALL_NOT', 3) RECOMMENDED = RequirementType('RECOMMENDED', 3) + def all() -> Dict[str, RequirementType]: + return {name: member for name, member in inspect.getmembers(RequirementLevels) + if not inspect.isroutine(member) + and not inspect.isdatadescriptor(member) and not name.startswith('__')} + + From 994c3988a828416c1412cca0ae3ec0fb1da3cdac Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 10 Mar 2024 14:31:36 +0100 Subject: [PATCH 044/902] fix(core): :bug: remove cyclic dependency --- rocrate_validator/errors.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 8ed6fd75..f0d66d7c 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -1,4 +1,3 @@ -from .models import ValidationResult class InvalidSerializationFormat(Exception): @@ -52,25 +51,3 @@ def check(self): def __repr__(self): return f"CheckValidationError({self._check!r}, {self._message!r}, {self._path!r})" - - -class SHACLValidationError(ValidationError): - - def __init__( - self, - result: ValidationResult = None, - message: str = "Document does not conform to SHACL shapes.", - path: str = ".", - code: int = 500, - ): - super().__init__(message, path, code) - self._result = result - - @property - def result(self) -> ValidationResult: - return self._result - - def __repr__(self): - return ( - f"SHACLValidationError({self._message!r}, {self._path!r}, {self.result!r})" - ) From 6c25b3b4d11ab77534d6222c21fef54fe6a23eb3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 10 Mar 2024 15:08:16 +0100 Subject: [PATCH 045/902] chore(utils): :package: add pylint dev dependency --- poetry.lock | 97 ++++++++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 1 + 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 017d7b1a..45420c69 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,16 @@ # This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. +[[package]] +name = "astroid" +version = "3.1.0" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "astroid-3.1.0-py3-none-any.whl", hash = "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819"}, + {file = "astroid-3.1.0.tar.gz", hash = "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"}, +] + [[package]] name = "click" version = "8.1.7" @@ -89,6 +100,21 @@ files = [ [package.extras] toml = ["tomli"] +[[package]] +name = "dill" +version = "0.3.8" +description = "serialize all of Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, + {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] + [[package]] name = "flake8" version = "6.1.0" @@ -170,6 +196,20 @@ files = [ [package.dependencies] six = "*" +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -241,6 +281,21 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "platformdirs" +version = "4.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] + [[package]] name = "pluggy" version = "1.4.0" @@ -310,6 +365,33 @@ files = [ plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pylint" +version = "3.1.0" +description = "python code static checker" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "pylint-3.1.0-py3-none-any.whl", hash = "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74"}, + {file = "pylint-3.1.0.tar.gz", hash = "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"}, +] + +[package.dependencies] +astroid = ">=3.1.0,<=3.2.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = [ + {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" +platformdirs = ">=2.2.0" +tomlkit = ">=0.10.1" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + [[package]] name = "pyparsing" version = "3.1.2" @@ -355,8 +437,8 @@ importlib-metadata = {version = ">6", markers = "python_version < \"3.12\""} owlrl = ">=6.0.2,<7" packaging = ">=21.3" prettytable = [ - {version = ">=3.5.0", markers = "python_version >= \"3.8\" and python_version < \"3.12\""}, {version = ">=3.7.0", markers = "python_version >= \"3.12\""}, + {version = ">=3.5.0", markers = "python_version >= \"3.8\" and python_version < \"3.12\""}, ] rdflib = {version = ">=6.3.2,<8.0", markers = "python_full_version >= \"3.8.1\""} @@ -466,6 +548,17 @@ files = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +[[package]] +name = "tomlkit" +version = "0.12.4" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, + {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, +] + [[package]] name = "wcwidth" version = "0.2.13" @@ -506,4 +599,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "2ab5150ef75873dd9c5023cc9e4a10b43c918586a8014930434b0a6d18bbdc9a" +content-hash = "fe7ca64347512f6550dfacaac30f0c9faae0eb20427b72f5a60f6a5f3603921b" diff --git a/pyproject.toml b/pyproject.toml index 811cef32..a088b909 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ toml = "^0.10.2" pyproject-flake8 = "^6.1.0" pytest = "^8.1.0" pytest-cov = "^4.1.0" +pylint = "^3.1.0" [tool.flake8] max-line-length = 120 From 46fa752f64034a005cb16f1e9092c850a95fbc54 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 10 Mar 2024 15:11:09 +0100 Subject: [PATCH 046/902] feat(utils): :sparkles: add method to determine the requirement name from the file --- rocrate_validator/utils.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 4110e3c9..0a26d4f3 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -1,5 +1,6 @@ import logging import os +import re from pathlib import Path from typing import List @@ -124,3 +125,24 @@ def get_full_graph( full_graph.parse(graph_path, format="turtle", publicID=publicID) logger.debug("Loaded triples from %s", graph_path) return full_graph + + +def get_requirement_name_from_file(file: Path) -> str: + """ + Get the requirement name from the file + + :param file: The file + :return: The requirement name + """ + return to_camel_case(file.stem).capitalize() + + +def to_camel_case(snake_str: str) -> str: + """ + Convert a snake case string to camel case + + :param snake_str: The snake case string + :return: The camel case string + """ + components = re.split('_|-', snake_str) + return components[0] + ''.join(x.title() for x in components[1:]) From 6d4222716d584e69f8cbc0d60f0a2853f27fde7c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 10 Mar 2024 15:13:59 +0100 Subject: [PATCH 047/902] refactor(shacl): :recycle: move shacl code to a dedicated package --- rocrate_validator/shacl/__init__.py | 5 + rocrate_validator/shacl/checks.py | 12 + rocrate_validator/shacl/errors.py | 24 ++ rocrate_validator/shacl/models.py | 215 ++++++++++++++++++ .../__init__.py => shacl/utils.py} | 0 .../shacl.py => shacl/validator.py} | 9 +- 6 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 rocrate_validator/shacl/__init__.py create mode 100644 rocrate_validator/shacl/checks.py create mode 100644 rocrate_validator/shacl/errors.py create mode 100644 rocrate_validator/shacl/models.py rename rocrate_validator/{validators/__init__.py => shacl/utils.py} (100%) rename rocrate_validator/{validators/shacl.py => shacl/validator.py} (94%) diff --git a/rocrate_validator/shacl/__init__.py b/rocrate_validator/shacl/__init__.py new file mode 100644 index 00000000..943ca488 --- /dev/null +++ b/rocrate_validator/shacl/__init__.py @@ -0,0 +1,5 @@ +from .checks import Check +from .models import ValidationResult +from .validator import Validator + +__all__ = ["Check", "Validator", "ValidationResult"] diff --git a/rocrate_validator/shacl/checks.py b/rocrate_validator/shacl/checks.py new file mode 100644 index 00000000..64c35f2e --- /dev/null +++ b/rocrate_validator/shacl/checks.py @@ -0,0 +1,12 @@ +from ..checks import Check + + +class SHACLCheck(Check): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.name = "SHACL" + self.description = "SHACL validation" + self.issues = [] + + def check(self): + pass diff --git a/rocrate_validator/shacl/errors.py b/rocrate_validator/shacl/errors.py new file mode 100644 index 00000000..6657bbba --- /dev/null +++ b/rocrate_validator/shacl/errors.py @@ -0,0 +1,24 @@ +from ..errors import ValidationError +from .models import ValidationResult + + +class SHACLValidationError(ValidationError): + + def __init__( + self, + result: ValidationResult = None, + message: str = "Document does not conform to SHACL shapes.", + path: str = ".", + code: int = 500, + ): + super().__init__(message, path, code) + self._result = result + + @property + def result(self) -> ValidationResult: + return self._result + + def __repr__(self): + return ( + f"SHACLValidationError({self._message!r}, {self._path!r}, {self.result!r})" + ) diff --git a/rocrate_validator/shacl/models.py b/rocrate_validator/shacl/models.py new file mode 100644 index 00000000..e81e37e8 --- /dev/null +++ b/rocrate_validator/shacl/models.py @@ -0,0 +1,215 @@ +import json +import logging +import os + +from rdflib import Graph, URIRef +from rdflib.term import Node + +from ..constants import SHACL_NS +from ..checks import CheckIssue + +# set up logging +logger = logging.getLogger(__name__) + + +class Shape: + + def __init__(self, shape_node: Node, graph: Graph) -> None: + # check the input + assert isinstance(shape_node, Node), "Invalid shape node" + assert isinstance(graph, Graph), "Invalid graph" + + # store the input + self._shape_node = shape_node + self._graph = graph + + # create a graph for the shape + shape_graph = Graph() + shape_graph += graph.triples((shape_node, None, None)) + self.shape_graph = shape_graph + + # serialize the graph in json-ld + shape_json = shape_graph.serialize(format="json-ld") + shape_obj = json.loads(shape_json) + logger.debug("Shape JSON: %s" % shape_obj) + try: + self.shape_json = shape_obj[0] + except Exception as e: + logger.error("Error parsing shape JSON: %s" % e) + # if logger.isEnabledFor(logging.DEBUG): + # logger.exception(e) + # raise e + pass + + @property + def node(self) -> Node: + return self._shape_node + + @property + def graph(self) -> Graph: + return self._graph + + @property + def name(self): + return self.shape_json[f'{SHACL_NS}name'][0]['@value'] + + @property + def description(self): + return self.shape_json[f'{SHACL_NS}description'][0]['@value'] + + @property + def path(self): + return self.shape_json[f'{SHACL_NS}path'][0]['@id'] + + @property + def nodeKind(self): + nodeKind = self.shape_json.get(f'{SHACL_NS}nodeKind', None) + if nodeKind: + return nodeKind[0]['@id'] + return None + + +class Violation(CheckIssue): + + def __init__(self, violation_node: Node, graph: Graph) -> None: + # check the input + assert isinstance(violation_node, Node), "Invalid violation node" + assert isinstance(graph, Graph), "Invalid graph" + + # store the input + self._violation_node = violation_node + self._graph = graph + + # create a graph for the violation + violation_graph = Graph() + violation_graph += graph.triples((violation_node, None, None)) + self.violation_graph = violation_graph + + # serialize the graph in json-ld + violation_json = violation_graph.serialize(format="json-ld") + violation_obj = json.loads(violation_json) + self.violation_json = violation_obj[0] + + # get the source shape + shapes = list(graph.triples( + (violation_node, URIRef(f"{SHACL_NS}sourceShape"), None))) + self.source_shape_node = shapes[0][2] + # initialize the parent class + super().__init__(severity=self.resultSeverity, + message=self.resultMessage) + + @property + def node(self) -> Node: + return self._violation_node + + @property + def graph(self) -> Graph: + return self._graph + + @property + def resultSeverity(self): + return self.violation_json[f'{SHACL_NS}resultSeverity'][0]['@id'] + + @property + def focusNode(self): + return self.violation_json[f'{SHACL_NS}focusNode'][0]['@id'] + + @property + def resultPath(self): + return self.violation_json[f'{SHACL_NS}resultPath'][0]['@id'] + + @property + def value(self): + value = self.violation_json.get(f'{SHACL_NS}value', None) + if not value: + return None + return value[0]['@id'] + + @property + def resultMessage(self): + return self.violation_json[f'{SHACL_NS}resultMessage'][0]['@value'] + + @property + def sourceConstraintComponent(self): + return self.violation_json[f'{SHACL_NS}sourceConstraintComponent'][0]['@id'] + + @property + def sourceShape(self) -> Shape: + try: + return Shape(self.source_shape_node, self._graph) + except Exception as e: + logger.error("Error getting source shape: %s" % e) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return None + + +class ValidationResult: + + def __init__(self, results_graph: Graph, conforms: bool = None, results_text: str = None) -> None: + # validate the results graph input + assert results_graph is not None, "Invalid graph" + assert isinstance(results_graph, Graph), "Invalid graph type" + # check if the graph is valid ValidationReport + assert (None, URIRef(f"{SHACL_NS}conforms"), + None) in results_graph, "Invalid ValidationReport" + # store the input properties + self._conforms = conforms + self.results_graph = results_graph + self._text = results_text + # parse the results graph + self._violations = self.parse_results_graph(results_graph) + # initialize the conforms property + if conforms is not None: + self._conforms = len(self._violations) == 0 + else: + assert self._conforms == len( + self._violations) == 0, "Invalid validation result" + + @staticmethod + def parse_results_graph(results_graph: Graph): + # Query for validation results + query = """ + SELECT ?subject + WHERE {{ + ?subject a <{0}ValidationResult> . + }} + """.format(SHACL_NS) + + query_results = results_graph.query(query) + + violations = [] + for r in query_results: + violation_node = r[0] + violation = Violation(violation_node, results_graph) + violations.append(violation) + + return violations + + @property + def conforms(self) -> bool: + return self._conforms + + @property + def violations(self) -> list: + return self._violations + + @property + def text(self) -> str: + return self._text + + @staticmethod + def from_serialized_results_graph(file_path: str, format: str = 'turtle'): + # check the input + assert format in ['turtle', 'n3', 'nt', + 'xml', 'rdf', 'json-ld'], "Invalid format" + assert file_path, "Invalid file path" + assert os.path.exists(file_path), "File does not exist" + # Load the graph + logger.debug("Loading graph from file: %s" % file_path) + g = Graph() + g.parse(file_path, format=format) + logger.debug("Graph loaded from file: %s" % file_path) + + # return the validation result + return ValidationResult(g) diff --git a/rocrate_validator/validators/__init__.py b/rocrate_validator/shacl/utils.py similarity index 100% rename from rocrate_validator/validators/__init__.py rename to rocrate_validator/shacl/utils.py diff --git a/rocrate_validator/validators/shacl.py b/rocrate_validator/shacl/validator.py similarity index 94% rename from rocrate_validator/validators/shacl.py rename to rocrate_validator/shacl/validator.py index b9db7788..2bc33723 100644 --- a/rocrate_validator/validators/shacl.py +++ b/rocrate_validator/shacl/validator.py @@ -1,12 +1,15 @@ import logging -from typing import Literal, Optional, Union +from typing import Optional, Union import pyshacl from pyshacl.pytypes import GraphLike from rdflib import Graph -from ..constants import RDF_SERIALIZATION_FORMATS, RDF_SERIALIZATION_FORMATS_TYPES, VALID_INFERENCE_OPTIONS, VALID_INFERENCE_OPTIONS_TYPES -from ..models import ValidationResult +from ..constants import (RDF_SERIALIZATION_FORMATS, + RDF_SERIALIZATION_FORMATS_TYPES, + VALID_INFERENCE_OPTIONS, + VALID_INFERENCE_OPTIONS_TYPES) +from .models import ValidationResult # set up logging logger = logging.getLogger(__name__) From 86408d2cc4f8d106816d4fc113b555b110482ce3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 10 Mar 2024 15:33:22 +0100 Subject: [PATCH 048/902] refactor(shacl): :recycle: move shacl code within checks package --- rocrate_validator/{ => checks}/shacl/__init__.py | 0 rocrate_validator/{ => checks}/shacl/checks.py | 4 ++-- rocrate_validator/{ => checks}/shacl/errors.py | 2 +- rocrate_validator/{ => checks}/shacl/models.py | 4 ++-- rocrate_validator/{ => checks}/shacl/utils.py | 0 rocrate_validator/{ => checks}/shacl/validator.py | 8 ++++---- rocrate_validator/cli.py | 3 ++- rocrate_validator/service.py | 8 +++++--- 8 files changed, 16 insertions(+), 13 deletions(-) rename rocrate_validator/{ => checks}/shacl/__init__.py (100%) rename rocrate_validator/{ => checks}/shacl/checks.py (75%) rename rocrate_validator/{ => checks}/shacl/errors.py (93%) rename rocrate_validator/{ => checks}/shacl/models.py (98%) rename rocrate_validator/{ => checks}/shacl/utils.py (100%) rename rocrate_validator/{ => checks}/shacl/validator.py (95%) diff --git a/rocrate_validator/shacl/__init__.py b/rocrate_validator/checks/shacl/__init__.py similarity index 100% rename from rocrate_validator/shacl/__init__.py rename to rocrate_validator/checks/shacl/__init__.py diff --git a/rocrate_validator/shacl/checks.py b/rocrate_validator/checks/shacl/checks.py similarity index 75% rename from rocrate_validator/shacl/checks.py rename to rocrate_validator/checks/shacl/checks.py index 64c35f2e..d7a1a53b 100644 --- a/rocrate_validator/shacl/checks.py +++ b/rocrate_validator/checks/shacl/checks.py @@ -1,7 +1,7 @@ -from ..checks import Check +from ...checks import Check as BaseCheck -class SHACLCheck(Check): +class Check(BaseCheck): def __init__(self, **kwargs): super().__init__(**kwargs) self.name = "SHACL" diff --git a/rocrate_validator/shacl/errors.py b/rocrate_validator/checks/shacl/errors.py similarity index 93% rename from rocrate_validator/shacl/errors.py rename to rocrate_validator/checks/shacl/errors.py index 6657bbba..7af6457d 100644 --- a/rocrate_validator/shacl/errors.py +++ b/rocrate_validator/checks/shacl/errors.py @@ -1,4 +1,4 @@ -from ..errors import ValidationError +from ...errors import ValidationError from .models import ValidationResult diff --git a/rocrate_validator/shacl/models.py b/rocrate_validator/checks/shacl/models.py similarity index 98% rename from rocrate_validator/shacl/models.py rename to rocrate_validator/checks/shacl/models.py index e81e37e8..b2ba143d 100644 --- a/rocrate_validator/shacl/models.py +++ b/rocrate_validator/checks/shacl/models.py @@ -5,8 +5,8 @@ from rdflib import Graph, URIRef from rdflib.term import Node -from ..constants import SHACL_NS -from ..checks import CheckIssue +from ...constants import SHACL_NS +from ...checks import CheckIssue # set up logging logger = logging.getLogger(__name__) diff --git a/rocrate_validator/shacl/utils.py b/rocrate_validator/checks/shacl/utils.py similarity index 100% rename from rocrate_validator/shacl/utils.py rename to rocrate_validator/checks/shacl/utils.py diff --git a/rocrate_validator/shacl/validator.py b/rocrate_validator/checks/shacl/validator.py similarity index 95% rename from rocrate_validator/shacl/validator.py rename to rocrate_validator/checks/shacl/validator.py index 2bc33723..50bb38e0 100644 --- a/rocrate_validator/shacl/validator.py +++ b/rocrate_validator/checks/shacl/validator.py @@ -5,10 +5,10 @@ from pyshacl.pytypes import GraphLike from rdflib import Graph -from ..constants import (RDF_SERIALIZATION_FORMATS, - RDF_SERIALIZATION_FORMATS_TYPES, - VALID_INFERENCE_OPTIONS, - VALID_INFERENCE_OPTIONS_TYPES) +from ...constants import (RDF_SERIALIZATION_FORMATS, + RDF_SERIALIZATION_FORMATS_TYPES, + VALID_INFERENCE_OPTIONS, + VALID_INFERENCE_OPTIONS_TYPES) from .models import ValidationResult # set up logging diff --git a/rocrate_validator/cli.py b/rocrate_validator/cli.py index d1708553..f04edfa8 100644 --- a/rocrate_validator/cli.py +++ b/rocrate_validator/cli.py @@ -4,10 +4,11 @@ import click from rich.console import Console -from rocrate_validator.errors import CheckValidationError, SHACLValidationError +from rocrate_validator.errors import CheckValidationError from rocrate_validator.service import validate as validate_rocrate from .checks import Severity +from .checks.shacl.errors import SHACLValidationError from .colors import get_severity_color from .models import ValidationResult diff --git a/rocrate_validator/service.py b/rocrate_validator/service.py index d49cdd4e..a053c09b 100644 --- a/rocrate_validator/service.py +++ b/rocrate_validator/service.py @@ -4,11 +4,11 @@ from pyshacl.pytypes import GraphLike from rdflib import Graph +from .checks.shacl import Validator +from .checks.shacl.errors import SHACLValidationError from .constants import ROCRATE_METADATA_FILE -from .errors import CheckValidationError, SHACLValidationError -from .utils import get_full_graph -from .validators.shacl import Validator from .models import ValidationResult +from .utils import get_full_graph # set up logging logger = logging.getLogger(__name__) @@ -118,5 +118,7 @@ def validate( if not result.conforms: logger.error("Validation failed") # TODO: wrap the validation error as issue + if not result.conforms: + raise SHACLValidationError(result, rocrate_path) return validation_result From e8b6d793f7c867262fdab496c0ed6c6623577e99 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 09:44:28 +0100 Subject: [PATCH 049/902] feat(shacl): :sparkles: refactor shacl check validator --- rocrate_validator/checks/shacl/__init__.py | 5 -- rocrate_validator/checks/shacl/checks.py | 12 --- rocrate_validator/profiles.py | 61 ------------- .../utils.py => requirements/__init__.py} | 0 .../requirements/shacl/__init__.py | 6 ++ .../requirements/shacl/checks.py | 87 +++++++++++++++++++ .../{checks => requirements}/shacl/errors.py | 0 .../{checks => requirements}/shacl/models.py | 18 +++- rocrate_validator/requirements/shacl/utils.py | 0 .../shacl/validator.py | 0 10 files changed, 109 insertions(+), 80 deletions(-) delete mode 100644 rocrate_validator/checks/shacl/__init__.py delete mode 100644 rocrate_validator/checks/shacl/checks.py delete mode 100644 rocrate_validator/profiles.py rename rocrate_validator/{checks/shacl/utils.py => requirements/__init__.py} (100%) create mode 100644 rocrate_validator/requirements/shacl/__init__.py create mode 100644 rocrate_validator/requirements/shacl/checks.py rename rocrate_validator/{checks => requirements}/shacl/errors.py (100%) rename rocrate_validator/{checks => requirements}/shacl/models.py (94%) create mode 100644 rocrate_validator/requirements/shacl/utils.py rename rocrate_validator/{checks => requirements}/shacl/validator.py (100%) diff --git a/rocrate_validator/checks/shacl/__init__.py b/rocrate_validator/checks/shacl/__init__.py deleted file mode 100644 index 943ca488..00000000 --- a/rocrate_validator/checks/shacl/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .checks import Check -from .models import ValidationResult -from .validator import Validator - -__all__ = ["Check", "Validator", "ValidationResult"] diff --git a/rocrate_validator/checks/shacl/checks.py b/rocrate_validator/checks/shacl/checks.py deleted file mode 100644 index d7a1a53b..00000000 --- a/rocrate_validator/checks/shacl/checks.py +++ /dev/null @@ -1,12 +0,0 @@ -from ...checks import Check as BaseCheck - - -class Check(BaseCheck): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.name = "SHACL" - self.description = "SHACL validation" - self.issues = [] - - def check(self): - pass diff --git a/rocrate_validator/profiles.py b/rocrate_validator/profiles.py deleted file mode 100644 index 279fe475..00000000 --- a/rocrate_validator/profiles.py +++ /dev/null @@ -1,61 +0,0 @@ -from dataclasses import dataclass -import inspect -import os -from pathlib import Path -from typing import Dict, List, Set - - -@dataclass -class RequirementType: - name: str - value: int - - def __eq__(self, other): - return self.name == other.name and self.value == other.severity - - def __ne__(self, other): - return self.name != other.name or self.value != other.severity - - def __lt__(self, other): - return self.value < other.severity - - def __le__(self, other): - return self.value <= other.severity - - def __gt__(self, other): - return self.value > other.severity - - def __ge__(self, other): - return self.value >= other.severity - - def __hash__(self): - return hash((self.name, self.value)) - - def __repr__(self): - return f'RequirementType(name={self.name}, severity={self.value})' - - -class RequirementLevels: - """ - * The key words MUST, MUST NOT, REQUIRED, - * SHALL, SHALL NOT, SHOULD, SHOULD NOT, - * RECOMMENDED, MAY, and OPTIONAL in this document - * are to be interpreted as described in RFC 2119. - """ - MAY = RequirementType('MAY', 1) - OPTIONAL = RequirementType('OPTIONAL', 1) - SHOULD = RequirementType('SHOULD', 2) - SHOULD_NOT = RequirementType('SHOULD_NOT', 2) - REQUIRED = RequirementType('REQUIRED', 3) - MUST = RequirementType('MUST', 3) - MUST_NOT = RequirementType('MUST_NOT', 3) - SHALL = RequirementType('SHALL', 3) - SHALL_NOT = RequirementType('SHALL_NOT', 3) - RECOMMENDED = RequirementType('RECOMMENDED', 3) - - def all() -> Dict[str, RequirementType]: - return {name: member for name, member in inspect.getmembers(RequirementLevels) - if not inspect.isroutine(member) - and not inspect.isdatadescriptor(member) and not name.startswith('__')} - - diff --git a/rocrate_validator/checks/shacl/utils.py b/rocrate_validator/requirements/__init__.py similarity index 100% rename from rocrate_validator/checks/shacl/utils.py rename to rocrate_validator/requirements/__init__.py diff --git a/rocrate_validator/requirements/shacl/__init__.py b/rocrate_validator/requirements/shacl/__init__.py new file mode 100644 index 00000000..68ed4f5d --- /dev/null +++ b/rocrate_validator/requirements/shacl/__init__.py @@ -0,0 +1,6 @@ +from .checks import SHACLCheck +from .models import ValidationResult +from .validator import Validator +from .errors import SHACLValidationError + +__all__ = ["SHACLCheck", "Validator", "ValidationResult", "SHACLValidationError"] diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py new file mode 100644 index 00000000..44ac5031 --- /dev/null +++ b/rocrate_validator/requirements/shacl/checks.py @@ -0,0 +1,87 @@ + +import logging +from typing import Optional + +from ...constants import SHACL_NS +from ...models import Check as BaseCheck +from ...models import Requirement, Validator +from .validator import Validator as SHACLValidator + +logger = logging.getLogger(__name__) + + +class SHACLCheck(BaseCheck): + def __init__(self, + requirement: Requirement, + validator: Validator, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> None: + super().__init__(requirement, validator, name, description) + + @property + def name(self): + if not self._name and self.shapes_graph is None: + return "SHACL Check" + + if not self._name: + query = """ + SELECT ?name + WHERE { + ?shape a sh:NodeShape ; + sh:name ?name . + } + """ + # Execute the query + results = [_ for _ in self.shapes_graph.query(query, initNs={"sh": SHACL_NS})] + if results: + self._name = results[0][0] + + return self._name + + @property + def description(self): + if not self._description and self.shapes_graph is None: + return "SHACL Check" + + if not self._description: + # Query to get the description of the shape + query = """ + SELECT ?description + WHERE { + ?shape a sh:NodeShape ; + sh:description ?description . + } + """ + # Execute the query + results = [_ for _ in self.shapes_graph.query(query, initNs={"sh": SHACL_NS})] + if results: + self._description = results[0][0] + + return self._description + + @property + def shapes_graph(self): + return self.validator.get_graph_of_shapes(self.requirement.name) + + def check(self): + shapes_graph = self.shapes_graph + ontology_graph = self.validator.ontologies_graph + data_graph = self.validator.data_graph + + shacl_validator = SHACLValidator( + shapes_graph=shapes_graph, ont_graph=ontology_graph) + result = shacl_validator.validate( + data_graph=data_graph, + **self.validator.validation_settings + ) + logger.debug("Validation conforms: %s", result.conforms) + if not result.conforms: + logger.debug("Validation failed") + logger.debug("Validation result: %s", result) + for issue in result.violations: + logger.debug("Validation issue: %s", issue.message) + self.result.add_issue(issue) + + return False + return True diff --git a/rocrate_validator/checks/shacl/errors.py b/rocrate_validator/requirements/shacl/errors.py similarity index 100% rename from rocrate_validator/checks/shacl/errors.py rename to rocrate_validator/requirements/shacl/errors.py diff --git a/rocrate_validator/checks/shacl/models.py b/rocrate_validator/requirements/shacl/models.py similarity index 94% rename from rocrate_validator/checks/shacl/models.py rename to rocrate_validator/requirements/shacl/models.py index b2ba143d..26cf9d78 100644 --- a/rocrate_validator/checks/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -1,12 +1,13 @@ import json import logging import os +from typing import List from rdflib import Graph, URIRef from rdflib.term import Node from ...constants import SHACL_NS -from ...checks import CheckIssue +from ...models import CheckIssue, Severity # set up logging logger = logging.getLogger(__name__) @@ -143,6 +144,19 @@ def sourceShape(self) -> Shape: logger.exception(e) return None + @property + def message(self): + return self.resultMessage + + @property + def description(self): + return self.sourceShape.description + + @property + def severity(self): + # TODO: map the severity to the CheckIssue severity + return Severity.ERROR + class ValidationResult: @@ -191,7 +205,7 @@ def conforms(self) -> bool: return self._conforms @property - def violations(self) -> list: + def violations(self) -> List: return self._violations @property diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py new file mode 100644 index 00000000..e69de29b diff --git a/rocrate_validator/checks/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py similarity index 100% rename from rocrate_validator/checks/shacl/validator.py rename to rocrate_validator/requirements/shacl/validator.py From 2824baedf87d99d08e1d02f90729f400cbbbbf01 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 09:47:05 +0100 Subject: [PATCH 050/902] feat(core): :sparkles: add main validator class --- rocrate_validator/models.py | 680 +++++++++++++++++++++++++++++++++++- 1 file changed, 678 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 28953533..5814dd9c 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1,7 +1,503 @@ +from __future__ import annotations + +import inspect +import logging +import os +from abc import ABC, abstractmethod +from dataclasses import dataclass from pathlib import Path -from typing import Dict, List +from typing import Dict, List, Optional, Set, Type, Union + +from rdflib import Graph + +from rocrate_validator.constants import (RDF_SERIALIZATION_FORMATS_TYPES, ROCRATE_METADATA_FILE, + VALID_INFERENCE_OPTIONS_TYPES) +from rocrate_validator.utils import (get_classes_from_file, + get_file_descriptor_path, + get_requirement_name_from_file) + +logger = logging.getLogger(__name__) + + +@dataclass +class RequirementType: + name: str + value: int + + def __eq__(self, other): + return self.name == other.name and self.value == other.value + + def __ne__(self, other): + return self.name != other.name or self.value != other.value + + def __lt__(self, other): + return self.value < other.value + + def __le__(self, other): + return self.value <= other.value + + def __gt__(self, other): + return self.value > other.value + + def __ge__(self, other): + return self.value >= other.value + + def __hash__(self): + return hash((self.name, self.value)) + + def __repr__(self): + return f'RequirementType(name={self.name}, severity={self.value})' + + +class RequirementLevels: + """ + * The key words MUST, MUST NOT, REQUIRED, + * SHALL, SHALL NOT, SHOULD, SHOULD NOT, + * RECOMMENDED, MAY, and OPTIONAL in this document + * are to be interpreted as described in RFC 2119. + """ + MAY = RequirementType('MAY', 1) + OPTIONAL = RequirementType('OPTIONAL', 1) + SHOULD = RequirementType('SHOULD', 2) + SHOULD_NOT = RequirementType('SHOULD_NOT', 2) + REQUIRED = RequirementType('REQUIRED', 3) + MUST = RequirementType('MUST', 3) + MUST_NOT = RequirementType('MUST_NOT', 3) + SHALL = RequirementType('SHALL', 3) + SHALL_NOT = RequirementType('SHALL_NOT', 3) + RECOMMENDED = RequirementType('RECOMMENDED', 3) + + def all() -> Dict[str, RequirementType]: + return {name: member for name, member in inspect.getmembers(RequirementLevels) + if not inspect.isroutine(member) + and not inspect.isdatadescriptor(member) and not name.startswith('__')} + + @staticmethod + def get(name: str) -> RequirementType: + return RequirementLevels.all()[name.upper()] + + +class Profile: + def __init__(self, name: str, path: Path = None, + requirements: Set[Requirement] = None): + self._path = path + self._name = name + self._requirements = requirements if requirements else [] + + @property + def path(self): + return self._path + + @property + def name(self): + return self._name + + @property + def requirements(self) -> List[Requirement]: + return self._requirements + + def get_requirement(self, name: str) -> Requirement: + for requirement in self.requirements: + if requirement.name == name: + return requirement + return None + + def has_requirement(self, name: str) -> bool: + return self.get_requirement(name) is not None + + def get_requirements_by_type(self, type: RequirementType) -> List[Requirement]: + return [requirement for requirement in self.requirements if requirement.type == type] + + def add_requirement(self, requirement: Requirement): + self.requirements.append(requirement) + + def remove_requirement(self, requirement: Requirement): + self.requirements.remove(requirement) + + def validate(self, rocrate_path: Path) -> ValidationResult: + pass + + def __eq__(self, other) -> bool: + return self.name == other.name and self.path == other.path and self.requirements == other.requirements + + def __ne__(self, other) -> bool: + return self.name != other.name or self.path != other.path or self.requirements != other.requirements + + def __hash__(self) -> int: + return hash((self.name, self.path, self.requirements)) + + def __repr__(self) -> str: + return ( + f'Profile(name={self.name}, ' + f'path={self.path}, ' if self.path else '' + f'requirements={self.requirements})' + ) + + def __str__(self) -> str: + return self.name + + @staticmethod + def load(path: Union[str, Path]): + # if the path is a string, convert it to a Path + if isinstance(path, str): + path = Path(path) + # check if the path is a directory + assert path.is_dir(), f"Invalid profile path: {path}" + # create a new profile + profile = Profile(name=path.name, path=path) + levels = [_.upper() for _ in RequirementLevels.all().keys()] + for root, dirs, files in os.walk(path): + logger.debug("Root: %s", root) + logger.debug("Dirs: %s", dirs) + logger.debug("Files: %s", files) + dirs[:] = [d for d in dirs + if not d.startswith('.') and not d.startswith('_') + and d.upper() in levels] + requirement_root = Path(root) + requirement_level = requirement_root.name + logger.debug("Sorted files: %s", sorted(files, key=lambda x: (not x.endswith('.py'), x))) + for file in sorted(files, key=lambda x: (not x.endswith('.py'), x)): + requirement_path = requirement_root / file + logger.debug("File: %s (root: %s)", requirement_path, requirement_root.name) + for requirement in Requirement.load(profile, RequirementLevels.get(requirement_level), requirement_path): + profile.add_requirement(requirement) + logger.debug("Added requirement: %s", requirement) + + return profile + + +class Requirement: + + def __init__(self, + check_class: Type[Check], + type: RequirementType, + profile: Profile, + name: str = None, + description: str = None, + path: Path = None): + self._name = name + self._type = type + self._profile = profile + self._description = description + self._path = path + self._check_class = check_class + + if not self._name and self._path: + self._name = get_requirement_name_from_file(self._path) + + @property + def name(self) -> str: + if not self._name and self._path: + return get_requirement_name_from_file(self._path) + return self._name + + @property + def type(self) -> RequirementType: + return self._type + + @property + def profile(self) -> Profile: + return self._profile + + @property + def description(self) -> str: + return self._description + + @property + def path(self) -> Path: + return self._path + + @property + def check_class(self) -> Type[Check]: + return self._check_class + + # def validate(self, rocrate_path: Path) -> CheckResult: + # assert self.check_class, "Check class not associated with requirement" + # # instantiate the check class + # check = self.check_class(self, rocrate_path) + # # run the check + # check.__do_check__() + # # return the result + # return check.result + + def __eq__(self, other): + return self.name == other.name \ + and self.type == other.type and self.description == other.description \ + and self.path == other.path + + def __ne__(self, other): + return self.name != other.name \ + or self.type != other.type \ + or self.description != other.description \ + or self.path != other.path + + def __hash__(self): + return hash((self.name, self.type, self.description, self.path)) + + def __repr__(self): + return ( + f'ProfileRequirement(' + f'name={self.name}, ' + f'type={self.type}, ' + f'description={self.description}' + f', path={self.path}, ' if self.path else '' + ')' + ) + + def __str__(self): + return self.name + + @staticmethod + def load(profile: Profile, requirement_type: RequirementType, file_path: Path) -> List[Requirement]: + # initialize the set of requirements + requirements = [] + + # check if the file is a python file + if file_path.suffix == ".py": + classes = get_classes_from_file(file_path, filter_class=Check) + logger.debug("Classes: %r" % classes) + + # instantiate a requirement for each class + for check_name, check_class in classes.items(): + r = Requirement( + check_class, requirement_type, profile, path=file_path, + name=get_requirement_name_from_file(file_path, check_name=check_name) + ) + logger.debug("Added Requirement: %r" % r) + requirements.append(r) + elif file_path.suffix == ".ttl": + from rocrate_validator.requirements.shacl.checks import SHACLCheck + r = Requirement(SHACLCheck, requirement_type, + profile, path=file_path) + requirements.append(r) + logger.debug("Added Requirement: %r" % r) + else: + logger.warning("Requirement type not supported: %s", file_path.suffix) + + return requirements + + +def issue_types(issues: List[Type[CheckIssue]]) -> Type[Check]: + def class_decorator(cls): + cls.issue_types = issues + return cls + return class_decorator + + +class Severity(RequirementLevels): + """Extends the RequirementLevels enum with additional values""" + INFO = RequirementType('INFO', 0) + WARNING = RequirementType('WARNING', 2) + ERROR = RequirementType('ERROR', 4) + + +class CheckResult: + """ + Class to store the result of a check + + Attributes: + check (Check): The check that was performed + code (int): The result code + message (str): The message + """ + + def __init__(self, check: Check): + self.check = check + self._issues: List[CheckIssue] = [] + + @property + def issues(self) -> List[CheckIssue]: + return self._issues + + def add_issue(self, issue: CheckIssue): + issue._check = self.check + self._issues.append(issue) -from .checks import CheckIssue, Severity + def add_error(self, message: str, code: int = None): + self._issues.append(CheckIssue(Severity.ERROR, message, code, self.check)) + + def add_warning(self, message: str, code: int = None): + self._issues.append(CheckIssue(Severity.WARNING, message, code, self.check)) + + def add_info(self, message: str, code: int = None): + self._issues.append(CheckIssue(Severity.INFO, message, code, self.check)) + + def add_optional(self, message: str, code: int = None): + self._issues.append(CheckIssue(Severity.OPTIONAL, message, code, self.check)) + + def add_may(self, message: str, code: int = None): + self._issues.append(CheckIssue(Severity.MAY, message, code, self.check)) + + def add_should(self, message: str, code: int = None): + self._issues.append(CheckIssue(Severity.SHOULD, message, code, self.check)) + + def add_should_not(self, message: str, code: int = None): + self._issues.append(CheckIssue(Severity.SHOULD_NOT, message, code, self.check)) + + def add_must(self, message: str, code: int = None): + self._issues.append(CheckIssue(Severity.MUST, message, code, self.check)) + + def add_must_not(self, message: str, code: int = None): + self._issues.append(CheckIssue(Severity.MUST_NOT, message, code, self.check)) + + def get_issues(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: + return [issue for issue in self.issues if issue.severity.value >= severity.value] + + def get_issues_by_severity(self, severity: Severity) -> List[CheckIssue]: + return [issue for issue in self.issues if issue.severity == severity] + + def passed(self, severity: Severity = Severity.WARNING) -> bool: + return not any(issue.severity.value >= severity.value for issue in self.issues) + + +class Check(ABC): + """ + Base class for checks + """ + + def __init__(self, + requirement: Requirement, + validator: Validator, + name: Optional[str] = None, + description: Optional[str] = None) -> None: + self._requirement = requirement + self._validator = validator + self._name = name + self._description = description + # create a result object for the check + self._result: CheckResult = CheckResult(self) + + @property + def requirement(self) -> Requirement: + return self._requirement + + @property + def name(self) -> str: + if not self._name: + return self.__class__.__name__.replace("Check", "") + return self._name + + @property + def description(self) -> str: + if not self._description: + return self.__doc__.strip() + return self._description + + @property + def ro_crate_path(self) -> Path: + return self._validator.rocrate_path + + @property + def file_descriptor_path(self) -> Path: + return get_file_descriptor_path(self.ro_crate_path) + + @property + def result(self) -> CheckResult: + return self._result + + @property + def validator(self) -> Validator: + return self._validator + + def __do_check__(self) -> bool: + """ + Internal method to perform the check + """ + # Check if the check has issue types defined + # TODO: check if this is necessary + # assert self.issue_types, "Check must have issue types defined in the decorator" + # Perform the check + try: + return self.check() + except Exception as e: + self.result.add_error(str(e)) + logger.error("Unexpected error during check: %s", e) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return False + + @abstractmethod + def check(self) -> bool: + raise NotImplementedError("Check not implemented") + + def passed(self, severity: Severity = Severity.WARNING) -> bool: + return self.result.passed(severity) + + def get_issues(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: + return self.result.get_issues(severity) + + def get_issues_by_severity(self, severity: Severity) -> List[CheckIssue]: + return self.result.get_issues_by_severity(severity) + + def __str__(self) -> str: + return self.name + + def __repr__(self) -> str: + return f"{self.name}Check()" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Check): + return False + return self.name == other.name + + def __hash__(self) -> int: + return hash(self.name) + + +class CheckIssue: + """ + Class to store an issue found during a check + + Attributes: + severity (IssueSeverity): The severity of the issue + message (str): The message + code (int): The code + """ + + def __init__(self, severity: Severity, + message: Optional[str] = None, + code: int = None, + check: Check = None): + self._severity = severity + self._message = message + self._code = code + self._check = check + + @property + def message(self) -> str: + """The message associated with the issue""" + return self._message + + @property + def severity(self) -> str: + """The severity of the issue""" + return self._severity + + @property + def check(self) -> Check: + """The check that generated the issue""" + return self._check + + @property + def code(self) -> int: + # If the code has not been set, calculate it + if not self._code: + """ + Calculate the code based on the severity, the class name and the message. + - All issues with the same severity, class name and message will have the same code. + - All issues with the same severity and class name but different message will have different codes. + - All issues with the same severity but different class name and message will have different codes. + - All issues with the same severity should start with the same number. + - All codes should be positive numbers. + """ + # Concatenate the severity, class name and message into a single string + issue_string = str(self.severity.value) + self.__class__.__name__ + str(self.message) + + # Use the built-in hash function to generate a unique code for this string + # The modulo operation ensures that the code is a positive number + self._code = hash(issue_string) % ((1 << 31) - 1) + # Return the code + return self._code class ValidationResult: @@ -40,3 +536,183 @@ def __str__(self): def __repr__(self): return f"ValidationResult(issues={self._issues})" + + +class Validator: + + def __init__(self, + rocrate_path: Path, + profiles_path: str = "./profiles", + profile_name: str = "ro-crate", + disable_profile_inheritance: bool = False, + requirement_level="MUST", + requirement_level_only: bool = False, + ontologies_path: Optional[Path] = None, + advanced: Optional[bool] = False, + inference: Optional[VALID_INFERENCE_OPTIONS_TYPES] = None, + inplace: Optional[bool] = False, + abort_on_first: Optional[bool] = False, + allow_infos: Optional[bool] = False, + allow_warnings: Optional[bool] = False, + serialization_output_path: str = None, + serialization_output_format: Optional[RDF_SERIALIZATION_FORMATS_TYPES] = "turtle", + **kwargs): + self.rocrate_path = rocrate_path + self.profiles_path = profiles_path + self.profile_name = profile_name + self.disable_profile_inheritance = disable_profile_inheritance + self.requirement_level = requirement_level + self.requirement_level_only = requirement_level_only + self.ontologies_path = ontologies_path + + self._validation_settings = { + 'advanced': advanced, + 'inference': inference, + 'inplace': inplace, + 'abort_on_first': abort_on_first, + 'allow_infos': allow_infos, + 'allow_warnings': allow_warnings, + 'serialization_output_path': serialization_output_path, + 'serialization_output_format': serialization_output_format, + 'publicID': rocrate_path, + **kwargs, + } + # self.advanced = advanced + # self.inference = inference + # self.inplace = inplace + # self.abort_on_first = abort_on_first + # self.allow_infos = allow_infos + # self.allow_warnings = allow_warnings + # self.serialization_output_path = serialization_output_path + # self.serialization_output_format = serialization_output_format + # self.kwargs = kwargs + + # reference to the data graph + self._data_graph = None + # reference to the profile + self._profile = None + # reference to the graph of shapes + self._shapes_graphs = {} + # reference to the graph of ontologies + self._ontologies_graph = None + + @property + def validation_settings(self) -> Dict[str, Union[str, Path, bool, int]]: + return self._validation_settings + + @property + def rocrate_metadata_path(self): + return f"{self.rocrate_path}/{ROCRATE_METADATA_FILE}" + + @property + def profile_path(self): + return f"{self.profiles_path}/{self.profile_name}" + + def load_data_graph(self): + data_graph = Graph() + data_graph.parse(self.rocrate_metadata_path, + format="json-ld", publicID=self.rocrate_path) + return data_graph + + def get_data_graph(self, refresh: bool = False): + # load the data graph + if not self._data_graph or refresh: + self._data_graph = self.load_data_graph() + return self._data_graph + + @property + def data_graph(self) -> Graph: + return self.get_data_graph() + + def load_profile(self): + # load profile + profile = Profile.load(self.profile_path) + logger.debug("Profile: %s", profile) + return profile + + def get_profile(self, refresh: bool = False): + # load the profile + if not self._profile or refresh: + self._profile = self.load_profile() + return self._profile + + @property + def profile(self) -> Profile: + return self.get_profile() + + def load_graphs_of_shapes(self): + # load the graph of shapes + shapes_graphs = {} + for requirement in self._profile.requirements: + if requirement.path.suffix == ".ttl": + shapes_graph = Graph() + shapes_graph.parse(str(requirement.path), format="ttl") + shapes_graphs[requirement.name] = shapes_graph + return shapes_graphs + + def get_graphs_of_shapes(self, refresh: bool = False): + # load the graph of shapes + if not self._shapes_graphs or refresh: + self._shapes_graphs = self.load_graphs_of_shapes() + return self._shapes_graphs + + @property + def shapes_graphs(self) -> Dict[str, Graph]: + return self.get_graphs_of_shapes() + + def get_graph_of_shapes(self, requirement_name: str, refresh: bool = False): + # load the graph of shapes + if not self._shapes_graphs or refresh: + self._shapes_graphs = self.load_graphs_of_shapes() + return self._shapes_graphs.get(requirement_name) + + def load_ontologies_graph(self): + # load the graph of ontologies + ontologies_graph = Graph() + if self.ontologies_path: + ontologies_graph.parse(self.ontologies_path, format="ttl") + return ontologies_graph + + def get_ontologies_graph(self, refresh: bool = False): + # load the graph of ontologies + if not self._ontologies_graph or refresh: + self._ontologies_graph = self.load_ontologies_graph() + return self._ontologies_graph + + @property + def ontologies_graph(self) -> Graph: + return self.get_ontologies_graph() + + def validate_requirement(self, requirement: Requirement) -> CheckResult: + # check if requirement is an instance of Requirement + assert isinstance(requirement, Requirement), "Invalid requirement" + # check if the requirement has a check class + assert requirement.check_class, "Check class not associated with requirement" + # instantiate the check class + check = requirement.check_class(requirement, self) + # run the check + check.__do_check__() + # return the result + return check.result + + def validate(self) -> ValidationResult: + + # initialize the validation result + validation_result = ValidationResult( + rocrate_path=self.rocrate_path, validation_settings=self.validation_settings) + + # perform the requirements validation + for requirement in self.profile.requirements: + logger.debug("Validating Requirement: %s", requirement) + result = self.validate_requirement(requirement) + logger.debug("Issues: %r", result.get_issues()) + if result and result.passed(): + logger.debug("Validation Requirement passed: %s", requirement) + else: + logger.debug(f"Validation Requirement {requirement} failed ") + validation_result.add_issues(result.get_issues()) + if self.validation_settings.get("abort_on_first"): + logger.debug("Aborting on first failure") + return validation_result + + return validation_result From fa2dbcaf5663d241a98a69a50c2a7e79ca937030 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 09:48:04 +0100 Subject: [PATCH 051/902] refactor(core): :recycle: rewrite validation service using the Validator class --- rocrate_validator/service.py | 102 +++++------------------------------ 1 file changed, 13 insertions(+), 89 deletions(-) diff --git a/rocrate_validator/service.py b/rocrate_validator/service.py index a053c09b..618bc1ea 100644 --- a/rocrate_validator/service.py +++ b/rocrate_validator/service.py @@ -2,13 +2,8 @@ from typing import Literal, Optional, Union from pyshacl.pytypes import GraphLike -from rdflib import Graph -from .checks.shacl import Validator -from .checks.shacl.errors import SHACLValidationError -from .constants import ROCRATE_METADATA_FILE -from .models import ValidationResult -from .utils import get_full_graph +from .models import ValidationResult, Validator # set up logging logger = logging.getLogger(__name__) @@ -16,7 +11,9 @@ def validate( rocrate_path: Union[GraphLike, str, bytes], - shapes_path: Union[GraphLike, str, bytes], + profiles_path: str = "./profiles", + profile_name: str = "ro-crate", + inherit_profiles: bool = True, ontologies_path: Union[GraphLike, str, bytes] = None, advanced: Optional[bool] = False, inference: Optional[Literal["owl", "rdfs"]] = False, @@ -26,83 +23,15 @@ def validate( allow_warnings: Optional[bool] = False, serialization_output_path: str = None, serialization_output_format: str = "turtle", - fail_fast: Optional[bool] = True, **kwargs, ) -> ValidationResult: - """ - Validate a data graph using SHACL shapes as constraints - - :param rocrate_path: The path to the RO-Crate metadata file - :param shapes_path: The path to the SHACL shapes file - - :return: True if the data graph conforms to the SHACL shapes - :raises SHACLValidationError: If the data graph does not conform to the SHACL shapes - :raises CheckValidationError: If a check fails - """ - - # set the RO-Crate metadata file - rocrate_metadata_path = f"{rocrate_path}/{ROCRATE_METADATA_FILE}" - logger.debug("rocrate_metadata_path: %s", rocrate_metadata_path) - - from .checks import get_checks - - # keep track of the validation settings - validation_settings = { - "shapes_path": shapes_path, - "ontologies_path": ontologies_path, - "advanced": advanced, - "inference": inference, - "inplace": inplace, - "abort_on_first": abort_on_first, - "allow_infos": allow_infos, - "allow_warnings": allow_warnings, - "serialization_output_path": serialization_output_path, - "serialization_output_format": serialization_output_format, - **kwargs, - } - - # initialize the validation result - validation_result = ValidationResult(rocrate_path=rocrate_path, validation_settings=validation_settings) - - # get the checks - for check_instance in get_checks(rocrate_path=rocrate_path): - logger.debug("Loaded check: %s", check_instance) - result = check_instance.check() - if result: - logger.debug("Check passed: %s", check_instance.name) - else: - logger.debug(f"Check {check_instance.name} failed ") - validation_result.add_issues(check_instance.get_issues()) - if fail_fast: - logger.debug("Failing fast") - return validation_result - # issues = check_instance.get_issues() - # raise CheckValidationError( - # check_instance, issues[0].message, rocrate_path, issues[0].code - # ) - - # load the data graph - data_graph = Graph() - data_graph.parse(rocrate_metadata_path, - format="json-ld", publicID=rocrate_path) - validation_settings["data_graph"] = data_graph - - # load the shapes graph - shacl_graph = None - if shapes_path: - shacl_graph = get_full_graph(shapes_path, publicID=rocrate_path) - validation_settings["shapes_graph"] = shacl_graph - - # load the ontology graph - ontology_graph = None - if ontologies_path: - ontology_graph = get_full_graph(ontologies_path) - validation_settings["ont_graph"] = ontology_graph validator = Validator( - shapes_graph=shacl_graph, ont_graph=ontology_graph) - result = validator.validate( - data_graph=data_graph, + rocrate_path=rocrate_path, + profiles_path=profiles_path, + profile_name=profile_name, + inherit_profiles=inherit_profiles, + ontologies_path=ontologies_path, advanced=advanced, inference=inference, inplace=inplace, @@ -111,14 +40,9 @@ def validate( allow_warnings=allow_warnings, serialization_output_path=serialization_output_path, serialization_output_format=serialization_output_format, - publicID=rocrate_path, **kwargs, ) - logger.debug("Validation conforms: %s", result.conforms) - if not result.conforms: - logger.error("Validation failed") - # TODO: wrap the validation error as issue - if not result.conforms: - raise SHACLValidationError(result, rocrate_path) - - return validation_result + logger.debug("Validator created. Starting validation...") + result = validator.validate() + logger.debug("Validation completed: %s", result) + return result From 9591591704dc30a1d9a15096e4b04fa53e589839 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 09:49:39 +0100 Subject: [PATCH 052/902] style(core): :rotating_light: fix line too long --- rocrate_validator/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 5814dd9c..b3cb3781 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -159,7 +159,8 @@ def load(path: Union[str, Path]): for file in sorted(files, key=lambda x: (not x.endswith('.py'), x)): requirement_path = requirement_root / file logger.debug("File: %s (root: %s)", requirement_path, requirement_root.name) - for requirement in Requirement.load(profile, RequirementLevels.get(requirement_level), requirement_path): + for requirement in Requirement.load( + profile, RequirementLevels.get(requirement_level), requirement_path): profile.add_requirement(requirement) logger.debug("Added requirement: %s", requirement) From 594536f199223fbc77ba5432a3d7874b9c0832e2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 10:25:49 +0100 Subject: [PATCH 053/902] feat(cli): :art: improve CLI output --- rocrate_validator/cli.py | 192 ++++++++++++++++++++------------------- 1 file changed, 98 insertions(+), 94 deletions(-) diff --git a/rocrate_validator/cli.py b/rocrate_validator/cli.py index f04edfa8..96bbc25a 100644 --- a/rocrate_validator/cli.py +++ b/rocrate_validator/cli.py @@ -1,16 +1,13 @@ import logging import os -import click +import rich_click as click from rich.console import Console -from rocrate_validator.errors import CheckValidationError from rocrate_validator.service import validate as validate_rocrate -from .checks import Severity -from .checks.shacl.errors import SHACLValidationError from .colors import get_severity_color -from .models import ValidationResult +from .models import Severity, ValidationResult # set up logging logger = logging.getLogger(__name__) @@ -21,29 +18,15 @@ @click.group(invoke_without_command=True) +@click.rich_config(help_config=click.RichHelpConfiguration(use_rich_markup=True)) @click.option( '--debug', is_flag=True, help="Enable debug logging", default=False ) -@click.option( - "-s", - "--shapes-path", - type=click.Path(exists=True), - default="./shapes", - help="Path containing the shapes files", -) -# @click.option( -# "-o", -# "--ontologies-path", -# type=click.Path(exists=True), -# default="./ontologies", -# help="Path containing the ontology files", -# ) -@click.argument("rocrate-path", type=click.Path(exists=True), default=".") @click.pass_context -def cli(ctx, debug, shapes_path, ontologies_path=None, rocrate_path="."): +def cli(ctx, debug: bool = False): # Set the log level if debug: logging.basicConfig(level=logging.DEBUG) @@ -52,44 +35,107 @@ def cli(ctx, debug, shapes_path, ontologies_path=None, rocrate_path="."): # If no subcommand is provided, invoke the default command if ctx.invoked_subcommand is None: # If no subcommand is provided, invoke the default command - ctx.invoke(validate, shapes_path=shapes_path, - ontologies_path=ontologies_path, - rocrate_path=rocrate_path) + ctx.invoke(validate) @cli.command("validate") -# ??? is it relevant ? +@click.argument("rocrate-path", type=click.Path(exists=True), default=".") +@click.option( + '-no-ff', + '--no-fail-fast', + is_flag=True, + help="Disable fail fast validation mode", + default=False, + show_default=True +) +@click.option( + "--profiles-path", + type=click.Path(exists=True), + default="./profiles", + show_default=True, + help="Path containing the profiles files", +) +@click.option( + "-p", + "--profile-name", + type=click.STRING, + default="ro-crate", + show_default=True, + help="Name of the profile to use for validation", +) +@click.option( + '-nh', + '--disable-profile-inheritance', + is_flag=True, + help="Disable inheritance of profiles", + default=False, + show_default=True +) +@click.option( + "-l", + "--requirement-level", + type=click.Choice(["MUST", "SHOULD", "MAY"], case_sensitive=False), + default="MUST", + show_default=True, + help="Level of the requirements to validate", +) @click.option( - '-x', - '--fail-fast', + '-lo', + '--requirement-level-only', is_flag=True, - help="Fail fast", - default=True + help="Validate only the requirements of the specified level (no levels with lower severity)", + default=False, + show_default=True ) -def validate(shapes_path: str, ontologies_path: str = None, - rocrate_path: str = ".", fail_fast: bool = True): +# @click.option( +# "-o", +# "--ontologies-path", +# type=click.Path(exists=True), +# default="./ontologies", +# help="Path containing the ontology files", +# ) +@click.pass_context +def validate(ctx, + profiles_path: str = "./profiles", + profile_name: str = "ro-crate", + disable_profile_inheritance: bool = False, + requirement_level: str = "MUST", + requirement_level_only: bool = False, + rocrate_path: str = ".", + no_fail_fast: bool = False, + ontologies_path: str = None): """ - Validate a RO-Crate using SHACL shapes as constraints. - * this command might be the only one needed for the CLI. - ??? merge this command with the main command ? + [magenta]rocrate-validator:[/magenta] Validate a RO-Crate against a profile """ - # Log the input parameters for debugging - if shapes_path: - logger.debug("shapes_path: %s", os.path.abspath(shapes_path)) + logger.debug("profiles_path: %s", os.path.abspath(profiles_path)) + logger.debug("profile_name: %s", profile_name) + logger.debug("requirement_level: %s", requirement_level) + logger.debug("requirement_level_only: %s", requirement_level_only) + + logger.debug("disable_inheritance: %s", disable_profile_inheritance) + logger.debug("rocrate_path: %s", os.path.abspath(rocrate_path)) + logger.debug("no_fail_fast: %s", no_fail_fast) + logger.debug("fail fast: %s", not no_fail_fast) + if ontologies_path: logger.debug("ontologies_path: %s", os.path.abspath(ontologies_path)) if rocrate_path: - logger.debug("ontologies_path: %s", os.path.abspath(rocrate_path)) + logger.debug("rocrate_path: %s", os.path.abspath(rocrate_path)) try: # Validate the RO-Crate result: ValidationResult = validate_rocrate( + profiles_path=profiles_path, + profile_name=profile_name, + requirement_level=requirement_level, + requirement_level_only=requirement_level_only, + disable_profile_inheritance=disable_profile_inheritance, rocrate_path=os.path.abspath(rocrate_path), - shapes_path=os.path.abspath(shapes_path) if shapes_path else None, ontologies_path=os.path.abspath( ontologies_path) if ontologies_path else None, + abort_on_first=not no_fail_fast ) # Print the validation result @@ -97,21 +143,11 @@ def validate(shapes_path: str, ontologies_path: str = None, except Exception as e: console.print( - "\n\n[bold]\[[red]FAILED[/red]] Unexpected error !!![/bold]\n", + f"\n\n[bold]\[[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", style="white", ) if logger.isEnabledFor(logging.DEBUG): - console.print_exception(e) - # if isinstance(e, CheckValidationError): - # console.print( - # f"Check [bold][red]{e.check.name}[/red][/bold] failed: ") - # console.print(f" -> {str(e)}\n\n", style="white") - # elif isinstance(e, SHACLValidationError): - # _log_validation_result_(e.result) - # else: - # console.print("Error: ", style="red", end="") - # console.print(f" -> {str(e)}\n\n", style="white") - # console.print("\n\n", style="white") + console.print_exception() def __print_validation_result__( @@ -131,53 +167,21 @@ def __print_validation_result__( style="white", ) - for issue in result.get_issues(severity=severity): - issue_color = get_severity_color(issue.severity) + for check in result.get_failed_checks(): + # TODO: Add color related to the requirement level associated with the check + issue_color = get_severity_color(Severity.MUST) console.print( - f" -> [bold][magenta]{issue.check.name}[/magenta] check [red]failed[/red][/bold]", + f" -> [bold][magenta]{check.name}[/magenta] check [red]failed[/red][/bold]", style="white", ) - console.print(f"{' '*4}{issue.check.description}\n", style="white italic") + console.print(f"{' '*4}{check.description}\n", style="white italic") console.print(f"{' '*4}Detected issues:", style="white bold") - console.print( - f"{' '*4}- [[{issue_color}]{issue.severity.name}[/{issue_color}] " - f"[magenta]{issue.code}[/magenta]]: {issue.message}\n\n", - style="white") - - -def _log_validation_result_(result: bool): - # Print the number of violations - logger.debug("* Number of violations: %s" % len(result.violations)) - - console.print("\n[bold]** %s validation errors: [/bold]" % - len(result.violations)) - - # Print the violations - count = 0 - for v in result.violations: - count += 1 - console.print( - "\n -> [red][bold]Violation " - f"{count}[/bold][/red]: {v.resultMessage}", - style="white", - ) - print(" - resultSeverity: %s" % v.resultSeverity) - print(" - focusNode: %s" % v.focusNode) - print(" - resultPath: %s" % v.resultPath) - print(" - value: %s" % v.value) - print(" - resultMessage: %s" % v.resultMessage) - print(" - sourceConstraintComponent: %s" % v.sourceConstraintComponent) - try: - if v.sourceShape: - print(" - sourceShape: %s" % v.sourceShape) - print(" - sourceShape.name: %s" % v.sourceShape.name) - print(" - sourceShape.description: %s" % - v.sourceShape.description) - print(" - sourceShape.path: %s" % v.sourceShape.path) - print(" - sourceShape.nodeKind: %s" % v.sourceShape.nodeKind) - except Exception as e: - print(f"Error getting source shape: {e}") - print("\n") + for issue in check.get_issues(): + console.print( + f"{' '*4}- [[{issue_color}]{issue.severity.name}[/{issue_color}] " + f"[magenta]{issue.code}[/magenta]]: {issue.message}", + style="white") + console.print("\n", style="white") if __name__ == "__main__": From e877376ea9b30e3f7498eee8162f707531004316 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 10:42:27 +0100 Subject: [PATCH 054/902] fix(shacl): :bug: set base URI for shapes loading --- rocrate_validator/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index b3cb3781..61ecd70f 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -611,8 +611,10 @@ def profile_path(self): def load_data_graph(self): data_graph = Graph() + logger.debug("Loading RO-Crate metadata: %s", self.rocrate_metadata_path) data_graph.parse(self.rocrate_metadata_path, format="json-ld", publicID=self.rocrate_path) + logger.debug("RO-Crate metadata loaded: %s", data_graph) return data_graph def get_data_graph(self, refresh: bool = False): From 6a96b234bb1ae371ec88322cf476d0a7d4dec7b6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 11:27:47 +0100 Subject: [PATCH 055/902] fix(core): :bug: ensure trailing '/' for the publicID --- rocrate_validator/models.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 61ecd70f..059f68aa 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -514,6 +514,10 @@ def get_rocrate_path(self): def get_validation_settings(self): return self._validation_settings + def get_failed_checks(self) -> Set[Check]: + # return the set of checks that failed + return set([issue.check for issue in self._issues]) + def add_issue(self, issue: CheckIssue): self._issues.append(issue) @@ -613,7 +617,7 @@ def load_data_graph(self): data_graph = Graph() logger.debug("Loading RO-Crate metadata: %s", self.rocrate_metadata_path) data_graph.parse(self.rocrate_metadata_path, - format="json-ld", publicID=self.rocrate_path) + format="json-ld", publicID=self.publicID) logger.debug("RO-Crate metadata loaded: %s", data_graph) return data_graph @@ -643,13 +647,20 @@ def get_profile(self, refresh: bool = False): def profile(self) -> Profile: return self.get_profile() + @property + def publicID(self) -> str: + if not self.rocrate_path.endswith("/"): + return f"{self.rocrate_path}/" + return self.rocrate_path + def load_graphs_of_shapes(self): # load the graph of shapes shapes_graphs = {} for requirement in self._profile.requirements: if requirement.path.suffix == ".ttl": shapes_graph = Graph() - shapes_graph.parse(str(requirement.path), format="ttl") + shapes_graph.parse(str(requirement.path), format="ttl", + publicID=self.publicID) shapes_graphs[requirement.name] = shapes_graph return shapes_graphs From 53a1de4c61472493145a2a0b03187f4eff46048f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 11:32:45 +0100 Subject: [PATCH 056/902] refactor(shacl): :art: improve navigation between objects --- .../requirements/shacl/__init__.py | 2 +- .../requirements/shacl/checks.py | 2 +- .../requirements/shacl/errors.py | 2 +- .../requirements/shacl/models.py | 164 +-------------- .../requirements/shacl/validator.py | 196 +++++++++++++++++- 5 files changed, 196 insertions(+), 170 deletions(-) diff --git a/rocrate_validator/requirements/shacl/__init__.py b/rocrate_validator/requirements/shacl/__init__.py index 68ed4f5d..3bf66a35 100644 --- a/rocrate_validator/requirements/shacl/__init__.py +++ b/rocrate_validator/requirements/shacl/__init__.py @@ -1,5 +1,5 @@ from .checks import SHACLCheck -from .models import ValidationResult +from .validator import ValidationResult from .validator import Validator from .errors import SHACLValidationError diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 44ac5031..bd186e0e 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -70,7 +70,7 @@ def check(self): data_graph = self.validator.data_graph shacl_validator = SHACLValidator( - shapes_graph=shapes_graph, ont_graph=ontology_graph) + self, shapes_graph=shapes_graph, ont_graph=ontology_graph) result = shacl_validator.validate( data_graph=data_graph, **self.validator.validation_settings diff --git a/rocrate_validator/requirements/shacl/errors.py b/rocrate_validator/requirements/shacl/errors.py index 7af6457d..085b42cd 100644 --- a/rocrate_validator/requirements/shacl/errors.py +++ b/rocrate_validator/requirements/shacl/errors.py @@ -1,5 +1,5 @@ from ...errors import ValidationError -from .models import ValidationResult +from .validator import ValidationResult class SHACLValidationError(ValidationError): diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 26cf9d78..d4101389 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -1,13 +1,10 @@ import json import logging -import os -from typing import List -from rdflib import Graph, URIRef +from rdflib import Graph from rdflib.term import Node from ...constants import SHACL_NS -from ...models import CheckIssue, Severity # set up logging logger = logging.getLogger(__name__) @@ -68,162 +65,3 @@ def nodeKind(self): if nodeKind: return nodeKind[0]['@id'] return None - - -class Violation(CheckIssue): - - def __init__(self, violation_node: Node, graph: Graph) -> None: - # check the input - assert isinstance(violation_node, Node), "Invalid violation node" - assert isinstance(graph, Graph), "Invalid graph" - - # store the input - self._violation_node = violation_node - self._graph = graph - - # create a graph for the violation - violation_graph = Graph() - violation_graph += graph.triples((violation_node, None, None)) - self.violation_graph = violation_graph - - # serialize the graph in json-ld - violation_json = violation_graph.serialize(format="json-ld") - violation_obj = json.loads(violation_json) - self.violation_json = violation_obj[0] - - # get the source shape - shapes = list(graph.triples( - (violation_node, URIRef(f"{SHACL_NS}sourceShape"), None))) - self.source_shape_node = shapes[0][2] - # initialize the parent class - super().__init__(severity=self.resultSeverity, - message=self.resultMessage) - - @property - def node(self) -> Node: - return self._violation_node - - @property - def graph(self) -> Graph: - return self._graph - - @property - def resultSeverity(self): - return self.violation_json[f'{SHACL_NS}resultSeverity'][0]['@id'] - - @property - def focusNode(self): - return self.violation_json[f'{SHACL_NS}focusNode'][0]['@id'] - - @property - def resultPath(self): - return self.violation_json[f'{SHACL_NS}resultPath'][0]['@id'] - - @property - def value(self): - value = self.violation_json.get(f'{SHACL_NS}value', None) - if not value: - return None - return value[0]['@id'] - - @property - def resultMessage(self): - return self.violation_json[f'{SHACL_NS}resultMessage'][0]['@value'] - - @property - def sourceConstraintComponent(self): - return self.violation_json[f'{SHACL_NS}sourceConstraintComponent'][0]['@id'] - - @property - def sourceShape(self) -> Shape: - try: - return Shape(self.source_shape_node, self._graph) - except Exception as e: - logger.error("Error getting source shape: %s" % e) - if logger.isEnabledFor(logging.DEBUG): - logger.exception(e) - return None - - @property - def message(self): - return self.resultMessage - - @property - def description(self): - return self.sourceShape.description - - @property - def severity(self): - # TODO: map the severity to the CheckIssue severity - return Severity.ERROR - - -class ValidationResult: - - def __init__(self, results_graph: Graph, conforms: bool = None, results_text: str = None) -> None: - # validate the results graph input - assert results_graph is not None, "Invalid graph" - assert isinstance(results_graph, Graph), "Invalid graph type" - # check if the graph is valid ValidationReport - assert (None, URIRef(f"{SHACL_NS}conforms"), - None) in results_graph, "Invalid ValidationReport" - # store the input properties - self._conforms = conforms - self.results_graph = results_graph - self._text = results_text - # parse the results graph - self._violations = self.parse_results_graph(results_graph) - # initialize the conforms property - if conforms is not None: - self._conforms = len(self._violations) == 0 - else: - assert self._conforms == len( - self._violations) == 0, "Invalid validation result" - - @staticmethod - def parse_results_graph(results_graph: Graph): - # Query for validation results - query = """ - SELECT ?subject - WHERE {{ - ?subject a <{0}ValidationResult> . - }} - """.format(SHACL_NS) - - query_results = results_graph.query(query) - - violations = [] - for r in query_results: - violation_node = r[0] - violation = Violation(violation_node, results_graph) - violations.append(violation) - - return violations - - @property - def conforms(self) -> bool: - return self._conforms - - @property - def violations(self) -> List: - return self._violations - - @property - def text(self) -> str: - return self._text - - @staticmethod - def from_serialized_results_graph(file_path: str, format: str = 'turtle'): - # check the input - assert format in ['turtle', 'n3', 'nt', - 'xml', 'rdf', 'json-ld'], "Invalid format" - assert file_path, "Invalid file path" - assert os.path.exists(file_path), "File does not exist" - # Load the graph - logger.debug("Loading graph from file: %s" % file_path) - g = Graph() - g.parse(file_path, format=format) - logger.debug("Graph loaded from file: %s" % file_path) - - # return the validation result - return ValidationResult(g) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 50bb38e0..51a675b5 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -1,24 +1,207 @@ +from __future__ import annotations + +import json import logging -from typing import Optional, Union +import os +from typing import List, Optional, Union import pyshacl from pyshacl.pytypes import GraphLike from rdflib import Graph +from rdflib.term import Node, URIRef from ...constants import (RDF_SERIALIZATION_FORMATS, - RDF_SERIALIZATION_FORMATS_TYPES, + RDF_SERIALIZATION_FORMATS_TYPES, SHACL_NS, VALID_INFERENCE_OPTIONS, VALID_INFERENCE_OPTIONS_TYPES) -from .models import ValidationResult +from ...models import Check, CheckIssue, Severity +from ...requirements.shacl.models import Shape # set up logging logger = logging.getLogger(__name__) +class Violation(CheckIssue): + + def __init__(self, result: ValidationResult, violation_node: Node, graph: Graph) -> None: + # check the input + assert isinstance(violation_node, Node), "Invalid violation node" + assert isinstance(graph, Graph), "Invalid graph" + + # store the input + self._violation_node = violation_node + self._graph = graph + + # store the result object + self._result = result + + # create a graph for the violation + violation_graph = Graph() + violation_graph += graph.triples((violation_node, None, None)) + self.violation_graph = violation_graph + + # serialize the graph in json-ld + violation_json = violation_graph.serialize(format="json-ld") + violation_obj = json.loads(violation_json) + self.violation_json = violation_obj[0] + + # get the source shape + shapes = list(graph.triples( + (violation_node, URIRef(f"{SHACL_NS}sourceShape"), None))) + self.source_shape_node = shapes[0][2] + # initialize the parent class + super().__init__(severity=self.resultSeverity, + message=self.resultMessage) + + @property + def result(self): + return self._result + + @property + def validator(self): + return self.result.validator + + @property + def check(self): + return self.result.validator.check + + @property + def node(self) -> Node: + return self._violation_node + + @property + def graph(self) -> Graph: + return self._graph + + @property + def resultSeverity(self): + return self.violation_json[f'{SHACL_NS}resultSeverity'][0]['@id'] + + @property + def focusNode(self): + return self.violation_json[f'{SHACL_NS}focusNode'][0]['@id'] + + @property + def resultPath(self): + return self.violation_json[f'{SHACL_NS}resultPath'][0]['@id'] + + @property + def value(self): + value = self.violation_json.get(f'{SHACL_NS}value', None) + if not value: + return None + return value[0]['@id'] + + @property + def sourceConstraintComponent(self): + return self.violation_json[f'{SHACL_NS}sourceConstraintComponent'][0]['@id'] + + @property + def sourceShape(self) -> Shape: + try: + return Shape(self.source_shape_node, self._graph) + except Exception as e: + logger.error("Error getting source shape: %s" % e) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return None + + @property + def message(self): + return self.resultMessage + + @property + def description(self): + return self.sourceShape.description + + @property + def severity(self): + # TODO: map the severity to the CheckIssue severity + return Severity.ERROR + + +class ValidationResult: + + def __init__(self, validator: Validator, results_graph: Graph, + conforms: bool = None, results_text: str = None) -> None: + # validate the results graph input + assert results_graph is not None, "Invalid graph" + assert isinstance(results_graph, Graph), "Invalid graph type" + # check if the graph is valid ValidationReport + assert (None, URIRef(f"{SHACL_NS}conforms"), + None) in results_graph, "Invalid ValidationReport" + # store the input properties + self._conforms = conforms + self.results_graph = results_graph + self._text = results_text + self._validator = validator + # parse the results graph + self._violations = self.parse_results_graph(results_graph) + # initialize the conforms property + logger.warning("Validation report: %s" % self._text) + if conforms is not None: + self._conforms = len(self._violations) == 0 + else: + assert self._conforms == len( + self._violations) == 0, "Invalid validation result" + + def parse_results_graph(self, results_graph: Graph): + # Query for validation results + query = """ + SELECT ?subject + WHERE {{ + ?subject a <{0}ValidationResult> . + }} + """.format(SHACL_NS) + + query_results = results_graph.query(query) + + violations = [] + for r in query_results: + violation_node = r[0] + violation = Violation(self, violation_node, results_graph) + violations.append(violation) + + return violations + + @property + def validator(self) -> Validator: + return self._validator + + @property + def conforms(self) -> bool: + return self._conforms + + @property + def violations(self) -> List: + return self._violations + + @property + def text(self) -> str: + return self._text + + @staticmethod + def from_serialized_results_graph(file_path: str, format: str = 'turtle'): + # check the input + assert format in ['turtle', 'n3', 'nt', + 'xml', 'rdf', 'json-ld'], "Invalid format" + assert file_path, "Invalid file path" + assert os.path.exists(file_path), "File does not exist" + # Load the graph + logger.debug("Loading graph from file: %s" % file_path) + g = Graph() + g.parse(file_path, format=format) + logger.debug("Graph loaded from file: %s" % file_path) + + # return the validation result + return ValidationResult(g) + + class Validator: def __init__( self, + check: Check, shapes_graph: Optional[Union[GraphLike, str, bytes]], ont_graph: Optional[Union[GraphLike, str, bytes]] = None, ) -> None: @@ -35,6 +218,7 @@ def __init__( """ self._shapes_graph = shapes_graph self._ont_graph = ont_graph + self._check = check @property def shapes_graph(self) -> Optional[Union[GraphLike, str, bytes]]: @@ -44,6 +228,10 @@ def shapes_graph(self) -> Optional[Union[GraphLike, str, bytes]]: def ont_graph(self) -> Optional[Union[GraphLike, str, bytes]]: return self._ont_graph + @property + def check(self) -> Check: + return self._check + def validate( self, data_graph: Union[GraphLike, str, bytes], @@ -138,4 +326,4 @@ def validate( serialization_output_path, format=serialization_output_format ) # return the validation result - return ValidationResult(results_graph, conforms, results_text) + return ValidationResult(self, results_graph, conforms, results_text) From 2e770840e3b7692885a0a9b886cd63abe66eb87e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 11:34:32 +0100 Subject: [PATCH 057/902] feat(shacl): :lipstick: make node URIs relative to the ro-crate path --- rocrate_validator/requirements/shacl/validator.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 51a675b5..ce054356 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -92,6 +92,16 @@ def value(self): return None return value[0]['@id'] + def make_uris_relative(self, text: str): + # globally replace the string "file://" with "./ + return text.replace(f'file://{self.check.ro_crate_path}', '.') + + @property + def resultMessage(self): + return self.make_uris_relative( + self.violation_json[f'{SHACL_NS}resultMessage'][0]['@value'] + ) + @property def sourceConstraintComponent(self): return self.violation_json[f'{SHACL_NS}sourceConstraintComponent'][0]['@id'] From f05796b9b6829c939db8fc6cdecac6e223559bb5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 11:35:09 +0100 Subject: [PATCH 058/902] fix(utils): :bug: update import --- rocrate_validator/colors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/colors.py b/rocrate_validator/colors.py index eb6a36cc..f7e00381 100644 --- a/rocrate_validator/colors.py +++ b/rocrate_validator/colors.py @@ -1,4 +1,4 @@ -from .checks import Severity +from .models import Severity def get_severity_color(severity: Severity) -> str: From 74db1b55078d7fccd852ff613fd243439e1933fd Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 11:36:07 +0100 Subject: [PATCH 059/902] fix(utils): :bug: capitalize camel case output --- rocrate_validator/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 0a26d4f3..97db6884 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -145,4 +145,4 @@ def to_camel_case(snake_str: str) -> str: :return: The camel case string """ components = re.split('_|-', snake_str) - return components[0] + ''.join(x.title() for x in components[1:]) + return components[0].capitalize() + ''.join(x.title() for x in components[1:]) From 4d9d912d6a0fbc9c5003beccece79c1e1930f769 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 11:36:57 +0100 Subject: [PATCH 060/902] feat(utils): :sparkles: add class loader --- rocrate_validator/utils.py | 55 +++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 97db6884..3b1f8ff0 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -1,8 +1,11 @@ +import inspect import logging import os import re +import sys +from importlib import import_module from pathlib import Path -from typing import List +from typing import List, Optional, Type import toml from rdflib import Graph @@ -127,14 +130,60 @@ def get_full_graph( return full_graph -def get_requirement_name_from_file(file: Path) -> str: +def get_classes_from_file(file_path: Path, filter_class: Optional[Type] = None): + # ensure the file path is a Path object + assert file_path, "The file path is required" + if not isinstance(file_path, Path): + file_path = Path(file_path) + + # Check if the file is a Python file + if not file_path.exists(): + raise ValueError("The file does not exist") + + # Check if the file is a Python file + if file_path.suffix != ".py": + raise ValueError("The file is not a Python file") + + # Get the module name from the file path + module_name = os.path.basename(file_path)[:-3] + logger.debug("Module: %r", module_name) + + # Add the directory containing the file to the system path + sys.path.insert(0, os.path.dirname(file_path)) + + # Import the module + module = import_module(module_name) + logger.debug("Module: %r", module) + logger.debug("Members: %r", inspect.getmembers(module)) + + for name, cls in inspect.getmembers(module, inspect.isclass): + logger.debug("Checking object %s", cls) + logger.debug("Module %s", inspect.getmodule(cls)) + logger.debug("Subclass %s", issubclass(cls, filter_class)) + logger.debug("Name %s", cls.__name__.endswith('Check')) + + # Get all classes in the module that are subclasses of Check + classes = {name: cls for name, cls in inspect.getmembers(module, inspect.isclass) + if cls.__module__ == module_name and cls.__name__.endswith('Check')} + # if not filter_class or (issubclass(cls, filter_class) and cls != filter_class)} + + return classes + + +def get_requirement_name_from_file(file: Path, check_name: Optional[str] = None) -> str: """ Get the requirement name from the file :param file: The file :return: The requirement name """ - return to_camel_case(file.stem).capitalize() + assert file, "The file is required" + if not isinstance(file, Path): + file = Path(file) + base_name = to_camel_case(file.stem) + if check_name: + return f"{base_name}.{check_name.replace('Check', '')}" + return base_name def to_camel_case(snake_str: str) -> str: From 6cf2edd5e55719c0f98f413439580d551251c906 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 11:44:33 +0100 Subject: [PATCH 061/902] feat(core): :sparkles: expose severity at check level --- rocrate_validator/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 059f68aa..84f9ebe5 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -372,6 +372,10 @@ def __init__(self, def requirement(self) -> Requirement: return self._requirement + @property + def severity(self) -> Severity: + return self._requirement.type + @property def name(self) -> str: if not self._name: From af4f2b24887485785a9677f3ab5bba28c52f3292 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 11:45:03 +0100 Subject: [PATCH 062/902] feat(cli): :sparkles: print check severity --- rocrate_validator/cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/cli.py b/rocrate_validator/cli.py index 96bbc25a..fe6a4d5e 100644 --- a/rocrate_validator/cli.py +++ b/rocrate_validator/cli.py @@ -169,9 +169,10 @@ def __print_validation_result__( for check in result.get_failed_checks(): # TODO: Add color related to the requirement level associated with the check - issue_color = get_severity_color(Severity.MUST) + issue_color = get_severity_color(check.severity) console.print( - f" -> [bold][magenta]{check.name}[/magenta] check [red]failed[/red][/bold]", + f" -> [bold][magenta]{check.name}[/magenta] check [red]failed[/red][/bold]" + f" (severity: [{issue_color}]{check.severity.name}[/{issue_color}])", style="white", ) console.print(f"{' '*4}{check.description}\n", style="white italic") From 23adcc2f455692f75e263e3c7769238f5adf82ba Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 11:53:33 +0100 Subject: [PATCH 063/902] refactor(core): :fire: rename module containing services --- rocrate_validator/cli.py | 3 +-- rocrate_validator/{service.py => services.py} | 0 2 files changed, 1 insertion(+), 2 deletions(-) rename rocrate_validator/{service.py => services.py} (100%) diff --git a/rocrate_validator/cli.py b/rocrate_validator/cli.py index fe6a4d5e..96691cf3 100644 --- a/rocrate_validator/cli.py +++ b/rocrate_validator/cli.py @@ -4,10 +4,9 @@ import rich_click as click from rich.console import Console -from rocrate_validator.service import validate as validate_rocrate - from .colors import get_severity_color from .models import Severity, ValidationResult +from .services import validate as validate_rocrate # set up logging logger = logging.getLogger(__name__) diff --git a/rocrate_validator/service.py b/rocrate_validator/services.py similarity index 100% rename from rocrate_validator/service.py rename to rocrate_validator/services.py From 3dd3c249ede5185e85fa17b7e109c4a72f8972a2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 11:54:28 +0100 Subject: [PATCH 064/902] refactor(cli): :wrench: rename cli option --- rocrate_validator/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/cli.py b/rocrate_validator/cli.py index 96691cf3..da1576da 100644 --- a/rocrate_validator/cli.py +++ b/rocrate_validator/cli.py @@ -40,7 +40,7 @@ def cli(ctx, debug: bool = False): @cli.command("validate") @click.argument("rocrate-path", type=click.Path(exists=True), default=".") @click.option( - '-no-ff', + '-nff', '--no-fail-fast', is_flag=True, help="Disable fail fast validation mode", From e0f1c3c9616dcbf95f9d07a7e0c03a835bc680b1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 11:57:17 +0100 Subject: [PATCH 065/902] style(core): :memo: add missing typing --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 84f9ebe5..776ff0d6 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -137,7 +137,7 @@ def __str__(self) -> str: return self.name @staticmethod - def load(path: Union[str, Path]): + def load(path: Union[str, Path]) -> Profile: # if the path is a string, convert it to a Path if isinstance(path, str): path = Path(path) From 54c9cb04e6e787163640bdbba284d37c836c1d7d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 12:33:56 +0100 Subject: [PATCH 066/902] feat(core): :sparkles: configure file extensions for filtering --- rocrate_validator/constants.py | 6 ++++++ rocrate_validator/models.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index a28c50b4..c86e27ac 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -7,6 +7,12 @@ ROCRATE_METADATA_FILE = "ro-crate-metadata.json" +# Define the list of directories to ignore when loading profiles +IGNORED_PROFILE_DIRECTORIES = ["__pycache__", ".", "README.md", "LICENSE"] + +# Define the list of enabled profile file extensions +PROFILE_FILE_EXTENSIONS = [".ttl", ".py"] + # Define allowed RDF extensions and serialization formats as map RDF_SERIALIZATION_FILE_FORMAT_MAP = { "xml": "xml", diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 776ff0d6..3db7ea8c 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -155,6 +155,8 @@ def load(path: Union[str, Path]) -> Profile: and d.upper() in levels] requirement_root = Path(root) requirement_level = requirement_root.name + files = [_ for _ in files if not _.startswith('.') and + not _.startswith('_') and Path(_).stem in PROFILE_FILE_EXTENSIONS] logger.debug("Sorted files: %s", sorted(files, key=lambda x: (not x.endswith('.py'), x))) for file in sorted(files, key=lambda x: (not x.endswith('.py'), x)): requirement_path = requirement_root / file From 514a384e2ac3d38c62da1887a02b951ed8d359b7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 13:13:26 +0100 Subject: [PATCH 067/902] fix(shacl): :loud_sound: change log level --- rocrate_validator/requirements/shacl/validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index ce054356..161f9371 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -148,7 +148,7 @@ def __init__(self, validator: Validator, results_graph: Graph, # parse the results graph self._violations = self.parse_results_graph(results_graph) # initialize the conforms property - logger.warning("Validation report: %s" % self._text) + logger.debug("Validation report: %s" % self._text) if conforms is not None: self._conforms = len(self._violations) == 0 else: From bc96fb1f14ec4dfbe8bb879196803e137a0cf614 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 14:39:49 +0100 Subject: [PATCH 068/902] feat(core): :zap: lazy loading of profile requirements --- rocrate_validator/models.py | 52 +++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 3db7ea8c..a8473347 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -102,6 +102,32 @@ def get_requirement(self, name: str) -> Requirement: return requirement return None + def load_requirements(self) -> List[Requirement]: + """ + Load the requirements from the profile directory + """ + self._requirements = [] + for root, dirs, files in os.walk(self.path): + dirs[:] = [d for d in dirs + if not d.startswith('.') and not d.startswith('_')] + requirement_root = Path(root) + requirement_level = requirement_root.name + # Filter out files that start with a dot or underscore + files = [_ for _ in files if not _.startswith('.') + and not _.startswith('_') + and Path(_).suffix in PROFILE_FILE_EXTENSIONS] + for file in sorted(files, key=lambda x: (not x.endswith('.py'), x)): + requirement_path = requirement_root / file + for requirement in Requirement.load( + self, RequirementLevels.get(requirement_level), requirement_path): + self.add_requirement(requirement) + return self._requirements + + @property + def requirements(self) -> List[Requirement]: + if not self._requirements: + self.load_requirements() + return self._requirements def has_requirement(self, name: str) -> bool: return self.get_requirement(name) is not None @@ -109,10 +135,10 @@ def get_requirements_by_type(self, type: RequirementType) -> List[Requirement]: return [requirement for requirement in self.requirements if requirement.type == type] def add_requirement(self, requirement: Requirement): - self.requirements.append(requirement) + self._requirements.append(requirement) def remove_requirement(self, requirement: Requirement): - self.requirements.remove(requirement) + self._requirements.remove(requirement) def validate(self, rocrate_path: Path) -> ValidationResult: pass @@ -145,26 +171,8 @@ def load(path: Union[str, Path]) -> Profile: assert path.is_dir(), f"Invalid profile path: {path}" # create a new profile profile = Profile(name=path.name, path=path) - levels = [_.upper() for _ in RequirementLevels.all().keys()] - for root, dirs, files in os.walk(path): - logger.debug("Root: %s", root) - logger.debug("Dirs: %s", dirs) - logger.debug("Files: %s", files) - dirs[:] = [d for d in dirs - if not d.startswith('.') and not d.startswith('_') - and d.upper() in levels] - requirement_root = Path(root) - requirement_level = requirement_root.name - files = [_ for _ in files if not _.startswith('.') and - not _.startswith('_') and Path(_).stem in PROFILE_FILE_EXTENSIONS] - logger.debug("Sorted files: %s", sorted(files, key=lambda x: (not x.endswith('.py'), x))) - for file in sorted(files, key=lambda x: (not x.endswith('.py'), x)): - requirement_path = requirement_root / file - logger.debug("File: %s (root: %s)", requirement_path, requirement_root.name) - for requirement in Requirement.load( - profile, RequirementLevels.get(requirement_level), requirement_path): - profile.add_requirement(requirement) - logger.debug("Added requirement: %s", requirement) + logger.debug("Loaded profile: %s", profile) + return profile return profile From 2853b571f9d134c8eaf68eae3eac2c6cc789f613 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 14:40:35 +0100 Subject: [PATCH 069/902] feat(core): :sparkles: expose method to load all profiles --- rocrate_validator/models.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index a8473347..0874ec1c 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -174,7 +174,23 @@ def load(path: Union[str, Path]) -> Profile: logger.debug("Loaded profile: %s", profile) return profile - return profile + @staticmethod + def load_profiles(profiles_path: Union[str, Path]) -> Dict[str, Profile]: + # if the path is a string, convert it to a Path + if isinstance(profiles_path, str): + profiles_path = Path(profiles_path) + # check if the path is a directory + assert profiles_path.is_dir(), f"Invalid profiles path: {profiles_path}" + # initialize the profiles + profiles = {} + # iterate through the directories + for profile_path in profiles_path.iterdir(): + logger.debug("Checking profile path: %s %s %r", profile_path, + profile_path.is_dir(), IGNORED_PROFILE_DIRECTORIES) + if profile_path.is_dir() and profile_path not in IGNORED_PROFILE_DIRECTORIES: + profile = Profile.load(profile_path) + profiles[profile.name] = profile + return profiles class Requirement: From f3823fea7d4d6b985cb4387c9614bd0ab5f2fb01 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 14:41:34 +0100 Subject: [PATCH 070/902] feat(core): :sparkles: allow to describe a profile through a README.md --- rocrate_validator/constants.py | 3 +++ rocrate_validator/models.py | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index c86e27ac..684ca07b 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -3,9 +3,12 @@ # Define SHACL namespace SHACL_NS = "http://www.w3.org/ns/shacl#" + # Define the rocrate-metadata.json file name ROCRATE_METADATA_FILE = "ro-crate-metadata.json" +# Define the default README file name for the RO-Crate profile +DEFAULT_PROFILE_README_FILE = "README.md" # Define the list of directories to ignore when loading profiles IGNORED_PROFILE_DIRECTORIES = ["__pycache__", ".", "README.md", "LICENSE"] diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 0874ec1c..11591417 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -82,6 +82,7 @@ def __init__(self, name: str, path: Path = None, requirements: Set[Requirement] = None): self._path = path self._name = name + self._description = None self._requirements = requirements if requirements else [] @property @@ -93,8 +94,18 @@ def name(self): return self._name @property - def requirements(self) -> List[Requirement]: - return self._requirements + def readme_file_path(self) -> Path: + return self.path / DEFAULT_PROFILE_README_FILE + + @property + def description(self) -> str: + if not self._description: + if self.path and self.readme_file_path.exists(): + with open(self.readme_file_path, "r") as f: + self._description = f.read() + else: + self._description = "RO-Crate profile" + return self._description def get_requirement(self, name: str) -> Requirement: for requirement in self.requirements: From d709c91386ebc34559d4950eed3ca277b37e74d2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 14:44:09 +0100 Subject: [PATCH 071/902] feat(utils): :sparkles: color by severity name --- rocrate_validator/colors.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/rocrate_validator/colors.py b/rocrate_validator/colors.py index f7e00381..43aae2ff 100644 --- a/rocrate_validator/colors.py +++ b/rocrate_validator/colors.py @@ -1,28 +1,30 @@ +from typing import Union + from .models import Severity -def get_severity_color(severity: Severity) -> str: +def get_severity_color(severity: Union[str, Severity]) -> str: """ Get the color for the severity :param severity: The severity :return: The color """ - if severity == Severity.ERROR: + if severity == Severity.ERROR or severity == "ERROR": return "red" - elif severity == Severity.MUST: + elif severity == Severity.MUST or severity == "MUST": return "red" - elif severity == Severity.MUST_NOT: + elif severity == Severity.MUST_NOT or severity == "MUST_NOT": return "purple" - elif severity == Severity.SHOULD: + elif severity == Severity.SHOULD or severity == "SHOULD": return "yellow" - elif severity == Severity.SHOULD_NOT: + elif severity == Severity.SHOULD_NOT or severity == "SHOULD_NOT": return "lightyellow" - elif severity == Severity.MAY: + elif severity == Severity.MAY or severity == "MAY": return "orange" - elif severity == Severity.INFO: + elif severity == Severity.INFO or severity == "INFO": return "lightblue" - elif severity == Severity.WARNING: + elif severity == Severity.WARNING or severity == "WARNING": return "yellow green" else: return "white" From ddc42bb5b61df45ff56fe7a46d641c34aba82cc8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 14:44:52 +0100 Subject: [PATCH 072/902] feat(services): :sparkles: add service endpoint to list profiles --- rocrate_validator/services.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 618bc1ea..5f867384 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -1,9 +1,9 @@ import logging -from typing import Literal, Optional, Union +from typing import Dict, Literal, Optional, Union from pyshacl.pytypes import GraphLike -from .models import ValidationResult, Validator +from .models import Profile, ValidationResult, Validator # set up logging logger = logging.getLogger(__name__) @@ -46,3 +46,10 @@ def validate( result = validator.validate() logger.debug("Validation completed: %s", result) return result + + +def get_profiles(profiles_path: str = "./profiles") -> Dict[str, Profile]: + + profiles = Profile.load_profiles(profiles_path) + logger.debug("Profiles loaded: %s", profiles) + return profiles From 9d4ba8a2ac5779e91b43bd453f8dcf53a1053aef Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 14:45:46 +0100 Subject: [PATCH 073/902] fix(core): --- rocrate_validator/models.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 11591417..30bfbbbe 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -10,7 +10,11 @@ from rdflib import Graph -from rocrate_validator.constants import (RDF_SERIALIZATION_FORMATS_TYPES, ROCRATE_METADATA_FILE, +from rocrate_validator.constants import (DEFAULT_PROFILE_README_FILE, + IGNORED_PROFILE_DIRECTORIES, + PROFILE_FILE_EXTENSIONS, + RDF_SERIALIZATION_FORMATS_TYPES, + ROCRATE_METADATA_FILE, VALID_INFERENCE_OPTIONS_TYPES) from rocrate_validator.utils import (get_classes_from_file, get_file_descriptor_path, @@ -25,21 +29,33 @@ class RequirementType: value: int def __eq__(self, other): + if not isinstance(other, RequirementType): + return False return self.name == other.name and self.value == other.value def __ne__(self, other): + if not isinstance(other, RequirementType): + return True return self.name != other.name or self.value != other.value def __lt__(self, other): + if not isinstance(other, RequirementType): + raise ValueError(f"Cannot compare RequirementType with {type(other)}") return self.value < other.value def __le__(self, other): + if not isinstance(other, RequirementType): + raise ValueError(f"Cannot compare RequirementType with {type(other)}") return self.value <= other.value def __gt__(self, other): + if not isinstance(other, RequirementType): + raise ValueError(f"Cannot compare RequirementType with {type(other)}") return self.value > other.value def __ge__(self, other): + if not isinstance(other, RequirementType): + raise ValueError(f"Cannot compare RequirementType with {type(other)}") return self.value >= other.value def __hash__(self): @@ -139,6 +155,12 @@ def requirements(self) -> List[Requirement]: if not self._requirements: self.load_requirements() return self._requirements + + @property + def requirements_by_severity_map(self) -> Dict[RequirementType, List[Requirement]]: + return {severity: self.get_requirements_by_type(severity) + for severity in RequirementLevels.all().values()} + def has_requirement(self, name: str) -> bool: return self.get_requirement(name) is not None From 12faaa94166cf4152a654dde142c86b71710f885 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 14:46:45 +0100 Subject: [PATCH 074/902] feat(cli): :sparkles: add command to list all available profiles --- rocrate_validator/cli.py | 67 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/cli.py b/rocrate_validator/cli.py index da1576da..10bc1feb 100644 --- a/rocrate_validator/cli.py +++ b/rocrate_validator/cli.py @@ -3,10 +3,13 @@ import rich_click as click from rich.console import Console +from rich.table import Table +from rich.text import Text +from rich.markdown import Markdown +from . import services from .colors import get_severity_color from .models import Severity, ValidationResult -from .services import validate as validate_rocrate # set up logging logger = logging.getLogger(__name__) @@ -37,6 +40,66 @@ def cli(ctx, debug: bool = False): ctx.invoke(validate) +@cli.group("profiles") +def profiles(): + """ + [magenta]rocrate-validator:[/magenta] Manage profiles + """ + pass + + +@profiles.command("list") +@click.option( + "-p", + "--profiles-path", + type=click.Path(exists=True), + default="./profiles", + show_default=True, + help="Path containing the profiles files", +) +@click.pass_context +def list_profiles(ctx, profiles_path: str = "./profiles"): + """ + List available profiles + """ + profiles = services.get_profiles(profiles_path=profiles_path) + # console.print("\nAvailable profiles:", style="white bold") + console.print("\n", style="white bold") + + table = Table(show_header=True, + title="Available profiles", + header_style="bold cyan", + border_style="bright_black", + show_footer=True, + caption="(*) Number of requirements by severity") + + # Define columns + table.add_column("Name", style="magenta bold") + table.add_column("Description", style="white italic") + table.add_column("Requirements (*)", style="white") + + # Add data to the table + for profile_name, profile in profiles.items(): + # Count requirements by severity + requirements = {} + logger.debug("Requirements: %s", requirements) + for req in profile.requirements: + if not requirements.get(req.type.name, None): + requirements[req.type.name] = 0 + requirements[req.type.name] += 1 + requirements = ", ".join( + [f"[bold][{get_severity_color(severity)}]{severity}: " + f"{count}[/{get_severity_color(severity)}][/bold]" + for severity, count in requirements.items() if count > 0]) + + # Add the row to the table + table.add_row(profile_name, Markdown(profile.description.strip()), requirements) + table.add_row() + + # Print the table + console.print(table) + + @cli.command("validate") @click.argument("rocrate-path", type=click.Path(exists=True), default=".") @click.option( @@ -125,7 +188,7 @@ def validate(ctx, try: # Validate the RO-Crate - result: ValidationResult = validate_rocrate( + result: ValidationResult = services.validate( profiles_path=profiles_path, profile_name=profile_name, requirement_level=requirement_level, From 799dca8ad3513f83214475404a6921a3c0f885f9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 15:08:36 +0100 Subject: [PATCH 075/902] style(cli): :lipstick: reformat list of failed checks --- rocrate_validator/cli.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/cli.py b/rocrate_validator/cli.py index 10bc1feb..c4e4a3b6 100644 --- a/rocrate_validator/cli.py +++ b/rocrate_validator/cli.py @@ -2,10 +2,10 @@ import os import rich_click as click +from rich.align import Align from rich.console import Console -from rich.table import Table -from rich.text import Text from rich.markdown import Markdown +from rich.table import Table from . import services from .colors import get_severity_color @@ -233,8 +233,11 @@ def __print_validation_result__( # TODO: Add color related to the requirement level associated with the check issue_color = get_severity_color(check.severity) console.print( - f" -> [bold][magenta]{check.name}[/magenta] check [red]failed[/red][/bold]" - f" (severity: [{issue_color}]{check.severity.name}[/{issue_color}])", + Align(f" [severity: [{issue_color}]{check.severity.name}[/{issue_color}], " + f"profile: [magenta]{check.requirement.profile.name }[/magenta]]", align="right") + ) + console.print( + f" -> [bold][magenta]{check.name}[/magenta] check [red]failed[/red][/bold]", style="white", ) console.print(f"{' '*4}{check.description}\n", style="white italic") From 0d5375d74c4ed3fe1fc7810e8163d5e9e9e00afa Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 15:38:16 +0100 Subject: [PATCH 076/902] refactor(cli): :art: reorganize cli into a package --- rocrate_validator/cli/__init__.py | 4 + rocrate_validator/cli/commands/__init__.py | 0 rocrate_validator/cli/commands/profiles.py | 71 ++++++++++++ .../{cli.py => cli/commands/validate.py} | 104 ++---------------- rocrate_validator/cli/main.py | 37 +++++++ 5 files changed, 120 insertions(+), 96 deletions(-) create mode 100644 rocrate_validator/cli/__init__.py create mode 100644 rocrate_validator/cli/commands/__init__.py create mode 100644 rocrate_validator/cli/commands/profiles.py rename rocrate_validator/{cli.py => cli/commands/validate.py} (64%) create mode 100644 rocrate_validator/cli/main.py diff --git a/rocrate_validator/cli/__init__.py b/rocrate_validator/cli/__init__.py new file mode 100644 index 00000000..6d98b3c4 --- /dev/null +++ b/rocrate_validator/cli/__init__.py @@ -0,0 +1,4 @@ +from .main import cli, click, console +from .commands import profiles, validate + +__all__ = ["cli", "click", "console", "profiles", "validate"] diff --git a/rocrate_validator/cli/commands/__init__.py b/rocrate_validator/cli/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py new file mode 100644 index 00000000..608495cf --- /dev/null +++ b/rocrate_validator/cli/commands/profiles.py @@ -0,0 +1,71 @@ +import logging + +from rich.markdown import Markdown +from rich.table import Table + +from ... import services +from ...colors import get_severity_color +from .. import cli, click, console + +# set up logging +logger = logging.getLogger(__name__) + + +@cli.group("profiles") +def profiles(): + """ + [magenta]rocrate-validator:[/magenta] Manage profiles + """ + pass + + +@profiles.command("list") +@click.option( + "-p", + "--profiles-path", + type=click.Path(exists=True), + default="./profiles", + show_default=True, + help="Path containing the profiles files", +) +@click.pass_context +def list_profiles(ctx, profiles_path: str = "./profiles"): + """ + List available profiles + """ + profiles = services.get_profiles(profiles_path=profiles_path) + # console.print("\nAvailable profiles:", style="white bold") + console.print("\n", style="white bold") + + table = Table(show_header=True, + title="Available profiles", + header_style="bold cyan", + border_style="bright_black", + show_footer=True, + caption="(*) Number of requirements by severity") + + # Define columns + table.add_column("Name", style="magenta bold") + table.add_column("Description", style="white italic") + table.add_column("Requirements (*)", style="white") + + # Add data to the table + for profile_name, profile in profiles.items(): + # Count requirements by severity + requirements = {} + logger.debug("Requirements: %s", requirements) + for req in profile.requirements: + if not requirements.get(req.type.name, None): + requirements[req.type.name] = 0 + requirements[req.type.name] += 1 + requirements = ", ".join( + [f"[bold][{get_severity_color(severity)}]{severity}: " + f"{count}[/{get_severity_color(severity)}][/bold]" + for severity, count in requirements.items() if count > 0]) + + # Add the row to the table + table.add_row(profile_name, Markdown(profile.description.strip()), requirements) + table.add_row() + + # Print the table + console.print(table) diff --git a/rocrate_validator/cli.py b/rocrate_validator/cli/commands/validate.py similarity index 64% rename from rocrate_validator/cli.py rename to rocrate_validator/cli/commands/validate.py index c4e4a3b6..c1e14b89 100644 --- a/rocrate_validator/cli.py +++ b/rocrate_validator/cli/commands/validate.py @@ -1,103 +1,19 @@ import logging import os -import rich_click as click from rich.align import Align -from rich.console import Console -from rich.markdown import Markdown -from rich.table import Table -from . import services -from .colors import get_severity_color -from .models import Severity, ValidationResult +from ... import services +from ...colors import get_severity_color +from ...models import Severity, ValidationResult +from .. import cli, click, console -# set up logging -logger = logging.getLogger(__name__) - - -# Create a Rich Console instance for enhanced output -console = Console() - - -@click.group(invoke_without_command=True) -@click.rich_config(help_config=click.RichHelpConfiguration(use_rich_markup=True)) -@click.option( - '--debug', - is_flag=True, - help="Enable debug logging", - default=False -) -@click.pass_context -def cli(ctx, debug: bool = False): - # Set the log level - if debug: - logging.basicConfig(level=logging.DEBUG) - else: - logging.basicConfig(level=logging.WARNING) - # If no subcommand is provided, invoke the default command - if ctx.invoked_subcommand is None: - # If no subcommand is provided, invoke the default command - ctx.invoke(validate) - - -@cli.group("profiles") -def profiles(): - """ - [magenta]rocrate-validator:[/magenta] Manage profiles - """ - pass +# from rich.markdown import Markdown +# from rich.table import Table -@profiles.command("list") -@click.option( - "-p", - "--profiles-path", - type=click.Path(exists=True), - default="./profiles", - show_default=True, - help="Path containing the profiles files", -) -@click.pass_context -def list_profiles(ctx, profiles_path: str = "./profiles"): - """ - List available profiles - """ - profiles = services.get_profiles(profiles_path=profiles_path) - # console.print("\nAvailable profiles:", style="white bold") - console.print("\n", style="white bold") - - table = Table(show_header=True, - title="Available profiles", - header_style="bold cyan", - border_style="bright_black", - show_footer=True, - caption="(*) Number of requirements by severity") - - # Define columns - table.add_column("Name", style="magenta bold") - table.add_column("Description", style="white italic") - table.add_column("Requirements (*)", style="white") - - # Add data to the table - for profile_name, profile in profiles.items(): - # Count requirements by severity - requirements = {} - logger.debug("Requirements: %s", requirements) - for req in profile.requirements: - if not requirements.get(req.type.name, None): - requirements[req.type.name] = 0 - requirements[req.type.name] += 1 - requirements = ", ".join( - [f"[bold][{get_severity_color(severity)}]{severity}: " - f"{count}[/{get_severity_color(severity)}][/bold]" - for severity, count in requirements.items() if count > 0]) - - # Add the row to the table - table.add_row(profile_name, Markdown(profile.description.strip()), requirements) - table.add_row() - - # Print the table - console.print(table) +# set up logging +logger = logging.getLogger(__name__) @cli.command("validate") @@ -248,7 +164,3 @@ def __print_validation_result__( f"[magenta]{issue.code}[/magenta]]: {issue.message}", style="white") console.print("\n", style="white") - - -if __name__ == "__main__": - cli() diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py new file mode 100644 index 00000000..d0312c96 --- /dev/null +++ b/rocrate_validator/cli/main.py @@ -0,0 +1,37 @@ +import logging + +import rich_click as click +from rich.console import Console + +# set up logging +logger = logging.getLogger(__name__) + + +# Create a Rich Console instance for enhanced output +console = Console() + + +@click.group(invoke_without_command=True) +@click.rich_config(help_config=click.RichHelpConfiguration(use_rich_markup=True)) +@click.option( + '--debug', + is_flag=True, + help="Enable debug logging", + default=False +) +@click.pass_context +def cli(ctx, debug: bool = False): + # Set the log level + if debug: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.WARNING) + # If no subcommand is provided, invoke the default command + if ctx.invoked_subcommand is None: + # If no subcommand is provided, invoke the default command + from .commands.validate import validate + ctx.invoke(validate) + + +if __name__ == "__main__": + cli() From 2f857872dc59b303622cce3ff91ec753c8f3d9a0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 16:01:01 +0100 Subject: [PATCH 077/902] refactor(core): :fire: remove obsolete code --- rocrate_validator/models.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 30bfbbbe..4f7deac7 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -271,15 +271,6 @@ def path(self) -> Path: def check_class(self) -> Type[Check]: return self._check_class - # def validate(self, rocrate_path: Path) -> CheckResult: - # assert self.check_class, "Check class not associated with requirement" - # # instantiate the check class - # check = self.check_class(self, rocrate_path) - # # run the check - # check.__do_check__() - # # return the result - # return check.result - def __eq__(self, other): return self.name == other.name \ and self.type == other.type and self.description == other.description \ From 3fcd3815a20be414f3cb8e1f65b1ea0d164c9dd2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 16:07:16 +0100 Subject: [PATCH 078/902] feat(core): :sparkles: integrate severity-color mapping on model classes --- rocrate_validator/models.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 4f7deac7..5ce53f45 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -255,6 +255,11 @@ def name(self) -> str: def type(self) -> RequirementType: return self._type + @property + def color(self) -> str: + from .colors import get_severity_color + return get_severity_color(self.type) + @property def profile(self) -> Profile: return self._profile @@ -426,6 +431,10 @@ def requirement(self) -> Requirement: def severity(self) -> Severity: return self._requirement.type + @property + def color(self) -> str: + return self.requirement.color + @property def name(self) -> str: if not self._name: From 4e817389fa08844990f8b2655de85caf1333fc4a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 16:59:52 +0100 Subject: [PATCH 079/902] feat(core): :sparkles: expose check description at requirement level --- rocrate_validator/models.py | 18 ++++++++++++++- .../requirements/shacl/checks.py | 22 ++++++++++++++----- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 5ce53f45..fb45fd50 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -266,6 +266,12 @@ def profile(self) -> Profile: @property def description(self) -> str: + if not self._description: + docs = None + if self._check_class: + docs = self._check_class.get_description(self) + self._description = docs.strip() if docs else f"Profile Requirement {self.name}" + return self._description @property @@ -444,9 +450,13 @@ def name(self) -> str: @property def description(self) -> str: if not self._description: - return self.__doc__.strip() + return self.get_description() return self._description + @classmethod + def get_description(cls, requirement) -> str: + return cls.__doc__.strip() + @property def ro_crate_path(self) -> Path: return self._validator.rocrate_path @@ -716,6 +726,12 @@ def publicID(self) -> str: return f"{self.rocrate_path}/" return self.rocrate_path + @classmethod + def load_graph_of_shapes(cls, requirement: Requirement, publicID: str = None) -> Graph: + shapes_graph = Graph() + shapes_graph.parse(str(requirement.path), format="ttl", publicID=publicID) + return shapes_graph + def load_graphs_of_shapes(self): # load the graph of shapes shapes_graphs = {} diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index bd186e0e..9d022205 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -43,9 +43,14 @@ def name(self): def description(self): if not self._description and self.shapes_graph is None: return "SHACL Check" - + # If the description is not set, query the shapes graph if not self._description: - # Query to get the description of the shape + self._description = self.query_description(self.shapes_graph) + return self._description + + @staticmethod + def query_description(shapes_graph) -> str: + try: query = """ SELECT ?description WHERE { @@ -54,11 +59,18 @@ def description(self): } """ # Execute the query - results = [_ for _ in self.shapes_graph.query(query, initNs={"sh": SHACL_NS})] + results = [_ for _ in shapes_graph.query(query, initNs={"sh": SHACL_NS})] if results: - self._description = results[0][0] + return results[0][0] + except Exception as e: + logger.debug("Error getting description: %s", e) + return None - return self._description + @classmethod + def get_description(cls, requirement: Requirement): + from ...models import Validator + graph_of_shapes = Validator.load_graph_of_shapes(requirement) + return cls.query_description(graph_of_shapes) @property def shapes_graph(self): From 08b8d4c6892a35920f0cc0eb09429c5ff791c6fc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 17:00:36 +0100 Subject: [PATCH 080/902] feat(services): :sparkles: add service endpoint to retrieve a profile --- rocrate_validator/services.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 5f867384..acdb107f 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -49,7 +49,18 @@ def validate( def get_profiles(profiles_path: str = "./profiles") -> Dict[str, Profile]: - + """ + Load the profiles from the given path + """ profiles = Profile.load_profiles(profiles_path) logger.debug("Profiles loaded: %s", profiles) return profiles + + +def get_profile(profiles_path: str = "./profiles", profile_name: str = "ro-crate") -> Profile: + """ + Load the profiles from the given path + """ + profile = Profile.load(f"{profiles_path}/{profile_name}") + logger.debug("Profile loaded: %s", profile) + return profile From 62f11f04fb39d4c5f2aa1c5c3014b47aeaac7bec Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 17:01:19 +0100 Subject: [PATCH 081/902] feat(cli): :sparkles: add CLI command to describe a profile --- rocrate_validator/cli/commands/profiles.py | 62 ++++++++++++++++++---- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 608495cf..dce28d3f 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -12,23 +12,24 @@ @cli.group("profiles") -def profiles(): - """ - [magenta]rocrate-validator:[/magenta] Manage profiles - """ - pass - - -@profiles.command("list") @click.option( "-p", "--profiles-path", type=click.Path(exists=True), default="./profiles", show_default=True, - help="Path containing the profiles files", + help="Path containing the profiles files" ) @click.pass_context +def profiles(ctx, profiles_path: str = "./profiles"): + """ + [magenta]rocrate-validator:[/magenta] Manage profiles + """ + pass + + +@profiles.command("list") +@click.pass_context def list_profiles(ctx, profiles_path: str = "./profiles"): """ List available profiles @@ -69,3 +70,46 @@ def list_profiles(ctx, profiles_path: str = "./profiles"): # Print the table console.print(table) + + +@profiles.command("describe") +@click.argument("profile-name", type=click.STRING, default="ro-crate", required=True) +@click.pass_context +def describe_profile(ctx, + profile_name: str = "ro-crate", + profiles_path: str = "./profiles"): + """ + Show a profile + """ + # Get the profile + profile = services.get_profile(profiles_path=profiles_path, profile_name=profile_name) + + console.print("\n", style="white bold") + console.print(f"[bold]Profile: {profile_name}[/bold]", style="magenta bold") + console.print("\n", style="white bold") + console.print(Markdown(profile.description.strip())) + console.print("\n", style="white bold") + + table_rows = [] + levels_list = set() + for requirement in profile.requirements: + level_info = f"[{requirement.color}]{requirement.type.name}[/{requirement.color}]" + levels_list.add(level_info) + table_rows.append((requirement.name, Markdown(requirement.description.strip()), level_info)) + + table = Table(show_header=True, + title="Profile Requirements Checks", + header_style="bold cyan", + border_style="bright_black", + show_footer=False, + caption=f"(*) Requirement level: {', '.join(levels_list)}") + + # Define columns + table.add_column("Name", style="magenta bold", justify="right") + table.add_column("Description", style="white italic") + table.add_column("Requirement Level (*)", style="white", justify="center") + # Add data to the table + for row in table_rows: + table.add_row(*row) + # Print the table + console.print(table) From 3d57e8901c1ff9bc563a9997b6d0259a3a058a50 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 17:10:24 +0100 Subject: [PATCH 082/902] chore(cli): :lipstick: change layout of list profiles table --- rocrate_validator/cli/commands/profiles.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index dce28d3f..41acc48a 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -42,13 +42,13 @@ def list_profiles(ctx, profiles_path: str = "./profiles"): title="Available profiles", header_style="bold cyan", border_style="bright_black", - show_footer=True, + show_footer=False, caption="(*) Number of requirements by severity") # Define columns - table.add_column("Name", style="magenta bold") + table.add_column("Name", style="magenta bold", justify="right") table.add_column("Description", style="white italic") - table.add_column("Requirements (*)", style="white") + table.add_column("Requirements (*)", style="white", justify="center") # Add data to the table for profile_name, profile in profiles.items(): From 9bc4ec7c2572ae33e393bc1249fd02c9ede89009 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 17:22:21 +0100 Subject: [PATCH 083/902] fix(core): :bug: missing parameter --- rocrate_validator/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index fb45fd50..ec85eee6 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -450,11 +450,11 @@ def name(self) -> str: @property def description(self) -> str: if not self._description: - return self.get_description() + return self.get_description(self.requirement) return self._description @classmethod - def get_description(cls, requirement) -> str: + def get_description(cls, requirement: Requirement) -> str: return cls.__doc__.strip() @property From 8bf5525af7293d0937f2449689694f5133c65ca9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 17:47:07 +0100 Subject: [PATCH 084/902] fix(cli): :lock: check if the profile path exists --- rocrate_validator/services.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index acdb107f..24c8ca34 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -61,6 +61,9 @@ def get_profile(profiles_path: str = "./profiles", profile_name: str = "ro-crate """ Load the profiles from the given path """ + profile_path = f"{profiles_path}/{profile_name}" + if not Path(profiles_path).exists(): + raise FileNotFoundError(f"Profile not found: {profile_path}") profile = Profile.load(f"{profiles_path}/{profile_name}") logger.debug("Profile loaded: %s", profile) return profile From de22b7ae60db214209bd2e309142f9096c7e5a0a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 18:06:24 +0100 Subject: [PATCH 085/902] feat(core): :sparkles: enable comparison between profiles --- rocrate_validator/models.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index ec85eee6..8bd89b68 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -182,6 +182,15 @@ def __eq__(self, other) -> bool: def __ne__(self, other) -> bool: return self.name != other.name or self.path != other.path or self.requirements != other.requirements + def __lt__(self, other) -> bool: + return self.name < other.name + + def __le__(self, other) -> bool: + return self.name <= other.name + + def __gt__(self, other) -> bool: + return self.name > other.name + def __hash__(self) -> int: return hash((self.name, self.path, self.requirements)) From ad69349dc74b3d2b916a80e8f2042e0940d66107 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 18:50:42 +0100 Subject: [PATCH 086/902] feat(core): :sparkles: extend profile with inherited profiles --- rocrate_validator/models.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 8bd89b68..4eedcf99 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -161,6 +161,15 @@ def requirements_by_severity_map(self) -> Dict[RequirementType, List[Requirement return {severity: self.get_requirements_by_type(severity) for severity in RequirementLevels.all().values()} + @property + def inherited_profiles(self) -> List[Profile]: + profiles = [ + _ for _ in sorted( + Profile.load_profiles(self.path.parent).values(), key=lambda x: x, reverse=True) + if _ < self] + logger.debug("Inherited profiles: %s", profiles) + return profiles + def has_requirement(self, name: str) -> bool: return self.get_requirement(name) is not None From 5762171f2147bc33dbd73f3aab770e652d1e6d98 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 18:51:43 +0100 Subject: [PATCH 087/902] feat(core): :sparkles: extend validation process to include inherited profiles --- rocrate_validator/models.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 4eedcf99..0045a32c 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -812,18 +812,26 @@ def validate(self) -> ValidationResult: validation_result = ValidationResult( rocrate_path=self.rocrate_path, validation_settings=self.validation_settings) - # perform the requirements validation - for requirement in self.profile.requirements: - logger.debug("Validating Requirement: %s", requirement) - result = self.validate_requirement(requirement) - logger.debug("Issues: %r", result.get_issues()) - if result and result.passed(): - logger.debug("Validation Requirement passed: %s", requirement) - else: - logger.debug(f"Validation Requirement {requirement} failed ") - validation_result.add_issues(result.get_issues()) - if self.validation_settings.get("abort_on_first"): - logger.debug("Aborting on first failure") - return validation_result + # list of profiles to validate + profiles = [self.profile] + logger.debug("Disable profile inheritance: %s", self.disable_profile_inheritance) + if not self.disable_profile_inheritance: + profiles.extend(self.profile.inherited_profiles) + logger.debug("Profiles to validate: %s", profiles) + + for profile in profiles: + # perform the requirements validation + for requirement in profile.requirements: + logger.debug("Validating Requirement: %s", requirement) + result = self.validate_requirement(requirement) + logger.debug("Issues: %r", result.get_issues()) + if result and result.passed(): + logger.debug("Validation Requirement passed: %s", requirement) + else: + logger.debug(f"Validation Requirement {requirement} failed ") + validation_result.add_issues(result.get_issues()) + if self.validation_settings.get("abort_on_first"): + logger.debug("Aborting on first failure") + return validation_result return validation_result From 0edb562dbdc0a29d0a266ec481b705a8798a49d3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 18:55:05 +0100 Subject: [PATCH 088/902] fix(cli): :goal_net: handle and report generic errors --- rocrate_validator/cli/commands/profiles.py | 68 ++++++++++++---------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 41acc48a..fbab4186 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -82,34 +82,40 @@ def describe_profile(ctx, Show a profile """ # Get the profile - profile = services.get_profile(profiles_path=profiles_path, profile_name=profile_name) - - console.print("\n", style="white bold") - console.print(f"[bold]Profile: {profile_name}[/bold]", style="magenta bold") - console.print("\n", style="white bold") - console.print(Markdown(profile.description.strip())) - console.print("\n", style="white bold") - - table_rows = [] - levels_list = set() - for requirement in profile.requirements: - level_info = f"[{requirement.color}]{requirement.type.name}[/{requirement.color}]" - levels_list.add(level_info) - table_rows.append((requirement.name, Markdown(requirement.description.strip()), level_info)) - - table = Table(show_header=True, - title="Profile Requirements Checks", - header_style="bold cyan", - border_style="bright_black", - show_footer=False, - caption=f"(*) Requirement level: {', '.join(levels_list)}") - - # Define columns - table.add_column("Name", style="magenta bold", justify="right") - table.add_column("Description", style="white italic") - table.add_column("Requirement Level (*)", style="white", justify="center") - # Add data to the table - for row in table_rows: - table.add_row(*row) - # Print the table - console.print(table) + try: + profile = services.get_profile(profiles_path=profiles_path, profile_name=profile_name) + + console.print("\n", style="white bold") + console.print(f"[bold]Profile: {profile_name}[/bold]", style="magenta bold") + console.print("\n", style="white bold") + console.print(Markdown(profile.description.strip())) + console.print("\n", style="white bold") + + table_rows = [] + levels_list = set() + for requirement in profile.requirements: + level_info = f"[{requirement.color}]{requirement.type.name}[/{requirement.color}]" + levels_list.add(level_info) + table_rows.append((requirement.name, Markdown(requirement.description.strip()), level_info)) + + table = Table(show_header=True, + title="Profile Requirements Checks", + header_style="bold cyan", + border_style="bright_black", + show_footer=False, + caption=f"(*) Requirement level: {', '.join(levels_list)}") + + # Define columns + table.add_column("Name", style="magenta bold", justify="right") + table.add_column("Description", style="white italic") + table.add_column("Requirement Level (*)", style="white", justify="center") + # Add data to the table + for row in table_rows: + table.add_row(*row) + # Print the table + console.print(table) + except Exception as e: + console.print(f"\n[red]ERROR:[/red] {e}\n", style="white") + if logger.isEnabledFor(logging.DEBUG): + console.print_exception(show_locals=True) + ctx.exit(1) From cd230c8218d9fec99b833d3a23a39942c8505990 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 18:55:42 +0100 Subject: [PATCH 089/902] fix(services): :art: missing import --- rocrate_validator/cli/main.py | 9 ++++++++- rocrate_validator/services.py | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index d0312c96..7b0a3b46 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -34,4 +34,11 @@ def cli(ctx, debug: bool = False): if __name__ == "__main__": - cli() + try: + cli() + except Exception as e: + console.print(f"\n\n[bold]\[[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", style="white") + if logger.isEnabledFor(logging.DEBUG): + console.print_exception() + else: + exit(1) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 24c8ca34..a818cd5c 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -1,4 +1,5 @@ import logging +from pathlib import Path from typing import Dict, Literal, Optional, Union from pyshacl.pytypes import GraphLike From 6b7bc44f03539d57041b270317bae30688bb0e7d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 18:56:55 +0100 Subject: [PATCH 090/902] feat(core): :sparkles: improve comparison between objects --- rocrate_validator/models.py | 49 +++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 0045a32c..a9bb2c24 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -314,6 +314,26 @@ def __ne__(self, other): def __hash__(self): return hash((self.name, self.type, self.description, self.path)) + def __lt__(self, other) -> bool: + if not isinstance(other, Requirement): + raise ValueError(f"Cannot compare Requirement with {type(other)}") + return self.type < other.type or self.name < other.name + + def __le__(self, other) -> bool: + if not isinstance(other, Requirement): + raise ValueError(f"Cannot compare Requirement with {type(other)}") + return self.type <= other.type or self.name <= other.name + + def __gt__(self, other) -> bool: + if not isinstance(other, Requirement): + raise ValueError(f"Cannot compare Requirement with {type(other)}") + return self.type > other.type or self.name > other.name + + def __ge__(self, other) -> bool: + if not isinstance(other, Requirement): + raise ValueError(f"Cannot compare Requirement with {type(other)}") + return self.type >= other.type or self.name >= other.name + def __repr__(self): return ( f'ProfileRequirement(' @@ -530,10 +550,35 @@ def __repr__(self) -> str: def __eq__(self, other: object) -> bool: if not isinstance(other, Check): return False - return self.name == other.name + return self.name == other.name and self.requirement == other.requirement def __hash__(self) -> int: - return hash(self.name) + return hash((self.name, self.requirement)) + + def __lt__(self, other: object) -> bool: + if not isinstance(other, Check): + raise ValueError(f"Cannot compare Check with {type(other)}") + return self.requirement < other.requirement or self.name < other.name + + def __le__(self, other: object) -> bool: + if not isinstance(other, Check): + raise ValueError(f"Cannot compare Check with {type(other)}") + return self.requirement <= other.requirement or self.name <= other.name + + def __gt__(self, other: object) -> bool: + if not isinstance(other, Check): + raise ValueError(f"Cannot compare Check with {type(other)}") + return self.requirement > other.requirement or self.name > other.name + + def __ge__(self, other: object) -> bool: + if not isinstance(other, Check): + raise ValueError(f"Cannot compare Check with {type(other)}") + return self.requirement >= other.requirement or self.name >= other.name + + def __ne__(self, other: object) -> bool: + if not isinstance(other, Check): + return True + return self.name != other.name or self.requirement != other.requirement class CheckIssue: From b8aca47b6344d8146060a9eb9a935d271acef3d1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 18:58:25 +0100 Subject: [PATCH 091/902] refactor(core): :fire: remove obsolete files --- rocrate_validator/checks/__init__.py | 269 --------------------------- 1 file changed, 269 deletions(-) delete mode 100644 rocrate_validator/checks/__init__.py diff --git a/rocrate_validator/checks/__init__.py b/rocrate_validator/checks/__init__.py deleted file mode 100644 index 1244020c..00000000 --- a/rocrate_validator/checks/__init__.py +++ /dev/null @@ -1,269 +0,0 @@ -from __future__ import annotations - -import inspect -import logging -import os -from abc import ABC, abstractmethod -from importlib import import_module -from pathlib import Path -from typing import List, Optional, Type - -from ..profiles import RequirementLevels, RequirementType - -from ..utils import get_config, get_file_descriptor_path - -# set up logging -logger = logging.getLogger(__name__) - -# current directory -__CURRENT_DIR__ = os.path.dirname(os.path.realpath(__file__)) - - -def issue_types(issues: List[Type[CheckIssue]]) -> Type[Check]: - def class_decorator(cls): - cls.issue_types = issues - return cls - return class_decorator - - -class Severity(RequirementLevels): - """Extends the RequirementLevels enum with additional values""" - INFO = RequirementType('INFO', 0) - WARNING = RequirementType('WARNING', 2) - ERROR = RequirementType('ERROR', 4) - - -class CheckIssue: - """ - Class to store an issue found during a check - - Attributes: - severity (IssueSeverity): The severity of the issue - message (str): The message - code (int): The code - """ - - def __init__(self, severity: Severity, - message: Optional[str] = None, - code: int = None, - check: Check = None): - self._severity = severity - self._message = message - self._code = code - self._check = check - - @property - def message(self) -> str: - """The message associated with the issue""" - return self._message - - @property - def severity(self) -> str: - """The severity of the issue""" - return self._severity - - @property - def check(self) -> Check: - """The check that generated the issue""" - return self._check - - @property - def code(self) -> int: - # If the code has not been set, calculate it - if not self._code: - """ - Calculate the code based on the severity, the class name and the message. - - All issues with the same severity, class name and message will have the same code. - - All issues with the same severity and class name but different message will have different codes. - - All issues with the same severity but different class name and message will have different codes. - - All issues with the same severity should start with the same number. - - All codes should be positive numbers. - """ - # Concatenate the severity, class name and message into a single string - issue_string = str(self._severity.value) + self.__class__.__name__ + str(self._message) - - # Use the built-in hash function to generate a unique code for this string - # The modulo operation ensures that the code is a positive number - self._code = hash(issue_string) % ((1 << 31) - 1) - # Return the code - return self._code - - -class Check(ABC): - """ - Base class for checks - """ - - def __init__(self, ro_crate_path: Path) -> None: - self._ro_crate_path = ro_crate_path - # create a result object for the check - self._result: CheckResult = CheckResult(self) - - @property - def name(self) -> str: - return self.__class__.__name__.replace("Check", "") - - @property - def description(self) -> str: - return self.__doc__.strip() - - @property - def ro_crate_path(self) -> Path: - return self._ro_crate_path - - @property - def file_descriptor_path(self) -> Path: - return get_file_descriptor_path(self.ro_crate_path) - - @property - def result(self) -> CheckResult: - return self._result - - def __do_check__(self) -> bool: - """ - Internal method to perform the check - """ - # Check if the check has issue types defined - assert self.issue_types, "Check must have issue types defined in the decorator" - # Perform the check - try: - return self.check() - except Exception as e: - self.check.result.add_error(str(e)) - logger.error("Unexpected error during check: %s", e) - if logger.isEnabledFor(logging.DEBUG): - logger.exception(e) - return False - - @abstractmethod - def check(self) -> bool: - raise NotImplementedError("Check not implemented") - - def passed(self) -> bool: - return self.result.passed() - - def get_issues(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: - return self.result.get_issues(severity) - - def get_issues_by_severity(self, severity: Severity) -> List[CheckIssue]: - return self.result.get_issues_by_severity(severity) - - def __str__(self) -> str: - return self.name - - def __repr__(self) -> str: - return f"{self.name}Check()" - - def __eq__(self, other: object) -> bool: - if not isinstance(other, Check): - return False - return self.name == other.name - - def __hash__(self) -> int: - return hash(self.name) - - -class CheckResult: - """ - Class to store the result of a check - - Attributes: - check (Check): The check that was performed - code (int): The result code - message (str): The message - """ - - def __init__(self, check: Check): - self.check = check - self._issues: List[CheckIssue] = [] - - @property - def issues(self) -> List[CheckIssue]: - return self._issues - - def add_issue(self, issue: CheckIssue): - issue._check = self.check - self._issues.append(issue) - - def add_error(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.ERROR, message, code, self.check)) - - def add_warning(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.WARNING, message, code, self.check)) - - def add_info(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.INFO, message, code, self.check)) - - def add_optional(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.OPTIONAL, message, code, self.check)) - - def add_may(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.MAY, message, code, self.check)) - - def add_should(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.SHOULD, message, code, self.check)) - - def add_should_not(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.SHOULD_NOT, message, code, self.check)) - - def add_must(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.MUST, message, code, self.check)) - - def add_must_not(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.MUST_NOT, message, code, self.check)) - - def get_issues(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: - return [issue for issue in self.issues if issue.severity.value >= severity.value] - - def get_issues_by_severity(self, severity: Severity) -> List[CheckIssue]: - return [issue for issue in self.issues if issue.severity == severity] - - def passed(self, severity: Severity = Severity.WARNING) -> bool: - return not any(issue.severity.value >= severity.value for issue in self.issues) - - -def get_checks(directory: str = __CURRENT_DIR__, - rocrate_path: Path = ".", - instances: bool = True, - skip_dirs: List[str] = None) -> List[Type[Check]]: - """ - Load all the classes from the directory - """ - logger.debug("Loading checks from %s", directory) - # create an empty list to store the classes - classes = {} - # skip directories that start with a dot - skip_dirs = skip_dirs or [] - skip_dirs.extend(get_config(property="skip_dirs")) - - # loop through the files in the directory - for root, dirs, files in os.walk(directory): - # skip directories that start with a dot - dirs[:] = [d for d in dirs if not d.startswith('.')] - # loop through the files - for file in files: - # check if the file is a python file - logger.debug("Checking file %s %s %s", root, dirs, file) - if file.endswith(".py") and not file.startswith("__init__"): - # get the file path - file_path = os.path.join(root, file) - # FIXME: works only on the main "general" general directory - m = '{}.{}'.format( - 'rocrate_validator.checks', os.path.basename(file_path)[:-3]) - logger.debug("Module: %r" % m) - # import the module - mod = import_module(m) - # loop through the objects in the module - # and store the classes - for _, obj in inspect.getmembers(mod): - logger.debug("Checking object %s", obj) - if inspect.isclass(obj) \ - and inspect.getmodule(obj) == mod \ - and issubclass(obj, Check) \ - and obj.__name__.endswith('Check'): - classes[obj.__name__] = obj - logger.debug("Loaded class %s", obj.__name__) - return [v(rocrate_path) if instances else v for v in classes.values()] - - # return the list of classes - return classes From 8e0485287abc4108bf632bdfff1676afb9dbb92a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 19:09:39 +0100 Subject: [PATCH 092/902] fix(core): :pencil2: typo --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index a9bb2c24..867bbe4a 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -67,7 +67,7 @@ def __repr__(self): class RequirementLevels: """ - * The key words MUST, MUST NOT, REQUIRED, + * The keywords MUST, MUST NOT, REQUIRED, * SHALL, SHALL NOT, SHOULD, SHOULD NOT, * RECOMMENDED, MAY, and OPTIONAL in this document * are to be interpreted as described in RFC 2119. From 9426b6d9fb3d5d1973e074ebb4e8742f42c3cae0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 22:44:59 +0100 Subject: [PATCH 093/902] feat(core): :sparkles: allow to filter profile requirments by severity --- rocrate_validator/models.py | 28 +++++++++++++++++++++++++--- rocrate_validator/services.py | 10 ++++++++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 867bbe4a..8bd5030d 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -64,6 +64,15 @@ def __hash__(self): def __repr__(self): return f'RequirementType(name={self.name}, severity={self.value})' + def __str__(self): + return self.name + + def __int__(self): + return self.value + + def __index__(self): + return self.value + class RequirementLevels: """ @@ -156,6 +165,13 @@ def requirements(self) -> List[Requirement]: self.load_requirements() return self._requirements + def get_requirements( + self, severity: RequirementType = RequirementLevels.MUST, + exact_match: bool = False) -> List[Requirement]: + return [requirement for requirement in self.requirements + if not exact_match and requirement.type >= severity or + exact_match and requirement.type == severity] + @property def requirements_by_severity_map(self) -> Dict[RequirementType, List[Requirement]]: return {severity: self.get_requirements_by_type(severity) @@ -686,7 +702,7 @@ def __init__(self, profiles_path: str = "./profiles", profile_name: str = "ro-crate", disable_profile_inheritance: bool = False, - requirement_level="MUST", + requirement_level: Union[str, RequirementType] = RequirementLevels.MUST, requirement_level_only: bool = False, ontologies_path: Optional[Path] = None, advanced: Optional[bool] = False, @@ -702,9 +718,13 @@ def __init__(self, self.profiles_path = profiles_path self.profile_name = profile_name self.disable_profile_inheritance = disable_profile_inheritance - self.requirement_level = requirement_level + self.requirement_level = \ + RequirementLevels.get(requirement_level) if isinstance(requirement_level, str) else \ + requirement_level self.requirement_level_only = requirement_level_only self.ontologies_path = ontologies_path + self.requirement_level = requirement_level + self.requirement_level_only = requirement_level_only self._validation_settings = { 'advanced': advanced, @@ -866,7 +886,9 @@ def validate(self) -> ValidationResult: for profile in profiles: # perform the requirements validation - for requirement in profile.requirements: + requirements = profile.get_requirements( + self.requirement_level, exact_match=self.requirement_level_only) + for requirement in requirements: logger.debug("Validating Requirement: %s", requirement) result = self.validate_requirement(requirement) logger.debug("Issues: %r", result.get_issues()) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index a818cd5c..e4154b0d 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -4,7 +4,7 @@ from pyshacl.pytypes import GraphLike -from .models import Profile, ValidationResult, Validator +from .models import Profile, RequirementLevels, RequirementType, ValidationResult, Validator # set up logging logger = logging.getLogger(__name__) @@ -22,11 +22,15 @@ def validate( abort_on_first: Optional[bool] = False, allow_infos: Optional[bool] = False, allow_warnings: Optional[bool] = False, + requirement_level: Union[str, RequirementType] = RequirementLevels.MUST, + requirement_level_only: bool = False, serialization_output_path: str = None, serialization_output_format: str = "turtle", **kwargs, ) -> ValidationResult: - + """ + Validate a RO-Crate against a profile + """ validator = Validator( rocrate_path=rocrate_path, profiles_path=profiles_path, @@ -39,6 +43,8 @@ def validate( abort_on_first=abort_on_first, allow_infos=allow_infos, allow_warnings=allow_warnings, + requirement_level=RequirementLevels.get(requirement_level), + requirement_level_only=requirement_level_only, serialization_output_path=serialization_output_path, serialization_output_format=serialization_output_format, **kwargs, From 82d9cce59061c9b01bb953aa1aeda563ae0e8fc0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 22:48:13 +0100 Subject: [PATCH 094/902] build(core): :package: add rich-click --- poetry.lock | 32 +++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 45420c69..f8f9a023 100644 --- a/poetry.lock +++ b/poetry.lock @@ -526,6 +526,25 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "rich-click" +version = "1.7.3" +description = "Format click help output nicely with rich" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rich-click-1.7.3.tar.gz", hash = "sha256:bced1594c497dc007ab49508ff198bb437c576d01291c13a61658999066481f4"}, + {file = "rich_click-1.7.3-py3-none-any.whl", hash = "sha256:bc4163d4e2a3361e21c4d72d300eca6eb8896dfc978667923cb1d4937b8769a3"}, +] + +[package.dependencies] +click = ">=7" +rich = ">=10.7.0" +typing-extensions = "*" + +[package.extras] +dev = ["flake8", "flake8-docstrings", "mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "types-setuptools"] + [[package]] name = "six" version = "1.16.0" @@ -559,6 +578,17 @@ files = [ {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, ] +[[package]] +name = "typing-extensions" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, +] + [[package]] name = "wcwidth" version = "0.2.13" @@ -599,4 +629,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "fe7ca64347512f6550dfacaac30f0c9faae0eb20427b72f5a60f6a5f3603921b" +content-hash = "3f2998c6f66b25aa83bcb4f744dd1741c24752998a00871ff44008fca5f20d58" diff --git a/pyproject.toml b/pyproject.toml index a088b909..36955076 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ pyshacl = "^0.25.0" click = "^8.1.7" rich = "^13.7.1" toml = "^0.10.2" +rich-click = "^1.7.3" [tool.poetry.group.dev.dependencies] pyproject-flake8 = "^6.1.0" From ca3bceba393e1e8f470ac679677fe93f0ee93fe5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 23:06:21 +0100 Subject: [PATCH 095/902] fix(core): :bug: preserve order of checks --- rocrate_validator/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 8bd5030d..7111eca1 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -666,9 +666,9 @@ def get_rocrate_path(self): def get_validation_settings(self): return self._validation_settings - def get_failed_checks(self) -> Set[Check]: - # return the set of checks that failed - return set([issue.check for issue in self._issues]) + def get_failed_checks(self) -> List[Check]: + # return the list of checks that failed + return [issue.check for issue in self._issues] def add_issue(self, issue: CheckIssue): self._issues.append(issue) From 048c2412b45b24ac9a03ed8b09f695f0129457d7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 11 Mar 2024 23:09:12 +0100 Subject: [PATCH 096/902] chore(core): :mute: remove log --- rocrate_validator/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 7111eca1..5dd18a01 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -539,7 +539,6 @@ def __do_check__(self) -> bool: return self.check() except Exception as e: self.result.add_error(str(e)) - logger.error("Unexpected error during check: %s", e) if logger.isEnabledFor(logging.DEBUG): logger.exception(e) return False From bcfe914a6da2e3c9b4a83570006bbcc21973a41c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 12:49:11 +0100 Subject: [PATCH 097/902] refactor(core): :building_construction: reorganize the validation process around a context --- rocrate_validator/errors.py | 13 + rocrate_validator/models.py | 493 ++++++++++-------- .../requirements/shacl/validator.py | 12 +- 3 files changed, 286 insertions(+), 232 deletions(-) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index f0d66d7c..85d93c81 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -1,3 +1,16 @@ +class OutOfValidationContext(Exception): + def __init__(self, message: str = None): + self._message = message + + @property + def message(self) -> str: + return self._message + + def __str__(self): + return self._message + + def __repr__(self): + return f"OutOfValidationContext({self._message!r})" class InvalidSerializationFormat(Exception): diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 5dd18a01..d174d79f 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -6,7 +6,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from pathlib import Path -from typing import Dict, List, Optional, Set, Type, Union +from typing import Callable, Dict, List, Optional, Set, Type, Union from rdflib import Graph @@ -17,9 +17,10 @@ ROCRATE_METADATA_FILE, VALID_INFERENCE_OPTIONS_TYPES) from rocrate_validator.utils import (get_classes_from_file, - get_file_descriptor_path, get_requirement_name_from_file) +from .errors import OutOfValidationContext + logger = logging.getLogger(__name__) @@ -102,6 +103,13 @@ def get(name: str) -> RequirementType: return RequirementLevels.all()[name.upper()] +class Severity(RequirementLevels): + """Extends the RequirementLevels enum with additional values""" + INFO = RequirementType('INFO', 0) + WARNING = RequirementType('WARNING', 2) + ERROR = RequirementType('ERROR', 4) + + class Profile: def __init__(self, name: str, path: Path = None, requirements: Set[Requirement] = None): @@ -260,10 +268,101 @@ def load_profiles(profiles_path: Union[str, Path]) -> Dict[str, Profile]: return profiles +def check(name=None): + def decorator(func): + func.check = True + func.name = name if name else func.__name__ + return func + return decorator + + +class RequirementCheck(ABC): + + def __init__(self, + requirement: Requirement, + name: str, + check: Callable, + description: str = None): + self._requirement: Requirement = requirement + self._name = name + self._description = description + self._check = check + # declare the reference to the validation context + self._validation_context: ValidationContext = None + # declare the reference to the validator + self._validator: Validator = None + # declare the result of the check + self._result: ValidationResult = None + + @property + def name(self) -> str: + if not self._name: + return self.__class__.__name__.replace("Check", "") + return self._name + + @property + def description(self) -> str: + if not self._description: + return self.__class__.__doc__.strip() if self.__class__.__doc__ else f"Check {self.name}" + return self._description + + @property + def requirement(self) -> Requirement: + return self._requirement + + @property + def validation_context(self) -> ValidationContext: + assert self._validation_context, "Validation context not set before the check" + return self._validation_context + + @property + def validator(self) -> Validator: + assert self._validator, "Validator not set before the check" + return self._validator + + @property + def result(self) -> ValidationResult: + assert self._result, "Result not set before the check" + return self._result + + @property + def ro_crate_path(self) -> Path: + assert self.validator, "ro-crate path not set before the check" + return self.validator.rocrate_path + + @property + def issues(self) -> List[CheckIssue]: + """Return the issues found during the check""" + assert self._result, "Issues not set before the check" + return self._result.get_issues_by_check(self, Severity.INFO) + + def get_issues(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: + return self._result.get_issues_by_check(self, severity) + + def get_issues_by_severity(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: + return self._result.get_issues_by_check_and_severity(self, severity) + + @abstractmethod + def check(self) -> bool: + raise NotImplementedError("Check not implemented") + + def __do_check__(self, context: ValidationContext) -> bool: + """ + Internal method to perform the check + """ + # Set the validation context + self._validation_context = context + # Set the validator + self._validator = context.validator + # Set the result + self._result = context.result + # Perform the check + return self.check() + + class Requirement: def __init__(self, - check_class: Type[Check], type: RequirementType, profile: Profile, name: str = None, @@ -274,7 +373,10 @@ def __init__(self, self._profile = profile self._description = description self._path = path - self._check_class = check_class + self._checks: List[RequirementCheck] = [] + + # reference to the current validation context + self._validation_context: ValidationContext = None if not self._name and self._path: self._name = get_requirement_name_from_file(self._path) @@ -301,20 +403,79 @@ def profile(self) -> Profile: @property def description(self) -> str: if not self._description: - docs = None - if self._check_class: - docs = self._check_class.get_description(self) + # set docs equal to docstring + docs = self.__class__.__doc__ self._description = docs.strip() if docs else f"Profile Requirement {self.name}" - return self._description @property def path(self) -> Path: return self._path + # write a method to collect the list of decorated check methods + def __init_checks__(self): + checks = [] + for name, member in inspect.getmembers(self._check_class, inspect.isfunction): + if hasattr(member, "check"): + check_name = member.name if hasattr(member, "name") else name + self._checks.append(RequirementCheck(self, check_name, member, member.__doc__)) + return checks + + def get_checks(self) -> List[RequirementCheck]: + return self._checks + + def get_check(self, name: str) -> RequirementCheck: + for check in self._checks: + if check.name == name: + return check + return None + + def __do_validate__(self, context: ValidationContext) -> bool: + """ + Internal method to perform the validation + """ + # Set the validation context + self._validation_context = context + logger.debug("Validation context initialized: %r", context) + # Perform the validation + try: + for check in self._checks: + try: + check.__do_check__(context) + except Exception as e: + self.validation_result.add_error("Unexpected error during check: %s" % e, check=check) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + # Return the result + return self.validation_result.passed() + finally: + # Clear the validation context + self._validation_context = None + logger.debug("Clearing validation context") + + @property + def validator(self): + if self._validation_context is None: + raise OutOfValidationContext("Validation context has not been initialized") + return self._validation_context.validator + @property - def check_class(self) -> Type[Check]: - return self._check_class + def validation_result(self): + if self._validation_context is None: + raise OutOfValidationContext("Validation context has not been initialized") + return self._validation_context.result + + @property + def validation_context(self): + if self._validation_context is None: + raise OutOfValidationContext("Validation context has not been initialized") + return self._validation_context + + @property + def validation_settings(self): + if self._validation_context is None: + raise OutOfValidationContext("Validation context has not been initialized") + return self._validation_context.settings def __eq__(self, other): return self.name == other.name \ @@ -368,6 +529,7 @@ def load(profile: Profile, requirement_type: RequirementType, file_path: Path) - # initialize the set of requirements requirements = [] + # TODO: implement a better way to identify the requirement and check classes # check if the file is a python file if file_path.suffix == ".py": classes = get_classes_from_file(file_path, filter_class=Check) @@ -376,24 +538,26 @@ def load(profile: Profile, requirement_type: RequirementType, file_path: Path) - # instantiate a requirement for each class for check_name, check_class in classes.items(): r = Requirement( - check_class, requirement_type, profile, path=file_path, + requirement_type, profile, path=file_path, name=get_requirement_name_from_file(file_path, check_name=check_name) ) logger.debug("Added Requirement: %r" % r) requirements.append(r) elif file_path.suffix == ".ttl": - from rocrate_validator.requirements.shacl.checks import SHACLCheck - r = Requirement(SHACLCheck, requirement_type, - profile, path=file_path) - requirements.append(r) - logger.debug("Added Requirement: %r" % r) + # from rocrate_validator.requirements.shacl.checks import SHACLCheck + from rocrate_validator.requirements.shacl.requirements import \ + SHACLRequirement + shapes_requirements = SHACLRequirement.load(profile, requirement_type, file_path) + logger.debug("Loaded SHACL requirements: %r" % shapes_requirements) + requirements.extend(shapes_requirements) + logger.debug("Added Requirement: %r" % shapes_requirements) else: logger.warning("Requirement type not supported: %s", file_path.suffix) return requirements -def issue_types(issues: List[Type[CheckIssue]]) -> Type[Check]: +def issue_types(issues: List[Type[CheckIssue]]) -> Type[RequirementCheck]: def class_decorator(cls): cls.issue_types = issues return cls @@ -407,195 +571,6 @@ class Severity(RequirementLevels): ERROR = RequirementType('ERROR', 4) -class CheckResult: - """ - Class to store the result of a check - - Attributes: - check (Check): The check that was performed - code (int): The result code - message (str): The message - """ - - def __init__(self, check: Check): - self.check = check - self._issues: List[CheckIssue] = [] - - @property - def issues(self) -> List[CheckIssue]: - return self._issues - - def add_issue(self, issue: CheckIssue): - issue._check = self.check - self._issues.append(issue) - - def add_error(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.ERROR, message, code, self.check)) - - def add_warning(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.WARNING, message, code, self.check)) - - def add_info(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.INFO, message, code, self.check)) - - def add_optional(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.OPTIONAL, message, code, self.check)) - - def add_may(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.MAY, message, code, self.check)) - - def add_should(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.SHOULD, message, code, self.check)) - - def add_should_not(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.SHOULD_NOT, message, code, self.check)) - - def add_must(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.MUST, message, code, self.check)) - - def add_must_not(self, message: str, code: int = None): - self._issues.append(CheckIssue(Severity.MUST_NOT, message, code, self.check)) - - def get_issues(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: - return [issue for issue in self.issues if issue.severity.value >= severity.value] - - def get_issues_by_severity(self, severity: Severity) -> List[CheckIssue]: - return [issue for issue in self.issues if issue.severity == severity] - - def passed(self, severity: Severity = Severity.WARNING) -> bool: - return not any(issue.severity.value >= severity.value for issue in self.issues) - - -class Check(ABC): - """ - Base class for checks - """ - - def __init__(self, - requirement: Requirement, - validator: Validator, - name: Optional[str] = None, - description: Optional[str] = None) -> None: - self._requirement = requirement - self._validator = validator - self._name = name - self._description = description - # create a result object for the check - self._result: CheckResult = CheckResult(self) - - @property - def requirement(self) -> Requirement: - return self._requirement - - @property - def severity(self) -> Severity: - return self._requirement.type - - @property - def color(self) -> str: - return self.requirement.color - - @property - def name(self) -> str: - if not self._name: - return self.__class__.__name__.replace("Check", "") - return self._name - - @property - def description(self) -> str: - if not self._description: - return self.get_description(self.requirement) - return self._description - - @classmethod - def get_description(cls, requirement: Requirement) -> str: - return cls.__doc__.strip() - - @property - def ro_crate_path(self) -> Path: - return self._validator.rocrate_path - - @property - def file_descriptor_path(self) -> Path: - return get_file_descriptor_path(self.ro_crate_path) - - @property - def result(self) -> CheckResult: - return self._result - - @property - def validator(self) -> Validator: - return self._validator - - def __do_check__(self) -> bool: - """ - Internal method to perform the check - """ - # Check if the check has issue types defined - # TODO: check if this is necessary - # assert self.issue_types, "Check must have issue types defined in the decorator" - # Perform the check - try: - return self.check() - except Exception as e: - self.result.add_error(str(e)) - if logger.isEnabledFor(logging.DEBUG): - logger.exception(e) - return False - - @abstractmethod - def check(self) -> bool: - raise NotImplementedError("Check not implemented") - - def passed(self, severity: Severity = Severity.WARNING) -> bool: - return self.result.passed(severity) - - def get_issues(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: - return self.result.get_issues(severity) - - def get_issues_by_severity(self, severity: Severity) -> List[CheckIssue]: - return self.result.get_issues_by_severity(severity) - - def __str__(self) -> str: - return self.name - - def __repr__(self) -> str: - return f"{self.name}Check()" - - def __eq__(self, other: object) -> bool: - if not isinstance(other, Check): - return False - return self.name == other.name and self.requirement == other.requirement - - def __hash__(self) -> int: - return hash((self.name, self.requirement)) - - def __lt__(self, other: object) -> bool: - if not isinstance(other, Check): - raise ValueError(f"Cannot compare Check with {type(other)}") - return self.requirement < other.requirement or self.name < other.name - - def __le__(self, other: object) -> bool: - if not isinstance(other, Check): - raise ValueError(f"Cannot compare Check with {type(other)}") - return self.requirement <= other.requirement or self.name <= other.name - - def __gt__(self, other: object) -> bool: - if not isinstance(other, Check): - raise ValueError(f"Cannot compare Check with {type(other)}") - return self.requirement > other.requirement or self.name > other.name - - def __ge__(self, other: object) -> bool: - if not isinstance(other, Check): - raise ValueError(f"Cannot compare Check with {type(other)}") - return self.requirement >= other.requirement or self.name >= other.name - - def __ne__(self, other: object) -> bool: - if not isinstance(other, Check): - return True - return self.name != other.name or self.requirement != other.requirement - - class CheckIssue: """ Class to store an issue found during a check @@ -604,12 +579,13 @@ class CheckIssue: severity (IssueSeverity): The severity of the issue message (str): The message code (int): The code + check (RequirementCheck): The check that generated the issue """ def __init__(self, severity: Severity, message: Optional[str] = None, code: int = None, - check: Check = None): + check: RequirementCheck = None): self._severity = severity self._message = message self._code = code @@ -626,7 +602,7 @@ def severity(self) -> str: return self._severity @property - def check(self) -> Check: + def check(self) -> RequirementCheck: """The check that generated the issue""" return self._check @@ -665,22 +641,65 @@ def get_rocrate_path(self): def get_validation_settings(self): return self._validation_settings - def get_failed_checks(self) -> List[Check]: - # return the list of checks that failed + @property + def checks(self) -> List[RequirementCheck]: return [issue.check for issue in self._issues] + def get_failed_checks(self) -> Set[RequirementCheck]: + # return the list of checks that failed + return set([issue.check for issue in self._issues]) + def add_issue(self, issue: CheckIssue): + # TODO: check if the issue belongs to the current validation context self._issues.append(issue) def add_issues(self, issues: List[CheckIssue]): + # TODO: check if the issues belong to the current validation context self._issues.extend(issues) + def add_error(self, message: str, check: RequirementCheck, code: int = None): + self._issues.append(CheckIssue(Severity.ERROR, message, code, check=check)) + + def add_warning(self, message: str, check: RequirementCheck, code: int = None): + self._issues.append(CheckIssue(Severity.WARNING, message, code, check=check)) + + def add_info(self, message: str, check: RequirementCheck, code: int = None): + self._issues.append(CheckIssue(Severity.INFO, message, code, check=check)) + + def add_optional(self, message: str, check: RequirementCheck, code: int = None): + self._issues.append(CheckIssue(Severity.OPTIONAL, message, code, check=check)) + + def add_may(self, message: str, check: RequirementCheck, code: int = None): + self._issues.append(CheckIssue(Severity.MAY, message, code, check=check)) + + def add_should(self, message: str, check: RequirementCheck, code: int = None): + self._issues.append(CheckIssue(Severity.SHOULD, message, code, check=check)) + + def add_should_not(self, message: str, check: RequirementCheck, code: int = None): + self._issues.append(CheckIssue(Severity.SHOULD_NOT, message, code, check=check)) + + def add_must(self, message: str, check: RequirementCheck, code: int = None): + self._issues.append(CheckIssue(Severity.MUST, message, code, check=check)) + + def add_must_not(self, message: str, check: RequirementCheck, code: int = None): + self._issues.append(CheckIssue(Severity.MUST_NOT, message, code, check=check)) + + @property + def issues(self) -> List[CheckIssue]: + return self._issues + def get_issues(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: return [issue for issue in self._issues if issue.severity.value >= severity.value] def get_issues_by_severity(self, severity: Severity) -> List[CheckIssue]: return [issue for issue in self._issues if issue.severity == severity] + def get_issues_by_check(self, check: RequirementCheck, severity: Severity.WARNING) -> List[CheckIssue]: + return [issue for issue in self.issues if issue.check == check and issue.severity.value >= severity.value] + + def get_issues_by_check_and_severity(self, check: RequirementCheck, severity: Severity) -> List[CheckIssue]: + return [issue for issue in self.issues if issue.check == check and issue.severity.value == severity.value] + def has_issues(self, severity: Severity = Severity.WARNING) -> bool: return any(issue.severity.value >= severity.value for issue in self._issues) @@ -858,19 +877,17 @@ def get_ontologies_graph(self, refresh: bool = False): def ontologies_graph(self) -> Graph: return self.get_ontologies_graph() - def validate_requirement(self, requirement: Requirement) -> CheckResult: + def validate_requirements(self, requirements: List[Requirement]) -> ValidationResult: # check if requirement is an instance of Requirement - assert isinstance(requirement, Requirement), "Invalid requirement" - # check if the requirement has a check class - assert requirement.check_class, "Check class not associated with requirement" - # instantiate the check class - check = requirement.check_class(requirement, self) - # run the check - check.__do_check__() - # return the result - return check.result + assert all(isinstance(requirement, Requirement) for requirement in requirements), \ + "Invalid requirement type" + # perform the requirements validation + return self.__do_validate__(requirements) def validate(self) -> ValidationResult: + return self.__do_validate__() + + def __do_validate__(self, requirements: List[Requirement] = None) -> ValidationResult: # initialize the validation result validation_result = ValidationResult( @@ -883,21 +900,45 @@ def validate(self) -> ValidationResult: profiles.extend(self.profile.inherited_profiles) logger.debug("Profiles to validate: %s", profiles) + # initialize the validation context + context = ValidationContext(self, validation_result) + + # for profile in profiles: # perform the requirements validation - requirements = profile.get_requirements( - self.requirement_level, exact_match=self.requirement_level_only) + if not requirements: + requirements = profile.get_requirements( + self.requirement_level, exact_match=self.requirement_level_only) for requirement in requirements: logger.debug("Validating Requirement: %s", requirement) - result = self.validate_requirement(requirement) - logger.debug("Issues: %r", result.get_issues()) - if result and result.passed(): + result = requirement.__do_validate__(context) + logger.debug("Validation Requirement result: %s", result) + if result: logger.debug("Validation Requirement passed: %s", requirement) else: logger.debug(f"Validation Requirement {requirement} failed ") - validation_result.add_issues(result.get_issues()) if self.validation_settings.get("abort_on_first"): logger.debug("Aborting on first failure") return validation_result return validation_result + + +class ValidationContext: + + def __init__(self, validator: Validator, result: ValidationResult): + self._validator = validator + self._result = result + self._settings = validator.validation_settings + + @property + def validator(self) -> Validator: + return self._validator + + @property + def result(self) -> ValidationResult: + return self._result + + @property + def settings(self) -> Dict: + return self._settings diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 161f9371..d5bf3f18 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -14,8 +14,8 @@ RDF_SERIALIZATION_FORMATS_TYPES, SHACL_NS, VALID_INFERENCE_OPTIONS, VALID_INFERENCE_OPTIONS_TYPES) -from ...models import Check, CheckIssue, Severity -from ...requirements.shacl.models import Shape +from ...models import CheckIssue, RequirementCheck, Severity +from ...requirements.shacl.models import ViolationShape # set up logging logger = logging.getLogger(__name__) @@ -107,9 +107,9 @@ def sourceConstraintComponent(self): return self.violation_json[f'{SHACL_NS}sourceConstraintComponent'][0]['@id'] @property - def sourceShape(self) -> Shape: + def sourceShape(self) -> ViolationShape: try: - return Shape(self.source_shape_node, self._graph) + return ViolationShape(self.source_shape_node, self._graph) except Exception as e: logger.error("Error getting source shape: %s" % e) if logger.isEnabledFor(logging.DEBUG): @@ -211,7 +211,7 @@ class Validator: def __init__( self, - check: Check, + check: RequirementCheck, shapes_graph: Optional[Union[GraphLike, str, bytes]], ont_graph: Optional[Union[GraphLike, str, bytes]] = None, ) -> None: @@ -239,7 +239,7 @@ def ont_graph(self) -> Optional[Union[GraphLike, str, bytes]]: return self._ont_graph @property - def check(self) -> Check: + def check(self) -> RequirementCheck: return self._check def validate( From 289581fbaedb8630b9bb0e31ce9278f0e17ab424 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 12:54:51 +0100 Subject: [PATCH 098/902] feat(shacl): :sparkles: improve shapes loading --- rocrate_validator/__init__.py | 0 rocrate_validator/cli/commands/__init__.py | 0 rocrate_validator/requirements/__init__.py | 0 .../requirements/shacl/checks.py | 66 ++------ .../requirements/shacl/models.py | 146 ++++++++++++++++++ .../requirements/shacl/requirements.py | 36 +++++ rocrate_validator/requirements/shacl/utils.py | 0 7 files changed, 198 insertions(+), 50 deletions(-) delete mode 100644 rocrate_validator/__init__.py delete mode 100644 rocrate_validator/cli/commands/__init__.py delete mode 100644 rocrate_validator/requirements/__init__.py create mode 100644 rocrate_validator/requirements/shacl/requirements.py delete mode 100644 rocrate_validator/requirements/shacl/utils.py diff --git a/rocrate_validator/__init__.py b/rocrate_validator/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/rocrate_validator/cli/commands/__init__.py b/rocrate_validator/cli/commands/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/rocrate_validator/requirements/__init__.py b/rocrate_validator/requirements/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 9d022205..855751d3 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -1,70 +1,36 @@ import logging -from typing import Optional -from ...constants import SHACL_NS -from ...models import Check as BaseCheck -from ...models import Requirement, Validator +from ...models import RequirementCheck +from ...models import Requirement +from .models import ShapeProperty from .validator import Validator as SHACLValidator logger = logging.getLogger(__name__) -class SHACLCheck(BaseCheck): +class SHACLCheck(RequirementCheck): def __init__(self, requirement: Requirement, - validator: Validator, - name: Optional[str] = None, - description: Optional[str] = None, - ) -> None: - super().__init__(requirement, validator, name, description) + shapeProperty: ShapeProperty) -> None: + self._shapeProperty = shapeProperty + super().__init__(requirement, shapeProperty.name, shapeProperty.description) @property def name(self): - if not self._name and self.shapes_graph is None: - return "SHACL Check" - - if not self._name: - query = """ - SELECT ?name - WHERE { - ?shape a sh:NodeShape ; - sh:name ?name . - } - """ - # Execute the query - results = [_ for _ in self.shapes_graph.query(query, initNs={"sh": SHACL_NS})] - if results: - self._name = results[0][0] - - return self._name + return self._shapeProperty.name @property def description(self): - if not self._description and self.shapes_graph is None: - return "SHACL Check" - # If the description is not set, query the shapes graph - if not self._description: - self._description = self.query_description(self.shapes_graph) - return self._description + return self._shapeProperty.description - @staticmethod - def query_description(shapes_graph) -> str: - try: - query = """ - SELECT ?description - WHERE { - ?shape a sh:NodeShape ; - sh:description ?description . - } - """ - # Execute the query - results = [_ for _ in shapes_graph.query(query, initNs={"sh": SHACL_NS})] - if results: - return results[0][0] - except Exception as e: - logger.debug("Error getting description: %s", e) - return None + @property + def shapeProperty(self) -> ShapeProperty: + return self._shapeProperty + + @property + def severity(self): + return self.requirement.type @classmethod def get_description(cls, requirement: Requirement): diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index d4101389..9caac41b 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -1,5 +1,9 @@ +from __future__ import annotations + import json import logging +from pathlib import Path +from typing import Dict, List, Optional, Union from rdflib import Graph from rdflib.term import Node @@ -10,7 +14,149 @@ logger = logging.getLogger(__name__) +class ShapeProperty: + def __init__(self, + shape: Shape, + name: str, + description: Optional[str] = None, + group: Optional[str] = None, + node: Optional[str] = None, + default: Optional[str] = None, + order: int = 0): + self._name = name + self._description = description + self._shape = shape + self._group = group + self._node = node + self._default = default + self._order = order + + @property + def name(self): + return self._name + + @property + def description(self): + return self._description + + @property + def shape(self): + return self._shape + + @property + def group(self): + return self._group + + @property + def node(self): + return self._node + + @property + def default(self): + return self._default + + @property + def order(self): + return self._order + + def compare_shape(self, other_model): + return self.shape == other_model.shape + + def compare_name(self, other_model): + return self.name == other_model.name + + class Shape: + def __init__(self, name, description): + self.name = name + self.description = description + self.properties = [] + + def add_property(self, name: str, description: str = None, + group: str = None, node: str = None, default: str = None, order: int = 0): + self.properties.append( + ShapeProperty(self, + name, description, + group, node, default, order)) + + def get_properties(self) -> List[ShapeProperty]: + return self.properties + + def get_property(self, name) -> ShapeProperty: + for prop in self.properties: + if prop.name == name: + return prop + return None + + def __str__(self): + return f"{self.name}: {self.description}" + + def __repr__(self): + return f"Shape({self.name})" + + def __eq__(self, other): + if not isinstance(other, Shape): + return False + return self.name == other.name + + def __hash__(self): + return hash(self.name) + + @classmethod + def load(cls, shapes_path: Union[str, Path]) -> Dict[str, Shape]: + """ + Load the shapes from the graph + """ + shapes_graph = Graph() + shapes_graph.parse(shapes_path, format="turtle") + logger.debug("Shapes graph: %s" % shapes_graph) + + # query the graph for the shapes and shape properties + query = """ + PREFIX sh: + SELECT ?shape ?shapeName ?shapeDescription + ?property ?propertyName ?propertyDescription ?propertyGroup + ?propertyNode ?defaultValue ?order + WHERE { + ?shape a sh:NodeShape ; + sh:name ?shapeName ; + sh:description ?shapeDescription ; + sh:property ?property . + OPTIONAL { + ?property sh:name ?propertyName ; + sh:description ?propertyDescription ; + sh:group ?propertyGroup ; + sh:node ?propertyNode ; + sh:defaultValue ?defaultValue ; + sh:order ?order . + } + } + """ + logger.debug("Performing query: %s" % query) + results = shapes_graph.query(query) + logger.debug("Query results: %s" % results) + + shapes: Dict[str, Shape] = {} + for row in results: + + shape = shapes.get(row['shapeName'], None) + if shape is None: + shape = Shape(row['shapeName'], row['shapeDescription']) + shapes[row['shapeName']] = shape + + print("propertyName", row.get('propertyName'), row['shapeName']) + shape.add_property( + row.get('propertyName') or row['shapeName'], + row.get('propertyDescription') or row['shapeDescription'], + row.get('propertyGroup') or None, + row.get('propertyNode') or None, + row.get('defaultValue') or None, + row.get('order') or 0 + ) + return shapes + + +class ViolationShape: def __init__(self, shape_node: Node, graph: Graph) -> None: # check the input diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py new file mode 100644 index 00000000..76abd1ca --- /dev/null +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -0,0 +1,36 @@ +import logging +from pathlib import Path +from typing import Dict, List + +from ...models import Profile, Requirement, RequirementType +from .checks import SHACLCheck +from .models import Shape + +# set up logging +logger = logging.getLogger(__name__) + + +class SHACLRequirement(Requirement): + + def __init__(self, + type: RequirementType, + shape: Shape, + profile: Profile, + path: Path + ): + self._shape = shape + super().__init__(type, profile, + shape.name, shape.description, path) + # init checks + self._checks = [] + for prop in shape.get_properties(): + self._checks.append(SHACLCheck(self, prop)) + + @staticmethod + def load(profile: Profile, requirement_type: RequirementType, file_path: Path) -> List[Requirement]: + shapes: Dict[str, Shape] = Shape.load(file_path) + logger.debug("Loaded shapes: %s" % shapes) + requirements = [] + for shape in shapes.values(): + requirements.append(SHACLRequirement(requirement_type, shape, profile, file_path)) + return requirements diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py deleted file mode 100644 index e69de29b..00000000 From 6d41495a31bef7d6378c9b498dfec6567a386e55 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 15:02:43 +0100 Subject: [PATCH 099/902] refactor(core): :truck: rename `type` prop to `severity` --- rocrate_validator/cli/commands/profiles.py | 8 ++-- rocrate_validator/models.py | 42 ++++++++++--------- .../requirements/shacl/checks.py | 2 +- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index fbab4186..56617cb0 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -56,9 +56,9 @@ def list_profiles(ctx, profiles_path: str = "./profiles"): requirements = {} logger.debug("Requirements: %s", requirements) for req in profile.requirements: - if not requirements.get(req.type.name, None): - requirements[req.type.name] = 0 - requirements[req.type.name] += 1 + if not requirements.get(req.severity.name, None): + requirements[req.severity.name] = 0 + requirements[req.severity.name] += 1 requirements = ", ".join( [f"[bold][{get_severity_color(severity)}]{severity}: " f"{count}[/{get_severity_color(severity)}][/bold]" @@ -94,7 +94,7 @@ def describe_profile(ctx, table_rows = [] levels_list = set() for requirement in profile.requirements: - level_info = f"[{requirement.color}]{requirement.type.name}[/{requirement.color}]" + level_info = f"[{requirement.color}]{requirement.severity.name}[/{requirement.color}]" levels_list.add(level_info) table_rows.append((requirement.name, Markdown(requirement.description.strip()), level_info)) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index d174d79f..b5ce1cc0 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -363,13 +363,13 @@ def __do_check__(self, context: ValidationContext) -> bool: class Requirement: def __init__(self, - type: RequirementType, + severity: RequirementType, profile: Profile, name: str = None, description: str = None, path: Path = None): self._name = name - self._type = type + self._severity = severity self._profile = profile self._description = description self._path = path @@ -388,13 +388,13 @@ def name(self) -> str: return self._name @property - def type(self) -> RequirementType: - return self._type + def severity(self) -> RequirementType: + return self._severity @property def color(self) -> str: from .colors import get_severity_color - return get_severity_color(self.type) + return get_severity_color(self.severity) @property def profile(self) -> Profile: @@ -477,45 +477,49 @@ def validation_settings(self): raise OutOfValidationContext("Validation context has not been initialized") return self._validation_context.settings - def __eq__(self, other): + def __eq__(self, other: Requirement): + if not isinstance(other, Requirement): + raise ValueError(f"Cannot compare Requirement with {type(other)}") return self.name == other.name \ - and self.type == other.type and self.description == other.description \ + and self.severity == other.severity and self.description == other.description \ and self.path == other.path - def __ne__(self, other): + def __ne__(self, other: Requirement): + if not isinstance(other, Requirement): + raise ValueError(f"Cannot compare Requirement with {type(other)}") return self.name != other.name \ - or self.type != other.type \ + or self.severity != other.type \ or self.description != other.description \ or self.path != other.path def __hash__(self): - return hash((self.name, self.type, self.description, self.path)) + return hash((self.name, self.severity, self.description, self.path)) - def __lt__(self, other) -> bool: + def __lt__(self, other: Requirement) -> bool: if not isinstance(other, Requirement): raise ValueError(f"Cannot compare Requirement with {type(other)}") - return self.type < other.type or self.name < other.name + return self.severity < other.severity or self.name < other.name - def __le__(self, other) -> bool: + def __le__(self, other: Requirement) -> bool: if not isinstance(other, Requirement): raise ValueError(f"Cannot compare Requirement with {type(other)}") - return self.type <= other.type or self.name <= other.name + return self.severity <= other.severity or self.name <= other.name - def __gt__(self, other) -> bool: + def __gt__(self, other: Requirement) -> bool: if not isinstance(other, Requirement): raise ValueError(f"Cannot compare Requirement with {type(other)}") - return self.type > other.type or self.name > other.name + return self.severity > other.severity or self.name > other.name - def __ge__(self, other) -> bool: + def __ge__(self, other: Requirement) -> bool: if not isinstance(other, Requirement): raise ValueError(f"Cannot compare Requirement with {type(other)}") - return self.type >= other.type or self.name >= other.name + return self.severity >= other.severity or self.name >= other.name def __repr__(self): return ( f'ProfileRequirement(' f'name={self.name}, ' - f'type={self.type}, ' + f'severity={self.severity}, ' f'description={self.description}' f', path={self.path}, ' if self.path else '' ')' diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 855751d3..f5131818 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -30,7 +30,7 @@ def shapeProperty(self) -> ShapeProperty: @property def severity(self): - return self.requirement.type + return self.requirement.severity @classmethod def get_description(cls, requirement: Requirement): From ba1e4386193a440ec14f7c42eaa115f13744b8ff Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 22:16:51 +0100 Subject: [PATCH 100/902] feat(cli): :lipstick: reformat output of validation command --- rocrate_validator/cli/commands/validate.py | 32 +++++++++++++--------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index c1e14b89..209937b4 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -134,6 +134,7 @@ def __print_validation_result__( """ Print the validation result """ + if result.passed(severity=severity): console.print( "\n\n[bold]\[[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", @@ -145,22 +146,27 @@ def __print_validation_result__( style="white", ) - for check in result.get_failed_checks(): - # TODO: Add color related to the requirement level associated with the check - issue_color = get_severity_color(check.severity) + for requirement in result.failed_requirements: + issue_color = get_severity_color(requirement.severity) console.print( - Align(f" [severity: [{issue_color}]{check.severity.name}[/{issue_color}], " - f"profile: [magenta]{check.requirement.profile.name }[/magenta]]", align="right") + Align(f" [severity: [{issue_color}]{requirement.severity.name}[/{issue_color}], " + f"profile: [magenta]{requirement.profile.name }[/magenta]]", align="right") ) console.print( - f" -> [bold][magenta]{check.name}[/magenta] check [red]failed[/red][/bold]", + f" * [u bold]Requirement \"[magenta]{requirement.name}[/magenta]\" has [red]not meet[/red][/u bold]", style="white", ) - console.print(f"{' '*4}{check.description}\n", style="white italic") - console.print(f"{' '*4}Detected issues:", style="white bold") - for issue in check.get_issues(): + console.print(f"\n{' '*4}{requirement.description}\n", style="white italic") + + console.print(f"{' '*4}Failed checks:\n", style="white bold") + for check in result.get_failed_checks_by_requirement(requirement): + issue_color = get_severity_color(check.severity) console.print( - f"{' '*4}- [[{issue_color}]{issue.severity.name}[/{issue_color}] " - f"[magenta]{issue.code}[/magenta]]: {issue.message}", - style="white") - console.print("\n", style="white") + f"{' '*4}- " + f"[[magenta]{check.name}[/magenta]]: {check.description}") + console.print(f"\n{' '*6}Detected issues:", style="white bold") + for issue in check.get_issues(): + console.print( + f"{' '*6}- [[{issue_color}]{issue.severity.name}[/{issue_color}] " + f"[magenta]{issue.code}[/magenta]]: {issue.message}") + console.print("\n", style="white") From fcf46bb88264e0160a0f81523f157e5f79feba20 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 22:33:12 +0100 Subject: [PATCH 101/902] feat(core): :sparkles: add progressive ID to requirement objects --- rocrate_validator/models.py | 61 +++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index b5ce1cc0..bea7370a 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -150,6 +150,7 @@ def load_requirements(self) -> List[Requirement]: """ Load the requirements from the profile directory """ + req_id = 0 self._requirements = [] for root, dirs, files in os.walk(self.path): dirs[:] = [d for d in dirs @@ -164,6 +165,8 @@ def load_requirements(self) -> List[Requirement]: requirement_path = requirement_root / file for requirement in Requirement.load( self, RequirementLevels.get(requirement_level), requirement_path): + req_id += 1 + requirement._id = req_id self.add_requirement(requirement) return self._requirements @@ -177,8 +180,8 @@ def get_requirements( self, severity: RequirementType = RequirementLevels.MUST, exact_match: bool = False) -> List[Requirement]: return [requirement for requirement in self.requirements - if not exact_match and requirement.type >= severity or - exact_match and requirement.type == severity] + if not exact_match and requirement.severity >= severity or + exact_match and requirement.severity == severity] @property def requirements_by_severity_map(self) -> Dict[RequirementType, List[Requirement]]: @@ -198,7 +201,7 @@ def has_requirement(self, name: str) -> bool: return self.get_requirement(name) is not None def get_requirements_by_type(self, type: RequirementType) -> List[Requirement]: - return [requirement for requirement in self.requirements if requirement.type == type] + return [requirement for requirement in self.requirements if requirement.severity == type] def add_requirement(self, requirement: Requirement): self._requirements.append(requirement) @@ -368,6 +371,7 @@ def __init__(self, name: str = None, description: str = None, path: Path = None): + self._id = None self._name = name self._severity = severity self._profile = profile @@ -381,6 +385,10 @@ def __init__(self, if not self._name and self._path: self._name = get_requirement_name_from_file(self._path) + @property + def id(self) -> int: + return self._id + @property def name(self) -> str: if not self._name and self._path: @@ -422,7 +430,7 @@ def __init_checks__(self): return checks def get_checks(self) -> List[RequirementCheck]: - return self._checks + return self._checks.copy() def get_check(self, name: str) -> RequirementCheck: for check in self._checks: @@ -635,9 +643,16 @@ def code(self) -> int: class ValidationResult: def __init__(self, rocrate_path: Path, validation_settings: Dict = None): - self._issues: List[CheckIssue] = [] + # reference to the ro-crate path self._rocrate_path = rocrate_path + # reference to the validation settings self._validation_settings = validation_settings + # keep track of the requirements that have been checked + self._validated_requirements: Set[Requirement] = set() + # keep track of the checks that have been performed + self._checks: Set[RequirementCheck] = set() + # keep track of the issues found during the validation + self._issues: List[CheckIssue] = [] def get_rocrate_path(self): return self._rocrate_path @@ -646,13 +661,41 @@ def get_validation_settings(self): return self._validation_settings @property - def checks(self) -> List[RequirementCheck]: - return [issue.check for issue in self._issues] + def validated_requirements(self) -> Set[Requirement]: + return self._validated_requirements.copy() + + @property + def failed_requirements(self) -> Set[Requirement]: + return set([issue.check.requirement for issue in self._issues]) + + @property + def passed_requirements(self) -> Set[Requirement]: + return self._validated_requirements - self.failed_requirements - def get_failed_checks(self) -> Set[RequirementCheck]: - # return the list of checks that failed + @property + def checks(self) -> Set[RequirementCheck]: + return set(self._checks) + + @property + def failed_checks(self) -> Set[RequirementCheck]: return set([issue.check for issue in self._issues]) + @property + def passed_checks(self) -> Set[RequirementCheck]: + return self._checks - self.failed_checks + + def get_passed_checks_by_requirement(self, requirement: Requirement) -> Set[RequirementCheck]: + return set([check for check in self.passed_checks if check.requirement == requirement]) + + def get_failed_checks_by_requirement(self, requirement: Requirement) -> Set[RequirementCheck]: + return set([check for check in self.failed_checks if check.requirement == requirement]) + + def get_failed_checks_by_requirement_and_severity( + self, requirement: Requirement, severity: Severity) -> Set[RequirementCheck]: + return set([check for check in self.failed_checks + if check.requirement == requirement + and check.severity == severity]) + def add_issue(self, issue: CheckIssue): # TODO: check if the issue belongs to the current validation context self._issues.append(issue) From c5aba59ba72f866a90050dece6436881dc446d79 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 22:48:58 +0100 Subject: [PATCH 102/902] fix(core): :pencil2: wrong type name --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index bea7370a..e0b6020d 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -544,7 +544,7 @@ def load(profile: Profile, requirement_type: RequirementType, file_path: Path) - # TODO: implement a better way to identify the requirement and check classes # check if the file is a python file if file_path.suffix == ".py": - classes = get_classes_from_file(file_path, filter_class=Check) + classes = get_classes_from_file(file_path, filter_class=RequirementCheck) logger.debug("Classes: %r" % classes) # instantiate a requirement for each class From 80ea88d745704a62c08ff9325679e80d40df16b3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 22:50:46 +0100 Subject: [PATCH 103/902] feat(core): :sparkles: associate IDs to the requirement checks --- rocrate_validator/models.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index e0b6020d..10d8a0f2 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -166,7 +166,7 @@ def load_requirements(self) -> List[Requirement]: for requirement in Requirement.load( self, RequirementLevels.get(requirement_level), requirement_path): req_id += 1 - requirement._id = req_id + requirement._number = req_id self.add_requirement(requirement) return self._requirements @@ -287,6 +287,7 @@ def __init__(self, check: Callable, description: str = None): self._requirement: Requirement = requirement + self._id = None self._name = name self._description = description self._check = check @@ -297,6 +298,10 @@ def __init__(self, # declare the result of the check self._result: ValidationResult = None + @property + def id(self) -> int: + return self._id + @property def name(self) -> str: if not self._name: @@ -371,7 +376,7 @@ def __init__(self, name: str = None, description: str = None, path: Path = None): - self._id = None + self._number = None self._name = name self._severity = severity self._profile = profile @@ -386,8 +391,8 @@ def __init__(self, self._name = get_requirement_name_from_file(self._path) @property - def id(self) -> int: - return self._id + def number(self) -> int: + return self._number @property def name(self) -> str: @@ -422,11 +427,15 @@ def path(self) -> Path: # write a method to collect the list of decorated check methods def __init_checks__(self): + # initialize the list of checks checks = [] for name, member in inspect.getmembers(self._check_class, inspect.isfunction): if hasattr(member, "check"): check_name = member.name if hasattr(member, "name") else name self._checks.append(RequirementCheck(self, check_name, member, member.__doc__)) + # assign the check ids + self.__assign_check_numbers__() + # return the checks return checks def get_checks(self) -> List[RequirementCheck]: @@ -438,6 +447,10 @@ def get_check(self, name: str) -> RequirementCheck: return check return None + def __assign_check_numbers__(self): + for i, check in enumerate(self._checks): + check._id = i + 1 + def __do_validate__(self, context: ValidationContext) -> bool: """ Internal method to perform the validation From e166998645e321035cdb7ce81ca833e91a1b1167 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 22:53:16 +0100 Subject: [PATCH 104/902] refactor(shacl): :recycle: reorganize method to initialise checks --- .../requirements/shacl/requirements.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index 76abd1ca..729512be 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -22,9 +22,19 @@ def __init__(self, super().__init__(type, profile, shape.name, shape.description, path) # init checks - self._checks = [] - for prop in shape.get_properties(): - self._checks.append(SHACLCheck(self, prop)) + self._checks = self.__init_checks__() + + def __init_checks__(self): + # assign a check to each property of the shape + checks = [] + for prop in self._shape.get_properties(): + property_check = SHACLCheck(self, prop) + logger.debug("Property check %s: %s", property_check.name, property_check.description) + checks.append(property_check) + # assign check IDs + self.__assign_check_numbers__() + # return checks + return checks @staticmethod def load(profile: Profile, requirement_type: RequirementType, file_path: Path) -> List[Requirement]: From 5598deb6ff2df08b20cd0a5135f1ab72d116d947 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 22:56:23 +0100 Subject: [PATCH 105/902] refactor(core): :recycle: rename property to denote obj order --- rocrate_validator/models.py | 20 +++++++++---------- .../requirements/shacl/requirements.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 10d8a0f2..d41757e0 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -166,7 +166,7 @@ def load_requirements(self) -> List[Requirement]: for requirement in Requirement.load( self, RequirementLevels.get(requirement_level), requirement_path): req_id += 1 - requirement._number = req_id + requirement._order_number = req_id self.add_requirement(requirement) return self._requirements @@ -287,7 +287,7 @@ def __init__(self, check: Callable, description: str = None): self._requirement: Requirement = requirement - self._id = None + self._order_number = None self._name = name self._description = description self._check = check @@ -299,8 +299,8 @@ def __init__(self, self._result: ValidationResult = None @property - def id(self) -> int: - return self._id + def order_number(self) -> int: + return self._order_number @property def name(self) -> str: @@ -376,7 +376,7 @@ def __init__(self, name: str = None, description: str = None, path: Path = None): - self._number = None + self._order_number = None self._name = name self._severity = severity self._profile = profile @@ -391,8 +391,8 @@ def __init__(self, self._name = get_requirement_name_from_file(self._path) @property - def number(self) -> int: - return self._number + def order_number(self) -> int: + return self._order_number @property def name(self) -> str: @@ -434,7 +434,7 @@ def __init_checks__(self): check_name = member.name if hasattr(member, "name") else name self._checks.append(RequirementCheck(self, check_name, member, member.__doc__)) # assign the check ids - self.__assign_check_numbers__() + self.__reorder_checks__() # return the checks return checks @@ -447,9 +447,9 @@ def get_check(self, name: str) -> RequirementCheck: return check return None - def __assign_check_numbers__(self): + def __reorder_checks__(self): for i, check in enumerate(self._checks): - check._id = i + 1 + check._order_number = i + 1 def __do_validate__(self, context: ValidationContext) -> bool: """ diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index 729512be..d65ff61f 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -32,7 +32,7 @@ def __init_checks__(self): logger.debug("Property check %s: %s", property_check.name, property_check.description) checks.append(property_check) # assign check IDs - self.__assign_check_numbers__() + self.__reorder_checks__() # return checks return checks From d9aec78e1137a048e71c2a046f4a3b68c30834f7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 22:59:07 +0100 Subject: [PATCH 106/902] feat(shacl): :bug: update query to parse shapes --- .../requirements/shacl/models.py | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 9caac41b..7778edbe 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -114,9 +114,9 @@ def load(cls, shapes_path: Union[str, Path]) -> Dict[str, Shape]: # query the graph for the shapes and shape properties query = """ PREFIX sh: - SELECT ?shape ?shapeName ?shapeDescription - ?property ?propertyName ?propertyDescription ?propertyGroup - ?propertyNode ?defaultValue ?order + SELECT ?shape ?shapeName ?shapeDescription + ?propertyName ?propertyDescription + ?propertyPath ?propertyGroup ?propertyOrder WHERE { ?shape a sh:NodeShape ; sh:name ?shapeName ; @@ -124,34 +124,33 @@ def load(cls, shapes_path: Union[str, Path]) -> Dict[str, Shape]: sh:property ?property . OPTIONAL { ?property sh:name ?propertyName ; - sh:description ?propertyDescription ; - sh:group ?propertyGroup ; - sh:node ?propertyNode ; - sh:defaultValue ?defaultValue ; - sh:order ?order . + sh:description ?propertyDescription ; + sh:group ?propertyGroup ; + sh:order ?propertyOrder ; + sh:path ?propertyPath . } } """ + logger.debug("Performing query: %s" % query) results = shapes_graph.query(query) logger.debug("Query results: %s" % results) shapes: Dict[str, Shape] = {} for row in results: - - shape = shapes.get(row['shapeName'], None) + shape = shapes.get(row.shapeName, None) if shape is None: - shape = Shape(row['shapeName'], row['shapeDescription']) - shapes[row['shapeName']] = shape + shape = Shape(row.shapeName, row.shapeDescription) + shapes[row.shapeName] = shape - print("propertyName", row.get('propertyName'), row['shapeName']) + # print("propertyName vs shapeName", row.propertyName, row.shapeName) shape.add_property( - row.get('propertyName') or row['shapeName'], - row.get('propertyDescription') or row['shapeDescription'], - row.get('propertyGroup') or None, - row.get('propertyNode') or None, - row.get('defaultValue') or None, - row.get('order') or 0 + row.propertyName or row.shapeName, + row.propertyDescription or row.shapeDescription, + group=row.propertyGroup or None, + # node=row.propertyNode or None, + default=None, + order=row.propertyOrder or 0 ) return shapes From 7d45dfd5b945a329539390c7c2477af4f1318f88 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 23:12:56 +0100 Subject: [PATCH 107/902] revert(cli): :lipstick: add order number of requirements and # checks --- rocrate_validator/cli/commands/validate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 209937b4..4636a79c 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -146,6 +146,8 @@ def __print_validation_result__( style="white", ) + console.print("\n[bold]The following requirements have not meet: [/bold]\n", style="white") + for requirement in result.failed_requirements: issue_color = get_severity_color(requirement.severity) console.print( @@ -153,7 +155,7 @@ def __print_validation_result__( f"profile: [magenta]{requirement.profile.name }[/magenta]]", align="right") ) console.print( - f" * [u bold]Requirement \"[magenta]{requirement.name}[/magenta]\" has [red]not meet[/red][/u bold]", + f" [u bold][magenta][{requirement.order_number}] {requirement.name}[/magenta][/u bold]", style="white", ) console.print(f"\n{' '*4}{requirement.description}\n", style="white italic") From 4a309c6cddb6b3cac24248b1b81c93f6b50334d2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 23:19:52 +0100 Subject: [PATCH 108/902] fix(shacl): --- rocrate_validator/requirements/shacl/requirements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index d65ff61f..43fe1c27 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -23,6 +23,8 @@ def __init__(self, shape.name, shape.description, path) # init checks self._checks = self.__init_checks__() + # assign check IDs + self.__reorder_checks__() def __init_checks__(self): # assign a check to each property of the shape @@ -31,8 +33,6 @@ def __init_checks__(self): property_check = SHACLCheck(self, prop) logger.debug("Property check %s: %s", property_check.name, property_check.description) checks.append(property_check) - # assign check IDs - self.__reorder_checks__() # return checks return checks From 25dcda40c4911ad47b6344a5c288983997903358 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 23:20:49 +0100 Subject: [PATCH 109/902] feat(core): :sparkles: add identifier for requirements and checks --- rocrate_validator/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index d41757e0..115909b2 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -302,6 +302,10 @@ def __init__(self, def order_number(self) -> int: return self._order_number + @property + def identifier(self) -> str: + return f"{self.requirement.identifier}.{self.order_number}" + @property def name(self) -> str: if not self._name: @@ -394,6 +398,10 @@ def __init__(self, def order_number(self) -> int: return self._order_number + @property + def identifier(self) -> str: + return f"{self.severity}.{self.order_number}" + @property def name(self) -> str: if not self._name and self._path: From 01aed1a4935eb2728d1837d05eee1bf1da0eb972 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 23:21:47 +0100 Subject: [PATCH 110/902] refactor(cli): :lipstick: reformat table of "profile describe" cmd --- rocrate_validator/cli/commands/profiles.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 56617cb0..03c026dc 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -96,7 +96,10 @@ def describe_profile(ctx, for requirement in profile.requirements: level_info = f"[{requirement.color}]{requirement.severity.name}[/{requirement.color}]" levels_list.add(level_info) - table_rows.append((requirement.name, Markdown(requirement.description.strip()), level_info)) + table_rows.append((str(requirement.order_number), requirement.name, + Markdown(requirement.description.strip()), + str(len(requirement.get_checks())), + level_info)) table = Table(show_header=True, title="Profile Requirements Checks", @@ -106,8 +109,10 @@ def describe_profile(ctx, caption=f"(*) Requirement level: {', '.join(levels_list)}") # Define columns - table.add_column("Name", style="magenta bold", justify="right") + table.add_column("#", style="yellow bold", justify="right") + table.add_column("Name", style="magenta bold", justify="center") table.add_column("Description", style="white italic") + table.add_column("# Checks", style="white", justify="center") table.add_column("Requirement Level (*)", style="white", justify="center") # Add data to the table for row in table_rows: From 89c808c3443303726a56be500ecfa9e880b30161 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 23:22:51 +0100 Subject: [PATCH 111/902] refactor(cli): :lipstick: minor change on output of validate cmd --- 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 4636a79c..f75365e4 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -169,6 +169,6 @@ def __print_validation_result__( console.print(f"\n{' '*6}Detected issues:", style="white bold") for issue in check.get_issues(): console.print( - f"{' '*6}- [[{issue_color}]{issue.severity.name}[/{issue_color}] " - f"[magenta]{issue.code}[/magenta]]: {issue.message}") + f"{' '*6}- [[{issue_color}]Violation[/{issue_color}] of " + f"[magenta]{issue.check.identifier}[/magenta]]: {issue.message}") console.print("\n", style="white") From e0f1116fae45a91940dafee1880d3d007d6c8662 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 23:24:03 +0100 Subject: [PATCH 112/902] build(utils): :package: add ipykernel to dev deps --- poetry.lock | 625 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 625 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index f8f9a023..8c0e10af 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,16 @@ # This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. +[[package]] +name = "appnope" +version = "0.1.4" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = ">=3.6" +files = [ + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, +] + [[package]] name = "astroid" version = "3.1.0" @@ -11,6 +22,88 @@ files = [ {file = "astroid-3.1.0.tar.gz", hash = "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"}, ] +[[package]] +name = "asttokens" +version = "2.4.1" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, +] + +[package.dependencies] +six = ">=1.12.0" + +[package.extras] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "click" version = "8.1.7" @@ -36,6 +129,23 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "comm" +version = "0.2.2" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = false +python-versions = ">=3.8" +files = [ + {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, + {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, +] + +[package.dependencies] +traitlets = ">=4" + +[package.extras] +test = ["pytest"] + [[package]] name = "coverage" version = "7.4.3" @@ -100,6 +210,48 @@ files = [ [package.extras] toml = ["tomli"] +[[package]] +name = "debugpy" +version = "1.8.1" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "debugpy-1.8.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:3bda0f1e943d386cc7a0e71bfa59f4137909e2ed947fb3946c506e113000f741"}, + {file = "debugpy-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda73bf69ea479c8577a0448f8c707691152e6c4de7f0c4dec5a4bc11dee516e"}, + {file = "debugpy-1.8.1-cp310-cp310-win32.whl", hash = "sha256:3a79c6f62adef994b2dbe9fc2cc9cc3864a23575b6e387339ab739873bea53d0"}, + {file = "debugpy-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:7eb7bd2b56ea3bedb009616d9e2f64aab8fc7000d481faec3cd26c98a964bcdd"}, + {file = "debugpy-1.8.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:016a9fcfc2c6b57f939673c874310d8581d51a0fe0858e7fac4e240c5eb743cb"}, + {file = "debugpy-1.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd97ed11a4c7f6d042d320ce03d83b20c3fb40da892f994bc041bbc415d7a099"}, + {file = "debugpy-1.8.1-cp311-cp311-win32.whl", hash = "sha256:0de56aba8249c28a300bdb0672a9b94785074eb82eb672db66c8144fff673146"}, + {file = "debugpy-1.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a9fe0829c2b854757b4fd0a338d93bc17249a3bf69ecf765c61d4c522bb92a8"}, + {file = "debugpy-1.8.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3ebb70ba1a6524d19fa7bb122f44b74170c447d5746a503e36adc244a20ac539"}, + {file = "debugpy-1.8.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2e658a9630f27534e63922ebf655a6ab60c370f4d2fc5c02a5b19baf4410ace"}, + {file = "debugpy-1.8.1-cp312-cp312-win32.whl", hash = "sha256:caad2846e21188797a1f17fc09c31b84c7c3c23baf2516fed5b40b378515bbf0"}, + {file = "debugpy-1.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:edcc9f58ec0fd121a25bc950d4578df47428d72e1a0d66c07403b04eb93bcf98"}, + {file = "debugpy-1.8.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:7a3afa222f6fd3d9dfecd52729bc2e12c93e22a7491405a0ecbf9e1d32d45b39"}, + {file = "debugpy-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d915a18f0597ef685e88bb35e5d7ab968964b7befefe1aaea1eb5b2640b586c7"}, + {file = "debugpy-1.8.1-cp38-cp38-win32.whl", hash = "sha256:92116039b5500633cc8d44ecc187abe2dfa9b90f7a82bbf81d079fcdd506bae9"}, + {file = "debugpy-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:e38beb7992b5afd9d5244e96ad5fa9135e94993b0c551ceebf3fe1a5d9beb234"}, + {file = "debugpy-1.8.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:bfb20cb57486c8e4793d41996652e5a6a885b4d9175dd369045dad59eaacea42"}, + {file = "debugpy-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd3fdd3f67a7e576dd869c184c5dd71d9aaa36ded271939da352880c012e703"}, + {file = "debugpy-1.8.1-cp39-cp39-win32.whl", hash = "sha256:58911e8521ca0c785ac7a0539f1e77e0ce2df753f786188f382229278b4cdf23"}, + {file = "debugpy-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:6df9aa9599eb05ca179fb0b810282255202a66835c6efb1d112d21ecb830ddd3"}, + {file = "debugpy-1.8.1-py2.py3-none-any.whl", hash = "sha256:28acbe2241222b87e255260c76741e1fbf04fdc3b6d094fcf57b6c6f75ce1242"}, + {file = "debugpy-1.8.1.zip", hash = "sha256:f696d6be15be87aef621917585f9bb94b1dc9e8aced570db1b8a6fc14e8f9b42"}, +] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + [[package]] name = "dill" version = "0.3.8" @@ -115,6 +267,20 @@ files = [ graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] +[[package]] +name = "executing" +version = "2.0.1" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.5" +files = [ + {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, + {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + [[package]] name = "flake8" version = "6.1.0" @@ -182,6 +348,74 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "ipykernel" +version = "6.29.3" +description = "IPython Kernel for Jupyter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ipykernel-6.29.3-py3-none-any.whl", hash = "sha256:5aa086a4175b0229d4eca211e181fb473ea78ffd9869af36ba7694c947302a21"}, + {file = "ipykernel-6.29.3.tar.gz", hash = "sha256:e14c250d1f9ea3989490225cc1a542781b095a18a19447fcf2b5eaf7d0ac5bd2"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.6.5" +ipython = ">=7.23.1" +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +matplotlib-inline = ">=0.1" +nest-asyncio = "*" +packaging = "*" +psutil = "*" +pyzmq = ">=24" +tornado = ">=6.1" +traitlets = ">=5.4.0" + +[package.extras] +cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "ipython" +version = "8.22.2" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.10" +files = [ + {file = "ipython-8.22.2-py3-none-any.whl", hash = "sha256:3c86f284c8f3d8f2b6c662f885c4889a91df7cd52056fd02b7d8d6195d7f56e9"}, + {file = "ipython-8.22.2.tar.gz", hash = "sha256:2dcaad9049f9056f1fef63514f176c7d41f930daa78d05b82a176202818f2c14"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt-toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5.13.0" + +[package.extras] +all = ["ipython[black,doc,kernel,nbconvert,nbformat,notebook,parallel,qtconsole,terminal]", "ipython[test,test-extra]"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "stack-data", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] + [[package]] name = "isodate" version = "0.6.1" @@ -210,6 +444,67 @@ files = [ [package.extras] colors = ["colorama (>=0.4.6)"] +[[package]] +name = "jedi" +version = "0.19.1" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, +] + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "jupyter-client" +version = "8.6.1" +description = "Jupyter protocol implementation and client libraries" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_client-8.6.1-py3-none-any.whl", hash = "sha256:3b7bd22f058434e3b9a7ea4b1500ed47de2713872288c0d511d19926f99b459f"}, + {file = "jupyter_client-8.6.1.tar.gz", hash = "sha256:e842515e2bab8e19186d89fdfea7abd15e39dd581f94e399f00e2af5a1652d3f"}, +] + +[package.dependencies] +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +python-dateutil = ">=2.8.2" +pyzmq = ">=23.0" +tornado = ">=6.2" +traitlets = ">=5.3" + +[package.extras] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, + {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, +] + +[package.dependencies] +platformdirs = ">=2.5" +pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +traitlets = ">=5.3" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -234,6 +529,20 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.5" +files = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] + +[package.dependencies] +traitlets = "*" + [[package]] name = "mccabe" version = "0.7.0" @@ -256,6 +565,17 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + [[package]] name = "owlrl" version = "6.0.2" @@ -281,6 +601,35 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + [[package]] name = "platformdirs" version = "4.2.0" @@ -328,6 +677,73 @@ wcwidth = "*" [package.extras] tests = ["pytest", "pytest-cov", "pytest-lazy-fixtures"] +[[package]] +name = "prompt-toolkit" +version = "3.0.43" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, + {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psutil" +version = "5.9.8" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, + {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, + {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, + {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, + {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, + {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, + {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, + {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, + {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, + {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] + +[package.extras] +tests = ["pytest"] + [[package]] name = "pycodestyle" version = "2.11.1" @@ -339,6 +755,17 @@ files = [ {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, ] +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + [[package]] name = "pyflakes" version = "3.1.0" @@ -487,6 +914,148 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +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"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "pyzmq" +version = "25.1.2" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:e624c789359f1a16f83f35e2c705d07663ff2b4d4479bad35621178d8f0f6ea4"}, + {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49151b0efece79f6a79d41a461d78535356136ee70084a1c22532fc6383f4ad0"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9a5f194cf730f2b24d6af1f833c14c10f41023da46a7f736f48b6d35061e76e"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:faf79a302f834d9e8304fafdc11d0d042266667ac45209afa57e5efc998e3872"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f51a7b4ead28d3fca8dda53216314a553b0f7a91ee8fc46a72b402a78c3e43d"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0ddd6d71d4ef17ba5a87becf7ddf01b371eaba553c603477679ae817a8d84d75"}, + {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:246747b88917e4867e2367b005fc8eefbb4a54b7db363d6c92f89d69abfff4b6"}, + {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:00c48ae2fd81e2a50c3485de1b9d5c7c57cd85dc8ec55683eac16846e57ac979"}, + {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a68d491fc20762b630e5db2191dd07ff89834086740f70e978bb2ef2668be08"}, + {file = "pyzmq-25.1.2-cp310-cp310-win32.whl", hash = "sha256:09dfe949e83087da88c4a76767df04b22304a682d6154de2c572625c62ad6886"}, + {file = "pyzmq-25.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:fa99973d2ed20417744fca0073390ad65ce225b546febb0580358e36aa90dba6"}, + {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:82544e0e2d0c1811482d37eef297020a040c32e0687c1f6fc23a75b75db8062c"}, + {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01171fc48542348cd1a360a4b6c3e7d8f46cdcf53a8d40f84db6707a6768acc1"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc69c96735ab501419c432110016329bf0dea8898ce16fab97c6d9106dc0b348"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e124e6b1dd3dfbeb695435dff0e383256655bb18082e094a8dd1f6293114642"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7598d2ba821caa37a0f9d54c25164a4fa351ce019d64d0b44b45540950458840"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d1299d7e964c13607efd148ca1f07dcbf27c3ab9e125d1d0ae1d580a1682399d"}, + {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4e6f689880d5ad87918430957297c975203a082d9a036cc426648fcbedae769b"}, + {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cc69949484171cc961e6ecd4a8911b9ce7a0d1f738fcae717177c231bf77437b"}, + {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9880078f683466b7f567b8624bfc16cad65077be046b6e8abb53bed4eeb82dd3"}, + {file = "pyzmq-25.1.2-cp311-cp311-win32.whl", hash = "sha256:4e5837af3e5aaa99a091302df5ee001149baff06ad22b722d34e30df5f0d9097"}, + {file = "pyzmq-25.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:25c2dbb97d38b5ac9fd15586e048ec5eb1e38f3d47fe7d92167b0c77bb3584e9"}, + {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:11e70516688190e9c2db14fcf93c04192b02d457b582a1f6190b154691b4c93a"}, + {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:313c3794d650d1fccaaab2df942af9f2c01d6217c846177cfcbc693c7410839e"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b3cbba2f47062b85fe0ef9de5b987612140a9ba3a9c6d2543c6dec9f7c2ab27"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc31baa0c32a2ca660784d5af3b9487e13b61b3032cb01a115fce6588e1bed30"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c9087b109070c5ab0b383079fa1b5f797f8d43e9a66c07a4b8b8bdecfd88ee"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f8429b17cbb746c3e043cb986328da023657e79d5ed258b711c06a70c2ea7537"}, + {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5074adeacede5f810b7ef39607ee59d94e948b4fd954495bdb072f8c54558181"}, + {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7ae8f354b895cbd85212da245f1a5ad8159e7840e37d78b476bb4f4c3f32a9fe"}, + {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b264bf2cc96b5bc43ce0e852be995e400376bd87ceb363822e2cb1964fcdc737"}, + {file = "pyzmq-25.1.2-cp312-cp312-win32.whl", hash = "sha256:02bbc1a87b76e04fd780b45e7f695471ae6de747769e540da909173d50ff8e2d"}, + {file = "pyzmq-25.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:ced111c2e81506abd1dc142e6cd7b68dd53747b3b7ae5edbea4578c5eeff96b7"}, + {file = "pyzmq-25.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7b6d09a8962a91151f0976008eb7b29b433a560fde056ec7a3db9ec8f1075438"}, + {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967668420f36878a3c9ecb5ab33c9d0ff8d054f9c0233d995a6d25b0e95e1b6b"}, + {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5edac3f57c7ddaacdb4d40f6ef2f9e299471fc38d112f4bc6d60ab9365445fb0"}, + {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0dabfb10ef897f3b7e101cacba1437bd3a5032ee667b7ead32bbcdd1a8422fe7"}, + {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2c6441e0398c2baacfe5ba30c937d274cfc2dc5b55e82e3749e333aabffde561"}, + {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:16b726c1f6c2e7625706549f9dbe9b06004dfbec30dbed4bf50cbdfc73e5b32a"}, + {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:a86c2dd76ef71a773e70551a07318b8e52379f58dafa7ae1e0a4be78efd1ff16"}, + {file = "pyzmq-25.1.2-cp36-cp36m-win32.whl", hash = "sha256:359f7f74b5d3c65dae137f33eb2bcfa7ad9ebefd1cab85c935f063f1dbb245cc"}, + {file = "pyzmq-25.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:55875492f820d0eb3417b51d96fea549cde77893ae3790fd25491c5754ea2f68"}, + {file = "pyzmq-25.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b8c8a419dfb02e91b453615c69568442e897aaf77561ee0064d789705ff37a92"}, + {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8807c87fa893527ae8a524c15fc505d9950d5e856f03dae5921b5e9aa3b8783b"}, + {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5e319ed7d6b8f5fad9b76daa0a68497bc6f129858ad956331a5835785761e003"}, + {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3c53687dde4d9d473c587ae80cc328e5b102b517447456184b485587ebd18b62"}, + {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9add2e5b33d2cd765ad96d5eb734a5e795a0755f7fc49aa04f76d7ddda73fd70"}, + {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e690145a8c0c273c28d3b89d6fb32c45e0d9605b2293c10e650265bf5c11cfec"}, + {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:00a06faa7165634f0cac1abb27e54d7a0b3b44eb9994530b8ec73cf52e15353b"}, + {file = "pyzmq-25.1.2-cp37-cp37m-win32.whl", hash = "sha256:0f97bc2f1f13cb16905a5f3e1fbdf100e712d841482b2237484360f8bc4cb3d7"}, + {file = "pyzmq-25.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6cc0020b74b2e410287e5942e1e10886ff81ac77789eb20bec13f7ae681f0fdd"}, + {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:bef02cfcbded83473bdd86dd8d3729cd82b2e569b75844fb4ea08fee3c26ae41"}, + {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e10a4b5a4b1192d74853cc71a5e9fd022594573926c2a3a4802020360aa719d8"}, + {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8c5f80e578427d4695adac6fdf4370c14a2feafdc8cb35549c219b90652536ae"}, + {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5dde6751e857910c1339890f3524de74007958557593b9e7e8c5f01cd919f8a7"}, + {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea1608dd169da230a0ad602d5b1ebd39807ac96cae1845c3ceed39af08a5c6df"}, + {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0f513130c4c361201da9bc69df25a086487250e16b5571ead521b31ff6b02220"}, + {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:019744b99da30330798bb37df33549d59d380c78e516e3bab9c9b84f87a9592f"}, + {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e2713ef44be5d52dd8b8e2023d706bf66cb22072e97fc71b168e01d25192755"}, + {file = "pyzmq-25.1.2-cp38-cp38-win32.whl", hash = "sha256:07cd61a20a535524906595e09344505a9bd46f1da7a07e504b315d41cd42eb07"}, + {file = "pyzmq-25.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb7e49a17fb8c77d3119d41a4523e432eb0c6932187c37deb6fbb00cc3028088"}, + {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:94504ff66f278ab4b7e03e4cba7e7e400cb73bfa9d3d71f58d8972a8dc67e7a6"}, + {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6dd0d50bbf9dca1d0bdea219ae6b40f713a3fb477c06ca3714f208fd69e16fd8"}, + {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:004ff469d21e86f0ef0369717351073e0e577428e514c47c8480770d5e24a565"}, + {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c0b5ca88a8928147b7b1e2dfa09f3b6c256bc1135a1338536cbc9ea13d3b7add"}, + {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9a79f1d2495b167119d02be7448bfba57fad2a4207c4f68abc0bab4b92925b"}, + {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:518efd91c3d8ac9f9b4f7dd0e2b7b8bf1a4fe82a308009016b07eaa48681af82"}, + {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1ec23bd7b3a893ae676d0e54ad47d18064e6c5ae1fadc2f195143fb27373f7f6"}, + {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db36c27baed588a5a8346b971477b718fdc66cf5b80cbfbd914b4d6d355e44e2"}, + {file = "pyzmq-25.1.2-cp39-cp39-win32.whl", hash = "sha256:39b1067f13aba39d794a24761e385e2eddc26295826530a8c7b6c6c341584289"}, + {file = "pyzmq-25.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:8e9f3fabc445d0ce320ea2c59a75fe3ea591fdbdeebec5db6de530dd4b09412e"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a8c1d566344aee826b74e472e16edae0a02e2a044f14f7c24e123002dcff1c05"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759cfd391a0996345ba94b6a5110fca9c557ad4166d86a6e81ea526c376a01e8"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c61e346ac34b74028ede1c6b4bcecf649d69b707b3ff9dc0fab453821b04d1e"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cb8fc1f8d69b411b8ec0b5f1ffbcaf14c1db95b6bccea21d83610987435f1a4"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3c00c9b7d1ca8165c610437ca0c92e7b5607b2f9076f4eb4b095c85d6e680a1d"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:df0c7a16ebb94452d2909b9a7b3337940e9a87a824c4fc1c7c36bb4404cb0cde"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:45999e7f7ed5c390f2e87ece7f6c56bf979fb213550229e711e45ecc7d42ccb8"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ac170e9e048b40c605358667aca3d94e98f604a18c44bdb4c102e67070f3ac9b"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1b604734bec94f05f81b360a272fc824334267426ae9905ff32dc2be433ab96"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a793ac733e3d895d96f865f1806f160696422554e46d30105807fdc9841b9f7d"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0806175f2ae5ad4b835ecd87f5f85583316b69f17e97786f7443baaf54b9bb98"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ef12e259e7bc317c7597d4f6ef59b97b913e162d83b421dd0db3d6410f17a244"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea253b368eb41116011add00f8d5726762320b1bda892f744c91997b65754d73"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b9b1f2ad6498445a941d9a4fee096d387fee436e45cc660e72e768d3d8ee611"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8b14c75979ce932c53b79976a395cb2a8cd3aaf14aef75e8c2cb55a330b9b49d"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:889370d5174a741a62566c003ee8ddba4b04c3f09a97b8000092b7ca83ec9c49"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a18fff090441a40ffda8a7f4f18f03dc56ae73f148f1832e109f9bffa85df15"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99a6b36f95c98839ad98f8c553d8507644c880cf1e0a57fe5e3a3f3969040882"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4345c9a27f4310afbb9c01750e9461ff33d6fb74cd2456b107525bbeebcb5be3"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3516e0b6224cf6e43e341d56da15fd33bdc37fa0c06af4f029f7d7dfceceabbc"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:146b9b1f29ead41255387fb07be56dc29639262c0f7344f570eecdcd8d683314"}, + {file = "pyzmq-25.1.2.tar.gz", hash = "sha256:93f1aa311e8bb912e34f004cf186407a4e90eec4f0ecc0efd26056bf7eda0226"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + [[package]] name = "rdflib" version = "7.0.0" @@ -556,6 +1125,25 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + [[package]] name = "toml" version = "0.10.2" @@ -578,6 +1166,41 @@ files = [ {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, ] +[[package]] +name = "tornado" +version = "6.4" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">= 3.8" +files = [ + {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"}, + {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"}, + {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"}, + {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"}, + {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"}, +] + +[[package]] +name = "traitlets" +version = "5.14.2" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +files = [ + {file = "traitlets-5.14.2-py3-none-any.whl", hash = "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80"}, + {file = "traitlets-5.14.2.tar.gz", hash = "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.1)", "pytest-mock", "pytest-mypy-testing"] + [[package]] name = "typing-extensions" version = "4.10.0" @@ -629,4 +1252,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "3f2998c6f66b25aa83bcb4f744dd1741c24752998a00871ff44008fca5f20d58" +content-hash = "6e52666b5d164ed9a31fcdcd404e23b0a12c3d3410d611ab7bcb47af1f5b3bc2" diff --git a/pyproject.toml b/pyproject.toml index 36955076..528ea7d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ pyproject-flake8 = "^6.1.0" pytest = "^8.1.0" pytest-cov = "^4.1.0" pylint = "^3.1.0" +ipykernel = "^6.29.3" [tool.flake8] max-line-length = 120 From 399e2393cec9d09c21403fbbf0b5a30df60889c7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 18 Mar 2024 23:25:09 +0100 Subject: [PATCH 113/902] fix(core): :ambulance: missing methods to compare checks --- rocrate_validator/requirements/shacl/checks.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index f5131818..043c66ad 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -63,3 +63,15 @@ def check(self): return False return True + + def __str__(self) -> str: + return super().__str__() + f" - {self._shapeProperty}" + + def __repr__(self) -> str: + return super().__repr__() + f" - {self._shapeProperty}" + + def __eq__(self, __value: object) -> bool: + return super().__eq__(__value) and self._shapeProperty == __value._shapeProperty + + def __hash__(self) -> int: + return super().__hash__() + hash(self._shapeProperty) From 47bd34276391b843c1f39b093b89047ce14a786a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 19 Mar 2024 10:15:16 +0100 Subject: [PATCH 114/902] feat(core): :sparkles: always return sorted object from validation result --- rocrate_validator/models.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 115909b2..7414782e 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -687,35 +687,44 @@ def validated_requirements(self) -> Set[Requirement]: @property def failed_requirements(self) -> Set[Requirement]: - return set([issue.check.requirement for issue in self._issues]) + return sorted(set([issue.check.requirement for issue in self._issues]), key=lambda x: x.order_number) @property def passed_requirements(self) -> Set[Requirement]: - return self._validated_requirements - self.failed_requirements + return sorted(self._validated_requirements - self.failed_requirements, key=lambda req: req.number_order) @property def checks(self) -> Set[RequirementCheck]: - return set(self._checks) + return sorted(set(self._checks), key=lambda x: x.order_number) @property def failed_checks(self) -> Set[RequirementCheck]: - return set([issue.check for issue in self._issues]) + return sorted(set([issue.check for issue in self._issues]), key=lambda x: x.order_number) @property def passed_checks(self) -> Set[RequirementCheck]: - return self._checks - self.failed_checks + return sorted(self._checks - self.failed_checks, key=lambda x: x.order_number) def get_passed_checks_by_requirement(self, requirement: Requirement) -> Set[RequirementCheck]: - return set([check for check in self.passed_checks if check.requirement == requirement]) + return sorted( + set([check for check in self.passed_checks if check.requirement == requirement]), + key=lambda x: x.order_number + ) def get_failed_checks_by_requirement(self, requirement: Requirement) -> Set[RequirementCheck]: - return set([check for check in self.failed_checks if check.requirement == requirement]) + return sorted( + set([check for check in self.failed_checks if check.requirement == requirement]), + key=lambda x: x.order_number + ) def get_failed_checks_by_requirement_and_severity( self, requirement: Requirement, severity: Severity) -> Set[RequirementCheck]: - return set([check for check in self.failed_checks - if check.requirement == requirement - and check.severity == severity]) + return sorted( + set([check for check in self.failed_checks + if check.requirement == requirement + and check.severity == severity]), + key=lambda x: x.order_number + ) def add_issue(self, issue: CheckIssue): # TODO: check if the issue belongs to the current validation context From d11d75d1b76e8e52fe3d4a4eff31f0bd25ed6da3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 19 Mar 2024 11:46:35 +0100 Subject: [PATCH 115/902] feat(cli): :sparkles: add option to print the package version --- rocrate_validator/cli/main.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index 7b0a3b46..21e7c306 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -2,6 +2,7 @@ import rich_click as click from rich.console import Console +from rocrate_validator.utils import get_version # set up logging logger = logging.getLogger(__name__) @@ -19,8 +20,19 @@ help="Enable debug logging", default=False ) +@click.option( + '-v', + '--version', + is_flag=True, + help="Show the version of the rocrate-validator package", + default=False +) @click.pass_context -def cli(ctx, debug: bool = False): +def cli(ctx, debug: bool = False, version: bool = False): + # If the version flag is set, print the version and exit + if version: + console.print(f"[bold]rocrate-validator [cyan]{get_version()}[/cyan][/bold]") + exit(0) # Set the log level if debug: logging.basicConfig(level=logging.DEBUG) From 11ec2b01804d4d724b4439de29a473b82263557f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 19 Mar 2024 12:36:11 +0100 Subject: [PATCH 116/902] feat(utils): :technologist: configure logging --- rocrate_validator/cli/main.py | 5 +++-- rocrate_validator/config.py | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 rocrate_validator/config.py diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index 21e7c306..6dbea114 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -3,6 +3,7 @@ import rich_click as click from rich.console import Console from rocrate_validator.utils import get_version +from rocrate_validator.config import configure_logging # set up logging logger = logging.getLogger(__name__) @@ -35,9 +36,9 @@ def cli(ctx, debug: bool = False, version: bool = False): exit(0) # Set the log level if debug: - logging.basicConfig(level=logging.DEBUG) + configure_logging(level=logging.DEBUG) else: - logging.basicConfig(level=logging.WARNING) + configure_logging(level=logging.WARNING) # If no subcommand is provided, invoke the default command if ctx.invoked_subcommand is None: # If no subcommand is provided, invoke the default command diff --git a/rocrate_validator/config.py b/rocrate_validator/config.py new file mode 100644 index 00000000..a39c0c3b --- /dev/null +++ b/rocrate_validator/config.py @@ -0,0 +1,27 @@ +import logging +import colorlog + + +def configure_logging(level: int = logging.WARNING): + """ + Configure the logging for the package + + :param level: The logging level + """ + log_format = '[%(log_color)s%(asctime)s%(reset)s] %(levelname)s in %(yellow)s%(module)s%(reset)s: '\ + '%(light_white)s%(message)s%(reset)s' + if level == logging.DEBUG: + log_format = '%(log_color)s%(levelname)s%(reset)s:%(yellow)s%(name)s:%(module)s::%(funcName)s%(reset)s '\ + '@ %(light_green)sline: %(lineno)s%(reset)s - %(light_black)s%(message)s%(reset)s' + + colorlog.basicConfig( + level=level, + format=log_format, + log_colors={ + 'DEBUG': 'cyan', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'red,bg_white', + }, + ) From b27535476dc122acf433b73ef4cc805fcce029e4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 19 Mar 2024 13:07:01 +0100 Subject: [PATCH 117/902] fix(core): :bug: pass strings not node objects --- rocrate_validator/requirements/shacl/checks.py | 4 +++- rocrate_validator/requirements/shacl/requirements.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 043c66ad..f991b0f6 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -14,7 +14,9 @@ def __init__(self, requirement: Requirement, shapeProperty: ShapeProperty) -> None: self._shapeProperty = shapeProperty - super().__init__(requirement, shapeProperty.name, shapeProperty.description) + super().__init__(requirement, + shapeProperty.name.value if shapeProperty.name else None, + shapeProperty.description if shapeProperty.description else None) @property def name(self): diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index 43fe1c27..fc7f6b3d 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -20,7 +20,9 @@ def __init__(self, ): self._shape = shape super().__init__(type, profile, - shape.name, shape.description, path) + shape.name.value if shape.name else "", + shape.description.value if shape.description else "", + path) # init checks self._checks = self.__init_checks__() # assign check IDs From 47089c87a9139881d35525d44d2a0c36bb2b2ece Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 19 Mar 2024 13:09:41 +0100 Subject: [PATCH 118/902] test(profiles): :white_check_mark: add a first test for the FileDescriptor requirement --- tests/conftest.py | 34 ++ .../ro-crate-metadata.json | 317 ++++++++++++++++++ 2 files changed, 351 insertions(+) create mode 100644 tests/conftest.py create mode 100644 tests/data/crates/invalid/0_file_descriptor/missing_file_descriptor/ro-crate-metadata.json diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..dab52b7f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,34 @@ +# calculate the absolute path of the rocrate-validator package +# and add it to the system path +import os + +from pytest import fixture +from rocrate_validator.config import configure_logging + +import logging + +# set up logging +configure_logging(level=logging.DEBUG) + +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +TEST_DATA_PATH = os.path.abspath(os.path.join(CURRENT_PATH, "data")) + + +@fixture +def random_path(): + return "/tmp/random_path" + + +@fixture +def ro_crates_path(): + return f"{TEST_DATA_PATH}/crates" + + +@fixture +def graphs_path(): + return f"{TEST_DATA_PATH}/graphs" + + +@fixture +def graph_books_path(): + return f"{TEST_DATA_PATH}/graphs/books" diff --git a/tests/data/crates/invalid/0_file_descriptor/missing_file_descriptor/ro-crate-metadata.json b/tests/data/crates/invalid/0_file_descriptor/missing_file_descriptor/ro-crate-metadata.json new file mode 100644 index 00000000..e6bf22f4 --- /dev/null +++ b/tests/data/crates/invalid/0_file_descriptor/missing_file_descriptor/ro-crate-metadata.json @@ -0,0 +1,317 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "creator": [ + { + "@id": "#macklin" + }, + { + "@id": "#getz" + }, + { + "@id": "#saglam" + }, + { + "@id": "#wang" + }, + { + "@id": "#heiland" + } + ], + "datePublished": "2021-11-09T00:51:55.837325", + "description": "This model simulates replication dynamics of SARS-CoV-2 (coronavirus / COVID19) in a layer of epithelium and several submodels (such as single-cell response, pyroptosis death model, tissue-damage model, lymph node model and immune response). It is being iteratively prototyped and refined with community support", + "hasPart": [ + { + "@id": "changes.md" + }, + { + "@id": "design/" + }, + { + "@id": "LICENSE" + }, + { + "@id": "math/" + }, + { + "@id": "PhysiCell/" + }, + { + "@id": "PhysiCell_V.1.6.1.zip" + }, + { + "@id": "PhysiCell_V.1.7.0.zip" + }, + { + "@id": "PhysiCell_V.1.7.1.zip" + }, + { + "@id": "PhysiCell_V.1.9.0.zip" + }, + { + "@id": "workflow/retropath.knime" + }, + { + "@id": "lots_of_little_files/" + }, + { + "@id": "README.md" + } + ], + "isBasedOn": "https://zenodo.org/record/5595136#.YYlJ9BB_phF", + "name": "pc4covid19/COVID19: Version 0.5.0: draft v5 prototype" + }, + { + "@id": "changes.md", + "@type": "File" + }, + { + "@id": "design/", + "@type": "Dataset" + }, + { + "@id": "lots_of_little_files/", + "@type": "Dataset", + "name": "Too many files", + "description": "This directory contains many small files, that we're not going to describe in detail.", + "distribution": { + "@id": "http://example.com/downloads/2020/lots_of_little_files.zip" + } + }, + { + "@id": "http://example.com/downloads/2020/lots_of_little_files.zip", + "@type": "DataDownload", + "encodingFormat": "application/zip", + "contentSize": "82818928" + }, + { + "@id": "LICENSE", + "@type": "File" + }, + { + "@id": "math/", + "@type": "Dataset", + "author": { "@id": "https://orcid.org/0000-0001-6121-5409" }, + "contentLocation": { + "@id": "http://sws.geonames.org/8152662/" + }, + "temporalCoverage": "1950/1975" + }, + { + "@id": "http://sws.geonames.org/8152662/", + "name": "Catalina Park" + }, + { + "@id": "https://orcid.org/0000-0001-6121-5409", + "@type": "Person", + "contactPoint": { + "@id": "mailto:tim.luckett@uts.edu.au" + }, + "familyName": "Luckett", + "givenName": "Tim", + "identifier": "https://orcid.org/0000-0001-6121-5409", + "name": "Tim Luckett" + }, + { + "@id": "mailto:tim.luckett@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "tim.luckett@uts.edu.au", + "identifier": "tim.luckett@uts.edu.au", + "url": "https://orcid.org/0000-0001-6121-5409" + }, + { + "@id": "PhysiCell/", + "@type": "Dataset", + "publisher": { "@id": "https://ror.org/03f0f6041" }, + "author": { "@id": "https://orcid.org/0000-0002-3545-944X" }, + "citation": { "@id": "https://doi.org/10.1109/TCYB.2014.2386282" }, + "funder": { + "@id": "https://eresearch.uts.edu.au/projects/provisioner" + } + }, + { + "@id": "https://eresearch.uts.edu.au/projects/provisioner", + "@type": "Organization", + "description": "The University of Technology Sydney Provisioner project is ...", + "funder": [ + { + "@id": "https://ror.org/03f0f6041" + }, + { + "@id": "https://ands.org.au" + } + ], + "identifier": "https://eresearch.uts.edu.au/projects/provisioner", + "name": "Provisioner" + }, + { + "@id": "https://ror.org/03f0f6041", + "@type": "Organization", + "identifier": "https://ror.org/03f0f6041", + "name": "University of Technology Sydney" + }, + { + "@id": "https://ands.org.au", + "@type": "Organization", + "description": "The core purpose of the Australian National Data Service (ANDS) is ...", + "identifier": "https://ands.org.au", + "name": "Australian National Data Service" + }, + { + "@id": "https://doi.org/10.1109/TCYB.2014.2386282", + "@type": "ScholarlyArticle", + "identifier": "https://doi.org/10.1109/TCYB.2014.2386282", + "issn": "2168-2267", + "name": "Topic Model for Graph Mining", + "journal": "IEEE Transactions on Cybernetics", + "datePublished": "2015" + }, + { + "@id": "https://ror.org/03f0f6041", + "@type": "Organization", + "name": "University of Technology Sydney" + }, + { + "@id": "https://orcid.org/0000-0002-3545-944X", + "@type": "Person", + "affiliation": { "@id": "https://ror.org/03f0f6041" }, + "email": "peter.sefton@uts.edu.au", + "name": "Peter Sefton" + }, + { + "@id": "PhysiCell_V.1.6.1.zip", + "@type": "File", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0", + "description": "Creative Commons Attribution 4.0 International License" + }, + { + "@id": "PhysiCell_V.1.7.0.zip", + "@type": "File" + }, + { + "@id": "PhysiCell_V.1.7.1.zip", + "@type": "File" + }, + { + "@id": "PhysiCell_V.1.9.0.zip", + "@type": "File", + "encodingFormat": ["text/plain", { "@id": "some_extension.md" }] + }, + { + "@id": "some_extension.md", + "@type": "File", + "name": "Common Workflow Language (CWL) Workflow Description, v1.0.2" + }, + { + "@id": "README.md", + "@type": "File", + "contentSize": null, + "encodingFormat": ["text/plain", { "@id": "some_extension/" }], + "url": "https://github.com/pc4covid19/COVID19/tree/0.5.0/README.md" + }, + { + "@id": "some_extension/", + "@type": "Dataset", + "name": "Common Workflow Language (CWL) Workflow Description, v1.0.2" + }, + { + "@id": "workflow/retropath.knime", + "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], + "author": { "@id": "#thomas" }, + "name": "RetroPath Knime workflow", + "description": "Retrosynthesis workflow calculating chemical reactions", + "license": { "@id": "https://spdx.org/licenses/CC-BY-NC-SA-4.0" }, + "programmingLanguage": { "@id": "#knime" } + }, + { + "@id": "https://spdx.org/licenses/CC-BY-NC-SA-4.0", + "@type": "CreativeWork", + "name": "CC BY 4.0", + "description": "Creative Commons Attribution 4.0 International License" + }, + { + "@id": "https://omeka.uws.edu.au/farmstofreeways/api/items/383", + "@type": ["RepositoryObject", "ImageObject"], + "identifier": ["ftf_photo_stapleton1"], + "interviewee": [ + { + "@id": "https://omeka.uws.edu.au/farmstofreeways/api/items/595" + } + ], + "description": ["Photo of Eugenie Stapleton inside her home"], + "hasFile": [ + { + "@id": "files/383/original_c0f1189ec13ca936e8f556161663d4ba.jpg" + }, + { + "@id": "files/383/fullsize_c0f1189ec13ca936e8f556161663d4ba.jpg" + }, + { + "@id": "files/383/thumbnail_c0f1189ec13ca936e8f556161663d4ba.jpg" + }, + { + "@id": "files/383/square_thumbnail_c0f1189ec13ca936e8f556161663d4ba.jpg" + } + ], + "thumbnail": [ + { + "@id": "files/383/thumbnail_cddd0f1189ec13ca936e8f556161663d4ba.jpg" + } + ], + "name": ["Photo of Eugenie Stapleton 1"], + "copyright": ["Copyright University of Western Sydney 2015"] + }, + { + "@type": "File", + "@id": "files/384/original_2ebbe681aa6ec138776343974ce8a3dd.jpg" + }, + { + "@type": "File", + "@id": "files/384/fullsize_2ebbe681aa6ec138776343974ce8a3dd.jpg" + }, + { + "@type": "File", + "@id": "files/384/thumbnail_2ebbe681aa6ec138776343974ce8a3dd.jpg" + }, + { + "@type": "File", + "@id": "files/384/square_thumbnail_2ebbe681aa6ec138776343974ce8a3dd.jpg" + }, + { + "@id": "#macklin", + "@type": "Person", + "name": "Paul Macklin" + }, + { + "@id": "#getz", + "@type": "Person", + "name": "Michael-Getz" + }, + { + "@id": "#saglam", + "@type": "Person", + "name": "Ali Sinan Saglam" + }, + { + "@id": "#wang", + "@type": "Person", + "name": "Yafei Wang" + }, + { + "@id": "#heiland", + "@type": "Person", + "name": "Randy Heiland" + } + ] +} From 8da26e2c8ef681c3b452e2f6b01dca82b1b000de Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 19 Mar 2024 15:57:55 +0100 Subject: [PATCH 119/902] refactor(core): :wheelchair: auto convert string path to Path object --- rocrate_validator/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 7414782e..e2c15f11 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -562,6 +562,10 @@ def load(profile: Profile, requirement_type: RequirementType, file_path: Path) - # initialize the set of requirements requirements = [] + # if the path is a string, convert it to a Path + if isinstance(file_path, str): + file_path = Path(file_path) + # TODO: implement a better way to identify the requirement and check classes # check if the file is a python file if file_path.suffix == ".py": From dc1a20bb59e2de81d7f34a0df78ee2925e7ec2ed Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 20 Mar 2024 09:25:23 +0100 Subject: [PATCH 120/902] refactor(cli): :lipstick: fix highlighting --- 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 f75365e4..c434c73c 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -155,7 +155,7 @@ def __print_validation_result__( f"profile: [magenta]{requirement.profile.name }[/magenta]]", align="right") ) console.print( - f" [u bold][magenta][{requirement.order_number}] {requirement.name}[/magenta][/u bold]", + f" [bold][magenta][{requirement.order_number}] [u]{requirement.name}[/u][/magenta][/bold]", style="white", ) console.print(f"\n{' '*4}{requirement.description}\n", style="white italic") From e18b02de6f49cfda4b4b588d695de676649e3562 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 20 Mar 2024 09:26:54 +0100 Subject: [PATCH 121/902] fix(cli): :pencil2: remove \ escape char --- 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 c434c73c..2ecdeb3f 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -137,12 +137,12 @@ def __print_validation_result__( if result.passed(severity=severity): console.print( - "\n\n[bold]\[[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", + "\n\n[bold][[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", style="white", ) else: console.print( - "\n\n[bold]\[[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", + "\n\n[bold][[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", style="white", ) From 542ef0ca8276878590395925e163ff77d885795a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 20 Mar 2024 09:50:39 +0100 Subject: [PATCH 122/902] refactor(core): :building_construction: make Requirement class abstract --- rocrate_validator/models.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index e2c15f11..465b295b 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -372,14 +372,15 @@ def __do_check__(self, context: ValidationContext) -> bool: return self.check() -class Requirement: +class Requirement(ABC): def __init__(self, severity: RequirementType, profile: Profile, name: str = None, description: str = None, - path: Path = None): + path: Path = None, + initialize_checks: bool = True): self._order_number = None self._name = name self._severity = severity @@ -394,6 +395,16 @@ def __init__(self, if not self._name and self._path: self._name = get_requirement_name_from_file(self._path) + # set flag to indicate if the checks have been initialized + self._checks_initialized = False + # initialize the checks if the flag is set + if initialize_checks: + self.__init_checks__() + # assign order numbers to checks + self.__reorder_checks__() + # update the checks initialized flag + self._checks_initialized = True + @property def order_number(self) -> int: return self._order_number @@ -433,18 +444,9 @@ def description(self) -> str: def path(self) -> Path: return self._path - # write a method to collect the list of decorated check methods + @abstractmethod def __init_checks__(self): - # initialize the list of checks - checks = [] - for name, member in inspect.getmembers(self._check_class, inspect.isfunction): - if hasattr(member, "check"): - check_name = member.name if hasattr(member, "name") else name - self._checks.append(RequirementCheck(self, check_name, member, member.__doc__)) - # assign the check ids - self.__reorder_checks__() - # return the checks - return checks + pass def get_checks(self) -> List[RequirementCheck]: return self._checks.copy() From 7b3029beccd76791d4fd58fe77b6908b7836498f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 20 Mar 2024 09:51:58 +0100 Subject: [PATCH 123/902] fix(core): :coffin: remove unused import --- rocrate_validator/models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 465b295b..f3dd1906 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -16,8 +16,7 @@ RDF_SERIALIZATION_FORMATS_TYPES, ROCRATE_METADATA_FILE, VALID_INFERENCE_OPTIONS_TYPES) -from rocrate_validator.utils import (get_classes_from_file, - get_requirement_name_from_file) +from rocrate_validator.utils import get_requirement_name_from_file from .errors import OutOfValidationContext From 6e02912b844a1f0f3c767819e768057b08cf63ce Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 20 Mar 2024 09:55:25 +0100 Subject: [PATCH 124/902] refactor(core): :recycle: make RequirementCheck concrete --- rocrate_validator/models.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index f3dd1906..b6e469ce 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -278,18 +278,18 @@ def decorator(func): return decorator -class RequirementCheck(ABC): +class RequirementCheck: def __init__(self, requirement: Requirement, name: str, - check: Callable, + check_function: Callable, description: str = None): self._requirement: Requirement = requirement self._order_number = None self._name = name self._description = description - self._check = check + self._check_function = check_function # declare the reference to the validation context self._validation_context: ValidationContext = None # declare the reference to the validator @@ -353,9 +353,8 @@ def get_issues(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: def get_issues_by_severity(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: return self._result.get_issues_by_check_and_severity(self, severity) - @abstractmethod def check(self) -> bool: - raise NotImplementedError("Check not implemented") + return self.check_function(self) def __do_check__(self, context: ValidationContext) -> bool: """ From 9d9fc7ef83f32e39c275bfc0b04ca197e59c3a1e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 20 Mar 2024 09:56:32 +0100 Subject: [PATCH 125/902] feat(core): :sparkles: more direct getters for Requirement and RequirementCheck classes --- rocrate_validator/models.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index b6e469ce..d334cb1d 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -321,6 +321,23 @@ def description(self) -> str: def requirement(self) -> Requirement: return self._requirement + @property + def severity(self) -> RequirementType: + return self.requirement.severity + + @property + def check_function(self) -> Callable: + return self._check_function + + @property + def rocrate_path(self) -> Path: + assert self.validator, "ro-crate path not set before the check" + return self.validation_context.rocrate_path + + @property + def file_descriptor_path(self) -> Path: + return self.rocrate_path / ROCRATE_METADATA_FILE + @property def validation_context(self) -> ValidationContext: assert self._validation_context, "Validation context not set before the check" @@ -484,25 +501,25 @@ def __do_validate__(self, context: ValidationContext) -> bool: @property def validator(self): - if self._validation_context is None: + if self._validation_context is None and self._checks_initialized: raise OutOfValidationContext("Validation context has not been initialized") return self._validation_context.validator @property def validation_result(self): - if self._validation_context is None: + if self._validation_context is None and self._checks_initialized: raise OutOfValidationContext("Validation context has not been initialized") return self._validation_context.result @property def validation_context(self): - if self._validation_context is None: + if self._validation_context is None and self._checks_initialized: raise OutOfValidationContext("Validation context has not been initialized") return self._validation_context @property def validation_settings(self): - if self._validation_context is None: + if self._validation_context is None and self._checks_initialized: raise OutOfValidationContext("Validation context has not been initialized") return self._validation_context.settings From 67afbe3abe40fbee8e2719c62d70358e7358efb4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 20 Mar 2024 10:02:40 +0100 Subject: [PATCH 126/902] refactor(core): :recycle: extend Requirement loader; add concrete PyRequirement class --- rocrate_validator/models.py | 18 ++---- .../requirements/python/__init__.py | 60 +++++++++++++++++++ 2 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 rocrate_validator/requirements/python/__init__.py diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index d334cb1d..4fa31952 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -583,20 +583,14 @@ def load(profile: Profile, requirement_type: RequirementType, file_path: Path) - if isinstance(file_path, str): file_path = Path(file_path) - # TODO: implement a better way to identify the requirement and check classes + # TODO: implement a better way to identify the requirement type and class # check if the file is a python file if file_path.suffix == ".py": - classes = get_classes_from_file(file_path, filter_class=RequirementCheck) - logger.debug("Classes: %r" % classes) - - # instantiate a requirement for each class - for check_name, check_class in classes.items(): - r = Requirement( - requirement_type, profile, path=file_path, - name=get_requirement_name_from_file(file_path, check_name=check_name) - ) - logger.debug("Added Requirement: %r" % r) - requirements.append(r) + from rocrate_validator.requirements.python import PyRequirement + py_requirements = PyRequirement.load(profile, requirement_type, file_path) + logger.debug("Loaded Python requirements: %r" % py_requirements) + requirements.extend(py_requirements) + logger.debug("Added Requirement: %r" % py_requirements) elif file_path.suffix == ".ttl": # from rocrate_validator.requirements.shacl.checks import SHACLCheck from rocrate_validator.requirements.shacl.requirements import \ diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py new file mode 100644 index 00000000..b8c5467f --- /dev/null +++ b/rocrate_validator/requirements/python/__init__.py @@ -0,0 +1,60 @@ +import inspect +import logging +from pathlib import Path +from typing import List + +from ...models import Profile, Requirement, RequirementCheck, RequirementType +from ...utils import get_classes_from_file + +# set up logging +logger = logging.getLogger(__name__) + + +class PyRequirement(Requirement): + + def __init__(self, + type: RequirementType, + profile: Profile, + name: str = None, + description: str = None, + path: Path = None, + requirement_check_class=None): + self.requirement_check_class = requirement_check_class + super().__init__(type, profile, name, description, path, initialize_checks=True) + + def __init_checks__(self): + # initialize the list of checks + checks = [] + for name, member in inspect.getmembers(self.requirement_check_class, inspect.isfunction): + if hasattr(member, "check"): + check_name = member.name if hasattr(member, "name") else name + check_description = member.__doc__ if member.__doc__ else "" + check = RequirementCheck(self, check_name, member, check_description) + self._checks.append(check) + logger.debug("Added check: %s", check_name, check) + # return the checks + return checks + + @classmethod + def load(cls, profile: Profile, requirement_type: RequirementType, file_path: Path): + # instantiate a list to store the requirements + requirements: List[Requirement] = [] + + # get the classes from the file + classes = get_classes_from_file(file_path, filter_class=RequirementCheck) + logger.debug("Classes: %r" % classes) + + # instantiate a requirement for each class + for requirement_name, requirement_class in classes.items(): + logger.debug("Processing requirement: %r" % requirement_name) + r = PyRequirement( + requirement_type, profile, + name=requirement_name, + description=requirement_class.__doc__, + path=file_path, + requirement_check_class=requirement_class + ) + logger.debug("Created requirement: %r" % r) + requirements.append(r) + + return requirements From 9d09f0a00f07872b0460c76cc5e590d4c0de28d1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 20 Mar 2024 10:11:43 +0100 Subject: [PATCH 127/902] refactor(core): :recycle: abort_on_first=True by default --- rocrate_validator/models.py | 2 +- rocrate_validator/services.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 4fa31952..16db3480 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -818,7 +818,7 @@ def __init__(self, advanced: Optional[bool] = False, inference: Optional[VALID_INFERENCE_OPTIONS_TYPES] = None, inplace: Optional[bool] = False, - abort_on_first: Optional[bool] = False, + abort_on_first: Optional[bool] = True, allow_infos: Optional[bool] = False, allow_warnings: Optional[bool] = False, serialization_output_path: str = None, diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index e4154b0d..56557abf 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -19,7 +19,7 @@ def validate( advanced: Optional[bool] = False, inference: Optional[Literal["owl", "rdfs"]] = False, inplace: Optional[bool] = False, - abort_on_first: Optional[bool] = False, + abort_on_first: Optional[bool] = True, allow_infos: Optional[bool] = False, allow_warnings: Optional[bool] = False, requirement_level: Union[str, RequirementType] = RequirementLevels.MUST, From df2895b1f44ddcffb19b5fd6206bac0a93b9ce7a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 20 Mar 2024 10:12:24 +0100 Subject: [PATCH 128/902] feat(core): :sparkles: expose rocrate_path on ValidationContext --- rocrate_validator/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 16db3480..9f08cc7d 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1034,3 +1034,9 @@ def result(self) -> ValidationResult: @property def settings(self) -> Dict: return self._settings + + @property + def rocrate_path(self) -> Path: + if isinstance(self.validator.rocrate_path, str): + return Path(self.validator.rocrate_path) + return self.validator.rocrate_path From 327b846fd4879d757b6bc46df620c0c89483500a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 20 Mar 2024 10:48:01 +0100 Subject: [PATCH 129/902] fix(core): :bug: extract check name from decorator --- rocrate_validator/requirements/python/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index b8c5467f..6c572c93 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -27,11 +27,15 @@ def __init_checks__(self): checks = [] for name, member in inspect.getmembers(self.requirement_check_class, inspect.isfunction): if hasattr(member, "check"): - check_name = member.name if hasattr(member, "name") else name + check_name = None + try: + check_name = member.name + except Exception: + check_name = name check_description = member.__doc__ if member.__doc__ else "" check = RequirementCheck(self, check_name, member, check_description) self._checks.append(check) - logger.debug("Added check: %s", check_name, check) + logger.debug("Added check: %s %r", check_name, check) # return the checks return checks From 06e43c35621c40445af8d712c73847cc1f676feb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 20 Mar 2024 10:57:01 +0100 Subject: [PATCH 130/902] style(core): :memo: strip on descriptions --- rocrate_validator/models.py | 5 ++--- rocrate_validator/requirements/python/__init__.py | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 9f08cc7d..a0561119 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -450,9 +450,8 @@ def profile(self) -> Profile: @property def description(self) -> str: if not self._description: - # set docs equal to docstring - docs = self.__class__.__doc__ - self._description = docs.strip() if docs else f"Profile Requirement {self.name}" + self._description = self.__class__.__doc__.strip( + ) if self.__class__.__doc__ else f"Profile Requirement {self.name}" return self._description @property diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index 6c572c93..26ef163f 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -29,10 +29,10 @@ def __init_checks__(self): if hasattr(member, "check"): check_name = None try: - check_name = member.name + check_name = member.name.strip() except Exception: - check_name = name - check_description = member.__doc__ if member.__doc__ else "" + check_name = name.strip() + check_description = member.__doc__.strip() if member.__doc__ else "" check = RequirementCheck(self, check_name, member, check_description) self._checks.append(check) logger.debug("Added check: %s %r", check_name, check) @@ -53,8 +53,8 @@ def load(cls, profile: Profile, requirement_type: RequirementType, file_path: Pa logger.debug("Processing requirement: %r" % requirement_name) r = PyRequirement( requirement_type, profile, - name=requirement_name, - description=requirement_class.__doc__, + name=requirement_name.strip() if requirement_name else "", + description=requirement_class.__doc__.strip() if requirement_class.__doc__ else "", path=file_path, requirement_check_class=requirement_class ) From 6ec0c515222aec87799078c38f7a66d3f9ce28a1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 20 Mar 2024 11:09:15 +0100 Subject: [PATCH 131/902] refactor(utils): :sparkles: restore class and suffix (optional) filters --- rocrate_validator/utils.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 3b1f8ff0..16b8682f 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -130,7 +130,9 @@ def get_full_graph( return full_graph -def get_classes_from_file(file_path: Path, filter_class: Optional[Type] = None): +def get_classes_from_file(file_path: Path, + filter_class: Optional[Type] = None, + class_name_suffix: str = None) -> dict: # ensure the file path is a Path object assert file_path, "The file path is required" if not isinstance(file_path, Path): @@ -154,18 +156,12 @@ def get_classes_from_file(file_path: Path, filter_class: Optional[Type] = None): # Import the module module = import_module(module_name) logger.debug("Module: %r", module) - logger.debug("Members: %r", inspect.getmembers(module)) - - for name, cls in inspect.getmembers(module, inspect.isclass): - logger.debug("Checking object %s", cls) - logger.debug("Module %s", inspect.getmodule(cls)) - logger.debug("Subclass %s", issubclass(cls, filter_class)) - logger.debug("Name %s", cls.__name__.endswith('Check')) # Get all classes in the module that are subclasses of Check classes = {name: cls for name, cls in inspect.getmembers(module, inspect.isclass) - if cls.__module__ == module_name and cls.__name__.endswith('Check')} - # if not filter_class or (issubclass(cls, filter_class) and cls != filter_class)} + if cls.__module__ == module_name + and (not class_name_suffix or cls.__name__.endswith(class_name_suffix)) + and (not filter_class or (issubclass(cls, filter_class) and cls != filter_class))} return classes From 5a795c9bda33a4a8b2defc1a4b153c1b0f8181b5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 20 Mar 2024 11:56:15 +0100 Subject: [PATCH 132/902] refactor(cli): :building_construction: rename table title --- rocrate_validator/cli/commands/profiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 03c026dc..f765a356 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -102,7 +102,7 @@ def describe_profile(ctx, level_info)) table = Table(show_header=True, - title="Profile Requirements Checks", + title="Profile Requirements", header_style="bold cyan", border_style="bright_black", show_footer=False, From 66f6edcfb7216634a18bcc65c47e0df2037847b3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 21 Mar 2024 15:57:42 +0100 Subject: [PATCH 133/902] feat(profiles/ro-crate): :sparkles: add minimal shape to validate the file descriptor existence --- profiles/ro-crate/must/1_file-descriptor.ttl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 profiles/ro-crate/must/1_file-descriptor.ttl diff --git a/profiles/ro-crate/must/1_file-descriptor.ttl b/profiles/ro-crate/must/1_file-descriptor.ttl new file mode 100644 index 00000000..00dc5c14 --- /dev/null +++ b/profiles/ro-crate/must/1_file-descriptor.ttl @@ -0,0 +1,20 @@ +@prefix : <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +:FileDescriptorShouldExists + a sh:NodeShape ; + sh:name "RO-Crate Metadata File Descriptor MUST exists" ; + sh:description "The root of the document MUST have a node with ID ro-crate-metadata.json" ; + sh:targetNode :ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:path rdf:type ; + sh:minCount 1 ; + ] . + From e8834d3c068bc4fa066c2b54b3da23b286cbecc1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 21 Mar 2024 16:27:35 +0100 Subject: [PATCH 134/902] test(test-conf): :wrench: update config --- tests/conftest.py | 30 ++++++++++++++++++++++++++++++ tests/pytest.ini | 5 +++++ tests/ro_crates.py | 27 +++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 tests/pytest.ini create mode 100644 tests/ro_crates.py diff --git a/tests/conftest.py b/tests/conftest.py index dab52b7f..c4636460 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,8 +11,13 @@ configure_logging(level=logging.DEBUG) CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) + +# test data paths TEST_DATA_PATH = os.path.abspath(os.path.join(CURRENT_PATH, "data")) +# profiles paths +PROFILES_PATH = f"{CURRENT_PATH}/../profiles" + @fixture def random_path(): @@ -29,6 +34,31 @@ def graphs_path(): return f"{TEST_DATA_PATH}/graphs" +@fixture +def profiles_path(): + return PROFILES_PATH + + @fixture def graph_books_path(): return f"{TEST_DATA_PATH}/graphs/books" + + +@fixture +def ro_crate_profile_path(profiles_path): + return os.path.join(profiles_path, "ro-crate") + + +@fixture +def ro_crate_profile_must_path(ro_crate_profile_path): + return os.path.join(ro_crate_profile_path, "must") + + +@fixture +def ro_crate_profile_should_path(ro_crate_profile_path): + return os.path.join(ro_crate_profile_path, "should") + + +@fixture +def ro_crate_profile_may_path(ro_crate_profile_path): + return os.path.join(ro_crate_profile_path, "may") diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 00000000..a1080c1e --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +# markers = sources +# log_cli=true +# log_level=INFO +; filterwarnings = diff --git a/tests/ro_crates.py b/tests/ro_crates.py new file mode 100644 index 00000000..6b01be11 --- /dev/null +++ b/tests/ro_crates.py @@ -0,0 +1,27 @@ +import os + +from pytest import fixture + +import logging + +logging.basicConfig(level=logging.DEBUG) + +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +TEST_DATA_PATH = os.path.abspath(os.path.join(CURRENT_PATH, "data")) +CRATES_DATA_PATH = f"{TEST_DATA_PATH}/crates" +VALID_CRATES_DATA_PATH = f"{CRATES_DATA_PATH}/valid" +INVALID_CRATES_DATA_PATH = f"{CRATES_DATA_PATH}/invalid" + + +@fixture +def ro_crates_path(): + return CRATES_DATA_PATH + + +class InvalidFileDescriptor: + + base_path = f"{INVALID_CRATES_DATA_PATH}/0_file_descriptor" + + @property + def missing_file_descriptor(self): + return f"{self.base_path}/missing_file_descriptor" From 6835dfca7690163dcda9b27bcd3d0a66594431b5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 12:21:58 +0100 Subject: [PATCH 135/902] feat(shacl): :building_construction: improve SHACL object network and its parser --- .../requirements/shacl/checks.py | 32 +-- .../requirements/shacl/models.py | 189 ++++++++++++------ .../requirements/shacl/requirements.py | 8 +- 3 files changed, 147 insertions(+), 82 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index f991b0f6..7339a6cd 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -10,21 +10,21 @@ class SHACLCheck(RequirementCheck): + + """ + A SHACL check for a specific shape property + """ + def __init__(self, requirement: Requirement, - shapeProperty: ShapeProperty) -> None: + shapeProperty: ShapeProperty = None) -> None: self._shapeProperty = shapeProperty - super().__init__(requirement, - shapeProperty.name.value if shapeProperty.name else None, - shapeProperty.description if shapeProperty.description else None) - @property - def name(self): - return self._shapeProperty.name - - @property - def description(self): - return self._shapeProperty.description + super().__init__(requirement, + shapeProperty.name + if shapeProperty and shapeProperty.name else None, + shapeProperty.description + if shapeProperty and shapeProperty.description else None) @property def shapeProperty(self) -> ShapeProperty: @@ -67,13 +67,15 @@ def check(self): return True def __str__(self) -> str: - return super().__str__() + f" - {self._shapeProperty}" + return super().__str__() + (f" - {self._shapeProperty}" if self._shapeProperty else "") def __repr__(self) -> str: - return super().__repr__() + f" - {self._shapeProperty}" + return super().__repr__() + (f" - {self._shapeProperty}" if self._shapeProperty else "") def __eq__(self, __value: object) -> bool: - return super().__eq__(__value) and self._shapeProperty == __value._shapeProperty + if not isinstance(__value, type(self)): + return NotImplemented + return super().__eq__(__value) and self._shapeProperty == getattr(__value, '_shapeProperty', None) def __hash__(self) -> int: - return super().__hash__() + hash(self._shapeProperty) + return super().__hash__() + (hash(self._shapeProperty) if self._shapeProperty else 0) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 7778edbe..52bbf647 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -3,9 +3,9 @@ import json import logging from pathlib import Path -from typing import Dict, List, Optional, Union +from typing import Dict, List, Union -from rdflib import Graph +from rdflib import Graph, Namespace, URIRef from rdflib.term import Node from ...constants import SHACL_NS @@ -15,49 +15,70 @@ class ShapeProperty: + + # define default values + name: str = None + description: str = None + group: str = None + defaultValue: str = None + order: int = 0 + def __init__(self, shape: Shape, - name: str, - description: Optional[str] = None, - group: Optional[str] = None, - node: Optional[str] = None, - default: Optional[str] = None, - order: int = 0): - self._name = name - self._description = description - self._shape = shape - self._group = group - self._node = node - self._default = default - self._order = order - - @property - def name(self): - return self._name + shape_property_node: Node): - @property - def description(self): - return self._description + # store the shape + self._shape = shape + # store the node + self._node = shape_property_node + + # TODO: refactor moving URIRef to constants + + # create a graph for the shape property + shapes_graph = shape.shapes_graph + shape_property_graph = Graph() + shape_property_graph += shapes_graph.triples((shape.node, None, None)) + shape_property_graph += shapes_graph.triples((shape_property_node, None, None)) + # remove dangling properties + for s, p, o in shape_property_graph: + logger.debug(f"Processing {p} of property graph {shape_property_node}") + if p == URIRef("http://www.w3.org/ns/shacl#property") and o != shape_property_node: + shape_property_graph.remove((s, p, o)) + # add BNodes + for s, p, o in shape_property_graph: + shape_property_graph += shapes_graph.triples((o, None, None)) + + # Use the triples method to get all triples that are part of a list + RDF = Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#") + first_predicate = RDF.first + rest_predicate = RDF.rest + shape_property_graph += shapes_graph.triples((None, first_predicate, None)) + shape_property_graph += shapes_graph.triples((None, rest_predicate, None)) + for _, _, object in shape_property_graph: + shape_property_graph += shapes_graph.triples((object, None, None)) + + # store the graph + self._shape_property_graph = shape_property_graph + + # inject attributes of the shape property + for _, p, o in shape_property_graph: + predicate_as_string = str(p) + logger.debug(f"Processing {predicate_as_string} of property graph {shape_property_node}") + if predicate_as_string.startswith("http://www.w3.org/ns/shacl#"): + property_name = predicate_as_string.split("#")[-1] + setattr(self, property_name, o.toPython()) @property def shape(self): return self._shape - @property - def group(self): - return self._group - @property def node(self): return self._node @property - def default(self): - return self._default - - @property - def order(self): - return self._order + def shape_property_graph(self): + return self._shape_property_graph def compare_shape(self, other_model): return self.shape == other_model.shape @@ -67,23 +88,76 @@ def compare_name(self, other_model): class Shape: - def __init__(self, name, description): + def __init__(self, name, description, node: Node, shapes_graph: Graph): + self.name = name self.description = description - self.properties = [] + self._properties = [] + self._node = node + self._shapes_graph = shapes_graph + + # create a graph for the shape + logger.warning("SHAPE NODE: %s" % node) + shape_graph = Graph() + shape_graph += shapes_graph.triples((node, None, None)) + + # Define the property predicate + predicate = URIRef("http://www.w3.org/ns/shacl#property") - def add_property(self, name: str, description: str = None, - group: str = None, node: str = None, default: str = None, order: int = 0): - self.properties.append( - ShapeProperty(self, - name, description, - group, node, default, order)) + # Use the triples method to get all triples with the particular predicate + first_triples = shape_graph.triples((None, predicate, None)) + + # For each triple from the first call, get all triples whose subject is the object of the first triple + for _, _, object in first_triples: + shape_graph += shapes_graph.triples((object, None, None)) + self._properties.append(ShapeProperty(self, object)) + + # print triples of the shape graph + for s, p, o in shape_graph: + logger.warning(f"SHAPE GRAPH: {s} {p} {o}") + + # store the graph + self._shape_graph = shape_graph + + shape_graph.serialize(f"/tmp/shapes/{name}.ttl", format="turtle") + + def __process_shape_property(self, shape_node: Node, property_node: Node) -> ShapeProperty: + + # create a graph for the shape + shape_property_graph = Graph() + shape_property_graph += self._shapes_graph.triples((shape_node, None, None)) + shape_property_graph += self._shapes_graph.triples((property_node, None, None)) + + # print triples of the shape graph + for s, p, o in shape_property_graph: + logger.warning(f"SHAPE PROPERTY GRAPH: {s} {p} {o}") + + # store the graph + shape_property_graph.serialize(f"/tmp/shapes/{self.name}_{property_node}.ttl", format="turtle") + + return ShapeProperty(self, "name", "description") + + @property + def node(self): + return self._node + + @property + def shape_graph(self): + return self._shape_graph + + @property + def shapes_graph(self): + return self._shapes_graph + + @property + def properties(self): + return self._properties.copy() def get_properties(self) -> List[ShapeProperty]: - return self.properties + return self._properties.copy() def get_property(self, name) -> ShapeProperty: - for prop in self.properties: + for prop in self._properties: if prop.name == name: return prop return None @@ -103,32 +177,24 @@ def __hash__(self): return hash(self.name) @classmethod - def load(cls, shapes_path: Union[str, Path]) -> Dict[str, Shape]: + def load(cls, shapes_path: Union[str, Path], publicID: str = None) -> Dict[str, Shape]: """ Load the shapes from the graph """ shapes_graph = Graph() - shapes_graph.parse(shapes_path, format="turtle") + shapes_graph.parse(shapes_path, format="turtle", publicID=publicID) logger.debug("Shapes graph: %s" % shapes_graph) - # query the graph for the shapes and shape properties + # query the graph for the shapes query = """ PREFIX sh: - SELECT ?shape ?shapeName ?shapeDescription + SELECT ?shape ?shapeName ?shapeDescription ?propertyName ?propertyDescription ?propertyPath ?propertyGroup ?propertyOrder WHERE { ?shape a sh:NodeShape ; sh:name ?shapeName ; - sh:description ?shapeDescription ; - sh:property ?property . - OPTIONAL { - ?property sh:name ?propertyName ; - sh:description ?propertyDescription ; - sh:group ?propertyGroup ; - sh:order ?propertyOrder ; - sh:path ?propertyPath . - } + sh:description ?shapeDescription . } """ @@ -140,18 +206,11 @@ def load(cls, shapes_path: Union[str, Path]) -> Dict[str, Shape]: for row in results: shape = shapes.get(row.shapeName, None) if shape is None: - shape = Shape(row.shapeName, row.shapeDescription) + shape = Shape(row.shapeName, row.shapeDescription, + row.shape, shapes_graph) shapes[row.shapeName] = shape - # print("propertyName vs shapeName", row.propertyName, row.shapeName) - shape.add_property( - row.propertyName or row.shapeName, - row.propertyDescription or row.shapeDescription, - group=row.propertyGroup or None, - # node=row.propertyNode or None, - default=None, - order=row.propertyOrder or 0 - ) + logger.debug("Loaded shapes: %s" % shapes) return shapes diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index fc7f6b3d..3385ef43 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -16,8 +16,7 @@ def __init__(self, type: RequirementType, shape: Shape, profile: Profile, - path: Path - ): + path: Path): self._shape = shape super().__init__(type, profile, shape.name.value if shape.name else "", @@ -32,9 +31,14 @@ def __init_checks__(self): # assign a check to each property of the shape checks = [] for prop in self._shape.get_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 + if len(checks) == 0: + checks.append(SHACLCheck(self)) # return checks return checks From 7fd0f5cb2a8f2187ae09243519d7c9582366011a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 12:28:36 +0100 Subject: [PATCH 136/902] fix(core): :bug: missing publicID parameter --- rocrate_validator/models.py | 2 +- rocrate_validator/requirements/shacl/requirements.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index a0561119..9c610677 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -898,7 +898,7 @@ def data_graph(self) -> Graph: def load_profile(self): # load profile - profile = Profile.load(self.profile_path) + profile = Profile.load(self.profile_path, publicID=self.publicID) logger.debug("Profile: %s", profile) return profile diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index 3385ef43..f9fe90fa 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -43,8 +43,9 @@ def __init_checks__(self): return checks @staticmethod - def load(profile: Profile, requirement_type: RequirementType, file_path: Path) -> List[Requirement]: - shapes: Dict[str, Shape] = Shape.load(file_path) + def load(profile: Profile, requirement_type: RequirementType, + file_path: Path, publicID: str = None) -> List[Requirement]: + shapes: Dict[str, Shape] = Shape.load(file_path, publicID=publicID) logger.debug("Loaded shapes: %s" % shapes) requirements = [] for shape in shapes.values(): From 8281f2aafaf9ea5c39d339392b37f4376f1a1556 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 12:32:33 +0100 Subject: [PATCH 137/902] refactor(shacl): :label: more specific typings --- rocrate_validator/requirements/shacl/validator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index d5bf3f18..45c91eab 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -211,7 +211,7 @@ class Validator: def __init__( self, - check: RequirementCheck, + check: SHACLCheck, shapes_graph: Optional[Union[GraphLike, str, bytes]], ont_graph: Optional[Union[GraphLike, str, bytes]] = None, ) -> None: @@ -239,7 +239,7 @@ def ont_graph(self) -> Optional[Union[GraphLike, str, bytes]]: return self._ont_graph @property - def check(self) -> RequirementCheck: + def check(self) -> SHACLCheck: return self._check def validate( From 2aaaa2fd6d1f27a540c345b446cb1f2e223b6a88 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 13:44:29 +0100 Subject: [PATCH 138/902] fix(core): :wastebasket: remove unused imports --- rocrate_validator/requirements/shacl/validator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 45c91eab..9c97e272 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -14,8 +14,9 @@ RDF_SERIALIZATION_FORMATS_TYPES, SHACL_NS, VALID_INFERENCE_OPTIONS, VALID_INFERENCE_OPTIONS_TYPES) -from ...models import CheckIssue, RequirementCheck, Severity +from ...models import CheckIssue, Severity from ...requirements.shacl.models import ViolationShape +from .checks import SHACLCheck # set up logging logger = logging.getLogger(__name__) From 5af18bb30f10bbfe00abcdf6ea6e155ec5386bbd Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 13:45:36 +0100 Subject: [PATCH 139/902] feat(shacl): :sparkles: constraint the shapes graph to the current property shape --- rocrate_validator/requirements/shacl/checks.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 7339a6cd..c9fa0fb2 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -4,7 +4,7 @@ from ...models import RequirementCheck from ...models import Requirement from .models import ShapeProperty -from .validator import Validator as SHACLValidator + logger = logging.getLogger(__name__) @@ -45,10 +45,13 @@ def shapes_graph(self): return self.validator.get_graph_of_shapes(self.requirement.name) def check(self): - shapes_graph = self.shapes_graph ontology_graph = self.validator.ontologies_graph data_graph = self.validator.data_graph + # constraint the shapes graph to the current property shape + shapes_graph = self.shapeProperty.shape_property_graph + + from .validator import Validator as SHACLValidator shacl_validator = SHACLValidator( self, shapes_graph=shapes_graph, ont_graph=ontology_graph) result = shacl_validator.validate( From 8cb99c21322afc182d03abb9f8d1820abe581396 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 13:52:13 +0100 Subject: [PATCH 140/902] refactor(shacl): :wastebasket: clean up --- .../requirements/shacl/models.py | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 52bbf647..da9d12df 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -112,31 +112,9 @@ def __init__(self, name, description, node: Node, shapes_graph: Graph): shape_graph += shapes_graph.triples((object, None, None)) self._properties.append(ShapeProperty(self, object)) - # print triples of the shape graph - for s, p, o in shape_graph: - logger.warning(f"SHAPE GRAPH: {s} {p} {o}") - # store the graph self._shape_graph = shape_graph - shape_graph.serialize(f"/tmp/shapes/{name}.ttl", format="turtle") - - def __process_shape_property(self, shape_node: Node, property_node: Node) -> ShapeProperty: - - # create a graph for the shape - shape_property_graph = Graph() - shape_property_graph += self._shapes_graph.triples((shape_node, None, None)) - shape_property_graph += self._shapes_graph.triples((property_node, None, None)) - - # print triples of the shape graph - for s, p, o in shape_property_graph: - logger.warning(f"SHAPE PROPERTY GRAPH: {s} {p} {o}") - - # store the graph - shape_property_graph.serialize(f"/tmp/shapes/{self.name}_{property_node}.ttl", format="turtle") - - return ShapeProperty(self, "name", "description") - @property def node(self): return self._node From 8345bbfebc90f7f8ab44a202797ac13f5ceb02ac Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 13:53:53 +0100 Subject: [PATCH 141/902] refactor(shacl): :loud_sound: update log level --- rocrate_validator/requirements/shacl/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index da9d12df..cbf0f3e7 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -97,7 +97,7 @@ def __init__(self, name, description, node: Node, shapes_graph: Graph): self._shapes_graph = shapes_graph # create a graph for the shape - logger.warning("SHAPE NODE: %s" % node) + logger.debug("Initializing graph for the shape: %s" % node) shape_graph = Graph() shape_graph += shapes_graph.triples((node, None, None)) From 5d5c0e7de440545486f95381013b0d9d883bcd9c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 13:54:49 +0100 Subject: [PATCH 142/902] refactor(shacl): :recycle: redifine sh:property uri --- rocrate_validator/requirements/shacl/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index cbf0f3e7..bec67510 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -102,7 +102,7 @@ def __init__(self, name, description, node: Node, shapes_graph: Graph): shape_graph += shapes_graph.triples((node, None, None)) # Define the property predicate - predicate = URIRef("http://www.w3.org/ns/shacl#property") + predicate = URIRef(SHACL_NS + "property") # Use the triples method to get all triples with the particular predicate first_triples = shape_graph.triples((None, predicate, None)) From 3437e2c7de1c373e299fb626f7f2f0966164566b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 15:03:04 +0100 Subject: [PATCH 143/902] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor(shacl)?= =?UTF-8?q?:=20simplify=20shape=20loading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/constants.py | 9 +++ .../requirements/shacl/models.py | 64 ++++++++----------- .../requirements/shacl/requirements.py | 4 +- rocrate_validator/requirements/shacl/utils.py | 43 +++++++++++++ 4 files changed, 81 insertions(+), 39 deletions(-) create mode 100644 rocrate_validator/requirements/shacl/utils.py diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index 684ca07b..5d875932 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -4,6 +4,9 @@ # Define SHACL namespace SHACL_NS = "http://www.w3.org/ns/shacl#" +# Define RDF syntax namespace +RDF_SYNTAX_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" + # Define the rocrate-metadata.json file name ROCRATE_METADATA_FILE = "ro-crate-metadata.json" @@ -36,3 +39,9 @@ # Define allowed inference options VALID_INFERENCE_OPTIONS_TYPES = typing.Literal["owl", "rdfs", "both", None] VALID_INFERENCE_OPTIONS = typing.get_args(VALID_INFERENCE_OPTIONS_TYPES) + +# Define allowed requirement levels +VALID_REQUIREMENT_LEVELS_TYPES = typing.Literal[ + 'MAY', 'OPTIONAL', 'SHOULD', 'SHOULD_NOT', + 'REQUIRED', 'MUST', 'MUST_NOT', 'SHALL', 'SHALL_NOT', 'RECOMMENDED' +] diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index bec67510..00d23e70 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -5,10 +5,11 @@ from pathlib import Path from typing import Dict, List, Union -from rdflib import Graph, Namespace, URIRef +from rdflib import RDF, Graph, Namespace, URIRef from rdflib.term import Node from ...constants import SHACL_NS +from .utils import inject_attributes # set up logging logger = logging.getLogger(__name__) @@ -60,13 +61,8 @@ def __init__(self, # store the graph self._shape_property_graph = shape_property_graph - # inject attributes of the shape property - for _, p, o in shape_property_graph: - predicate_as_string = str(p) - logger.debug(f"Processing {predicate_as_string} of property graph {shape_property_node}") - if predicate_as_string.startswith("http://www.w3.org/ns/shacl#"): - property_name = predicate_as_string.split("#")[-1] - setattr(self, property_name, o.toPython()) + # inject attributes of the shape property to the object + inject_attributes(shape_property_graph, self) @property def shape(self): @@ -88,19 +84,28 @@ def compare_name(self, other_model): class Shape: - def __init__(self, name, description, node: Node, shapes_graph: Graph): - self.name = name - self.description = description - self._properties = [] + # define default values + name: str = None + description: str = None + + def __init__(self, node: Node, shapes_graph: Graph): + + # store the shape node self._node = node + # store the shapes graph self._shapes_graph = shapes_graph + # initialize the properties + self._properties = [] # create a graph for the shape logger.debug("Initializing graph for the shape: %s" % node) shape_graph = Graph() shape_graph += shapes_graph.triples((node, None, None)) + # inject attributes of the shape to the object + inject_attributes(shape_graph, self) + # Define the property predicate predicate = URIRef(SHACL_NS + "property") @@ -163,32 +168,17 @@ def load(cls, shapes_path: Union[str, Path], publicID: str = None) -> Dict[str, shapes_graph.parse(shapes_path, format="turtle", publicID=publicID) logger.debug("Shapes graph: %s" % shapes_graph) - # query the graph for the shapes - query = """ - PREFIX sh: - SELECT ?shape ?shapeName ?shapeDescription - ?propertyName ?propertyDescription - ?propertyPath ?propertyGroup ?propertyOrder - WHERE { - ?shape a sh:NodeShape ; - sh:name ?shapeName ; - sh:description ?shapeDescription . - } - """ - - logger.debug("Performing query: %s" % query) - results = shapes_graph.query(query) - logger.debug("Query results: %s" % results) - + # extract shapeNode triples from the shapes_graph + shapeNode = URIRef(SHACL_NS + "NodeShape") + shapes_nodes = shapes_graph.triples((None, RDF.type, shapeNode)) + logger.debug("Shapes nodes: %s" % shapes_nodes) + # create a shape object for each shape node shapes: Dict[str, Shape] = {} - for row in results: - shape = shapes.get(row.shapeName, None) - if shape is None: - shape = Shape(row.shapeName, row.shapeDescription, - row.shape, shapes_graph) - shapes[row.shapeName] = shape - - logger.debug("Loaded shapes: %s" % shapes) + for shape_node, _, _ in shapes_nodes: + logger.debug(f"Processing Shape Node: {shape_node}") + shape = Shape(shape_node, shapes_graph) + shapes[str(shape).replace(publicID, "")] = shape + return shapes diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index f9fe90fa..c849ff85 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -19,8 +19,8 @@ def __init__(self, path: Path): self._shape = shape super().__init__(type, profile, - shape.name.value if shape.name else "", - shape.description.value if shape.description else "", + shape.name if shape.name else "", + shape.description if shape.description else "", path) # init checks self._checks = self.__init_checks__() diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py new file mode 100644 index 00000000..57d96814 --- /dev/null +++ b/rocrate_validator/requirements/shacl/utils.py @@ -0,0 +1,43 @@ +import logging + +from rdflib import Graph, Namespace +from rdflib.term import Node + +from rocrate_validator.constants import RDF_SYNTAX_NS + +# set up logging +logger = logging.getLogger(__name__) + + +def build_node_subgraph(graph: Graph, node: Node) -> Graph: + shape_graph = Graph() + shape_graph += graph.triples((node, None, None)) + + # add BNodes + for _, _, o in shape_graph: + shape_graph += graph.triples((o, None, None)) + + # Use the triples method to get all triples that are part of a list + RDF = Namespace(RDF_SYNTAX_NS) + first_predicate = RDF.first + rest_predicate = RDF.rest + shape_graph += graph.triples((None, first_predicate, None)) + shape_graph += graph.triples((None, rest_predicate, None)) + for _, _, object in shape_graph: + shape_graph += graph.triples((object, None, None)) + + # return the subgraph + return shape_graph + + +def inject_attributes(node_graph: Graph, obj: object) -> object: + # inject attributes of the shape property + for node, p, o in node_graph: + predicate_as_string = p.toPython() + logger.debug(f"Processing {predicate_as_string} of property graph {node}") + if predicate_as_string.startswith("http://www.w3.org/ns/shacl#"): + property_name = predicate_as_string.split("#")[-1] + setattr(obj, property_name, o.toPython()) + + # return the object + return obj From dcfc05635ecb28cf258a5ce333a76430c0dc1480 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 15:05:36 +0100 Subject: [PATCH 144/902] =?UTF-8?q?=F0=9F=90=9B=20fix(core):=20fix=20missi?= =?UTF-8?q?ng=20publicID=20parameter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models.py | 8 ++++---- rocrate_validator/services.py | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 9c610677..7294d4dd 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -240,19 +240,19 @@ def __str__(self) -> str: return self.name @staticmethod - def load(path: Union[str, Path]) -> Profile: + def load(path: Union[str, Path], publicID: str = None) -> Profile: # if the path is a string, convert it to a Path if isinstance(path, str): path = Path(path) # check if the path is a directory assert path.is_dir(), f"Invalid profile path: {path}" # create a new profile - profile = Profile(name=path.name, path=path) + profile = Profile(name=path.name, path=path, publicID=publicID) logger.debug("Loaded profile: %s", profile) return profile @staticmethod - def load_profiles(profiles_path: Union[str, Path]) -> Dict[str, Profile]: + def load_profiles(profiles_path: Union[str, Path], publicID: str = None) -> Dict[str, Profile]: # if the path is a string, convert it to a Path if isinstance(profiles_path, str): profiles_path = Path(profiles_path) @@ -265,7 +265,7 @@ def load_profiles(profiles_path: Union[str, Path]) -> Dict[str, Profile]: logger.debug("Checking profile path: %s %s %r", profile_path, profile_path.is_dir(), IGNORED_PROFILE_DIRECTORIES) if profile_path.is_dir() and profile_path not in IGNORED_PROFILE_DIRECTORIES: - profile = Profile.load(profile_path) + profile = Profile.load(profile_path, publicID=publicID) profiles[profile.name] = profile return profiles diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 56557abf..ee31eaef 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -55,22 +55,23 @@ def validate( return result -def get_profiles(profiles_path: str = "./profiles") -> Dict[str, Profile]: +def get_profiles(profiles_path: str = "./profiles", publicID: str = None) -> Dict[str, Profile]: """ Load the profiles from the given path """ - profiles = Profile.load_profiles(profiles_path) + profiles = Profile.load_profiles(profiles_path, publicID=publicID) logger.debug("Profiles loaded: %s", profiles) return profiles -def get_profile(profiles_path: str = "./profiles", profile_name: str = "ro-crate") -> Profile: +def get_profile(profiles_path: str = "./profiles", + profile_name: str = "ro-crate", publicID: str = None) -> Profile: """ Load the profiles from the given path """ profile_path = f"{profiles_path}/{profile_name}" if not Path(profiles_path).exists(): raise FileNotFoundError(f"Profile not found: {profile_path}") - profile = Profile.load(f"{profiles_path}/{profile_name}") + profile = Profile.load(f"{profiles_path}/{profile_name}", publicID=publicID) logger.debug("Profile loaded: %s", profile) return profile From 5de1665c376e34cd277a1541e5b77c4605061bb7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 15:11:44 +0100 Subject: [PATCH 145/902] refactor(core): :recycle: minor changes --- rocrate_validator/models.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 7294d4dd..c1fe1349 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -111,11 +111,13 @@ class Severity(RequirementLevels): class Profile: def __init__(self, name: str, path: Path = None, - requirements: Set[Requirement] = None): + requirements: Set[Requirement] = None, + publicID: str = None): self._path = path self._name = name self._description = None self._requirements = requirements if requirements else [] + self._publicID = publicID @property def path(self): @@ -129,6 +131,10 @@ def name(self): def readme_file_path(self) -> Path: return self.path / DEFAULT_PROFILE_README_FILE + @property + def publicID(self) -> str: + return self._publicID + @property def description(self) -> str: if not self._description: @@ -163,7 +169,8 @@ def load_requirements(self) -> List[Requirement]: for file in sorted(files, key=lambda x: (not x.endswith('.py'), x)): requirement_path = requirement_root / file for requirement in Requirement.load( - self, RequirementLevels.get(requirement_level), requirement_path): + self, RequirementLevels.get(requirement_level), + requirement_path, publicID=self.publicID): req_id += 1 requirement._order_number = req_id self.add_requirement(requirement) @@ -250,7 +257,6 @@ def load(path: Union[str, Path], publicID: str = None) -> Profile: profile = Profile(name=path.name, path=path, publicID=publicID) logger.debug("Loaded profile: %s", profile) return profile - @staticmethod def load_profiles(profiles_path: Union[str, Path], publicID: str = None) -> Dict[str, Profile]: # if the path is a string, convert it to a Path @@ -574,7 +580,10 @@ def __str__(self): return self.name @staticmethod - def load(profile: Profile, requirement_type: RequirementType, file_path: Path) -> List[Requirement]: + def load(profile: Profile, + requirement_type: RequirementType, + file_path: Path, + publicID: str = None) -> List[Requirement]: # initialize the set of requirements requirements = [] @@ -594,7 +603,8 @@ def load(profile: Profile, requirement_type: RequirementType, file_path: Path) - # from rocrate_validator.requirements.shacl.checks import SHACLCheck from rocrate_validator.requirements.shacl.requirements import \ SHACLRequirement - shapes_requirements = SHACLRequirement.load(profile, requirement_type, file_path) + shapes_requirements = SHACLRequirement.load(profile, requirement_type, + file_path, publicID=publicID) logger.debug("Loaded SHACL requirements: %r" % shapes_requirements) requirements.extend(shapes_requirements) logger.debug("Added Requirement: %r" % shapes_requirements) From cc0f1179e5b8391700af79c9c5c9b59b0b4c5891 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 15:14:06 +0100 Subject: [PATCH 146/902] =?UTF-8?q?=F0=9F=93=9D=20docs(shacl):=20add=20som?= =?UTF-8?q?e=20method=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/requirements/shacl/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 00d23e70..ef905e5f 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -122,24 +122,30 @@ def __init__(self, node: Node, shapes_graph: Graph): @property def node(self): + """Return the node of the shape""" return self._node @property def shape_graph(self): + """Return the subgraph of the shape""" return self._shape_graph @property def shapes_graph(self): + """Return the graph of the shapes which contains the shape""" return self._shapes_graph @property def properties(self): + """Return the properties of the shape""" return self._properties.copy() def get_properties(self) -> List[ShapeProperty]: + """Return the properties of the shape""" return self._properties.copy() def get_property(self, name) -> ShapeProperty: + """Return the property of the shape with the given name""" for prop in self._properties: if prop.name == name: return prop From 1c80c2c48b54e015a562394e55a081a0da1f713a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 15:47:49 +0100 Subject: [PATCH 147/902] refactor(core): :wastebasket: clean up --- rocrate_validator/models.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index c1fe1349..aada8e63 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -257,6 +257,7 @@ def load(path: Union[str, Path], publicID: str = None) -> Profile: profile = Profile(name=path.name, path=path, publicID=publicID) logger.debug("Loaded profile: %s", profile) return profile + @staticmethod def load_profiles(profiles_path: Union[str, Path], publicID: str = None) -> Dict[str, Profile]: # if the path is a string, convert it to a Path @@ -857,15 +858,6 @@ def __init__(self, 'publicID': rocrate_path, **kwargs, } - # self.advanced = advanced - # self.inference = inference - # self.inplace = inplace - # self.abort_on_first = abort_on_first - # self.allow_infos = allow_infos - # self.allow_warnings = allow_warnings - # self.serialization_output_path = serialization_output_path - # self.serialization_output_format = serialization_output_format - # self.kwargs = kwargs # reference to the data graph self._data_graph = None From c31b834ee8ccab9fc514155fcb8ba602d6cb40b6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 17:04:37 +0100 Subject: [PATCH 148/902] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor(utils)?= =?UTF-8?q?:=20add=20log=20related=20with=20the=20injection=20of=20obj=20p?= =?UTF-8?q?roperties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/requirements/shacl/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index 57d96814..a169f368 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -30,7 +30,7 @@ def build_node_subgraph(graph: Graph, node: Node) -> Graph: return shape_graph -def inject_attributes(node_graph: Graph, obj: object) -> object: +def inject_attributes(node_graph: Graph, obj: object) -> object: # inject attributes of the shape property for node, p, o in node_graph: predicate_as_string = p.toPython() @@ -38,6 +38,7 @@ def inject_attributes(node_graph: Graph, obj: object) -> object: if predicate_as_string.startswith("http://www.w3.org/ns/shacl#"): property_name = predicate_as_string.split("#")[-1] setattr(obj, property_name, o.toPython()) + logger.debug("Injecting attribute %s: %s", property_name, o.toPython()) # return the object return obj From 0636a62be5c90a2279d3cd99724a5d78152e50e0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 17:08:23 +0100 Subject: [PATCH 149/902] fix(shacl): :bug: fix SHACLCheck initialisation --- rocrate_validator/requirements/shacl/checks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index c9fa0fb2..b6bc7be3 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -19,10 +19,11 @@ def __init__(self, requirement: Requirement, shapeProperty: ShapeProperty = None) -> None: self._shapeProperty = shapeProperty - + logger.debug(f"ShapeProperty: {shapeProperty.name} - {shapeProperty.description}") super().__init__(requirement, shapeProperty.name if shapeProperty and shapeProperty.name else None, + self.check, shapeProperty.description if shapeProperty and shapeProperty.description else None) From 2a765246dcf0cdbae37595e7cd41a62cd1967f40 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 17:10:15 +0100 Subject: [PATCH 150/902] fix(shacl): :bug: store graph before properties init --- rocrate_validator/requirements/shacl/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index ef905e5f..fd1caa81 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -112,14 +112,14 @@ def __init__(self, node: Node, shapes_graph: Graph): # Use the triples method to get all triples with the particular predicate first_triples = shape_graph.triples((None, predicate, None)) + # store the graph + self._shape_graph = shape_graph + # For each triple from the first call, get all triples whose subject is the object of the first triple for _, _, object in first_triples: shape_graph += shapes_graph.triples((object, None, None)) self._properties.append(ShapeProperty(self, object)) - # store the graph - self._shape_graph = shape_graph - @property def node(self): """Return the node of the shape""" From 1545d21b32ecffbcd5ba7d1ee0ead1d8c0c27ead Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 17:20:37 +0100 Subject: [PATCH 151/902] refactor(core): :recycle: remove useless replace --- rocrate_validator/requirements/shacl/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index fd1caa81..6f8f3135 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -183,7 +183,7 @@ def load(cls, shapes_path: Union[str, Path], publicID: str = None) -> Dict[str, for shape_node, _, _ in shapes_nodes: logger.debug(f"Processing Shape Node: {shape_node}") shape = Shape(shape_node, shapes_graph) - shapes[str(shape).replace(publicID, "")] = shape + shapes[str(shape)] = shape return shapes From 9ead6947f23f0f3c1f265f0c0fa6a1ea605f3309 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 18:39:51 +0100 Subject: [PATCH 152/902] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor(shacl)?= =?UTF-8?q?:=20remove=20obsolete=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/requirements/shacl/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 6f8f3135..61c839b4 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -33,8 +33,6 @@ def __init__(self, # store the node self._node = shape_property_node - # TODO: refactor moving URIRef to constants - # create a graph for the shape property shapes_graph = shape.shapes_graph shape_property_graph = Graph() From be8d7c74770ca8656354278783e830bbb9f6100b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 19:16:09 +0100 Subject: [PATCH 153/902] =?UTF-8?q?=F0=9F=90=9B=20fix(shacl):=20ensure=20i?= =?UTF-8?q?njection=20of=20right=20properties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../requirements/shacl/models.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 61c839b4..57608155 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -35,16 +35,20 @@ def __init__(self, # create a graph for the shape property shapes_graph = shape.shapes_graph - shape_property_graph = Graph() - shape_property_graph += shapes_graph.triples((shape.node, None, None)) - shape_property_graph += shapes_graph.triples((shape_property_node, None, None)) + shape_graph = Graph() + shape_graph += shapes_graph.triples((shape.node, None, None)) + shape_property_attributes_graph = Graph() + shape_property_attributes_graph += shapes_graph.triples((shape_property_node, None, None)) + # global shape property graph + shape_property_graph = shape_graph + shape_property_attributes_graph + # remove dangling properties for s, p, o in shape_property_graph: logger.debug(f"Processing {p} of property graph {shape_property_node}") if p == URIRef("http://www.w3.org/ns/shacl#property") and o != shape_property_node: shape_property_graph.remove((s, p, o)) # add BNodes - for s, p, o in shape_property_graph: + for s, p, o in shape_property_attributes_graph: shape_property_graph += shapes_graph.triples((o, None, None)) # Use the triples method to get all triples that are part of a list @@ -60,7 +64,7 @@ def __init__(self, self._shape_property_graph = shape_property_graph # inject attributes of the shape property to the object - inject_attributes(shape_property_graph, self) + inject_attributes(shape_property_attributes_graph, self) @property def shape(self): @@ -100,19 +104,17 @@ def __init__(self, node: Node, shapes_graph: Graph): logger.debug("Initializing graph for the shape: %s" % node) shape_graph = Graph() shape_graph += shapes_graph.triples((node, None, None)) + # store the graph + self._shape_graph = shape_graph # inject attributes of the shape to the object inject_attributes(shape_graph, self) + # Initialize the properties # Define the property predicate predicate = URIRef(SHACL_NS + "property") - # Use the triples method to get all triples with the particular predicate first_triples = shape_graph.triples((None, predicate, None)) - - # store the graph - self._shape_graph = shape_graph - # For each triple from the first call, get all triples whose subject is the object of the first triple for _, _, object in first_triples: shape_graph += shapes_graph.triples((object, None, None)) From b952a6014a004fc06e4c3c81b693868c979fb556 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 19:19:48 +0100 Subject: [PATCH 154/902] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor(core):?= =?UTF-8?q?=20updated=20=5F=5Feq=5F=5F=20and=20=5F=5Fhash=5F=5F=20methods?= =?UTF-8?q?=20in=20Python=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rocrate_validator/models.py | 13 +++++++++++++ rocrate_validator/requirements/shacl/models.py | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index aada8e63..bf50fb24 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -393,6 +393,19 @@ def __do_check__(self, context: ValidationContext) -> bool: # Perform the check return self.check() + def __eq__(self, other: RequirementCheck): + if not isinstance(other, RequirementCheck): + raise ValueError(f"Cannot compare RequirementCheck with {type(other)}") + return self.requirement == other.requirement and self.name == other.name + + def __ne__(self, other: RequirementCheck): + if not isinstance(other, RequirementCheck): + raise ValueError(f"Cannot compare RequirementCheck with {type(other)}") + return self.requirement != other.requirement or self.name != other.name + + def __hash__(self): + return hash((self.requirement, self.name or "")) + class Requirement(ABC): diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 57608155..7f08d75e 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -84,6 +84,15 @@ def compare_shape(self, other_model): def compare_name(self, other_model): return self.name == other_model.name + def __eq__(self, __value: object) -> bool: + if not isinstance(__value, type(self)): + raise ValueError("Invalid comparison") + return self._shape == getattr(__value, '_shape', None)\ + and self._node == getattr(__value, '_node', None) + + def __hash__(self): + return hash(self._node) + class Shape: @@ -160,10 +169,10 @@ def __repr__(self): def __eq__(self, other): if not isinstance(other, Shape): return False - return self.name == other.name + return self._node == other._node def __hash__(self): - return hash(self.name) + return hash(self._node) @classmethod def load(cls, shapes_path: Union[str, Path], publicID: str = None) -> Dict[str, Shape]: @@ -183,7 +192,8 @@ def load(cls, shapes_path: Union[str, Path], publicID: str = None) -> Dict[str, for shape_node, _, _ in shapes_nodes: logger.debug(f"Processing Shape Node: {shape_node}") shape = Shape(shape_node, shapes_graph) - shapes[str(shape)] = shape + if str(shape): + shapes[str(shape)] = shape return shapes From 06ae02ad8db387f1c48538115c62ed05465c9d51 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 19:20:49 +0100 Subject: [PATCH 155/902] =?UTF-8?q?=F0=9F=92=84=20ui(cli):=20remove=20[]?= 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 2ecdeb3f..d4f4e7ff 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -165,7 +165,7 @@ def __print_validation_result__( issue_color = get_severity_color(check.severity) console.print( f"{' '*4}- " - f"[[magenta]{check.name}[/magenta]]: {check.description}") + f"[magenta]{check.name}[/magenta]: {check.description}") console.print(f"\n{' '*6}Detected issues:", style="white bold") for issue in check.get_issues(): console.print( From 91fa4bfe379fe309d723a93a833f1158b3f054b2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 19:24:27 +0100 Subject: [PATCH 156/902] =?UTF-8?q?=F0=9F=9A=A8=20fix-lint(cli):=20remove?= =?UTF-8?q?=20useless=20escape?= 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 d4f4e7ff..16aa8c2d 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -121,7 +121,7 @@ def validate(ctx, except Exception as e: console.print( - f"\n\n[bold]\[[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", + f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", style="white", ) if logger.isEnabledFor(logging.DEBUG): From 81595100edefa93a0bc322b9008574b9c6364659 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 22 Mar 2024 19:28:49 +0100 Subject: [PATCH 157/902] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor(profil?= =?UTF-8?q?es/ro-crate):=20update=20base=20shape?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- profiles/ro-crate/must/1_file-descriptor.ttl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/profiles/ro-crate/must/1_file-descriptor.ttl b/profiles/ro-crate/must/1_file-descriptor.ttl index 00dc5c14..b1ac9206 100644 --- a/profiles/ro-crate/must/1_file-descriptor.ttl +++ b/profiles/ro-crate/must/1_file-descriptor.ttl @@ -7,14 +7,18 @@ @prefix xsd: . -:FileDescriptorShouldExists +:FileDescriptorExistence a sh:NodeShape ; - sh:name "RO-Crate Metadata File Descriptor MUST exists" ; + sh:name "RO-Crate Metadata File Descriptor MUST exist" ; sh:description "The root of the document MUST have a node with ID ro-crate-metadata.json" ; sh:targetNode :ro-crate-metadata.json ; sh:property [ a sh:PropertyShape ; + sh:description "Check if the RO-Crate metadata file exists" ; sh:path rdf:type ; sh:minCount 1 ; + # sh:order 1 ; + ] . + ] . From f20cfbd9a188f67dabfd2073e71f361f96747844 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 23 Mar 2024 12:17:26 +0100 Subject: [PATCH 158/902] refactor(profiles): :memo: add message --- profiles/ro-crate/must/1_file-descriptor.ttl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/profiles/ro-crate/must/1_file-descriptor.ttl b/profiles/ro-crate/must/1_file-descriptor.ttl index b1ac9206..cc09094c 100644 --- a/profiles/ro-crate/must/1_file-descriptor.ttl +++ b/profiles/ro-crate/must/1_file-descriptor.ttl @@ -14,11 +14,13 @@ sh:targetNode :ro-crate-metadata.json ; sh:property [ a sh:PropertyShape ; + sh:name "Check metadata file existence" ; sh:description "Check if the RO-Crate metadata file exists" ; sh:path rdf:type ; sh:minCount 1 ; - # sh:order 1 ; + sh:message "The RO-Crate metadata file descriptor MUST exist" ; ] . + ] . From 1e8c1d90bd66f97575a13b90efbd4ab0407bbce1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 11:17:22 +0100 Subject: [PATCH 159/902] fix(core): :bug: use concrete check py class --- rocrate_validator/requirements/python/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index 26ef163f..dbadb0ad 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -33,7 +33,7 @@ def __init_checks__(self): except Exception: check_name = name.strip() check_description = member.__doc__.strip() if member.__doc__ else "" - check = RequirementCheck(self, check_name, member, check_description) + check = self.requirement_check_class(self, check_name, member, check_description) self._checks.append(check) logger.debug("Added check: %s %r", check_name, check) # return the checks From 692e1efd4f6abc307802fb1944cdbe9566635e5f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 11:18:13 +0100 Subject: [PATCH 160/902] fix(core): :bug: safe extraction of public ID from path --- rocrate_validator/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index bf50fb24..b7f9f986 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -929,9 +929,10 @@ def profile(self) -> Profile: @property def publicID(self) -> str: - if not self.rocrate_path.endswith("/"): - return f"{self.rocrate_path}/" - return self.rocrate_path + path = str(self.rocrate_path) + if not path.endswith("/"): + return f"{path}/" + return path @classmethod def load_graph_of_shapes(cls, requirement: Requirement, publicID: str = None) -> Graph: From a1d8ca19a182aac961a96412440f51121146f7f5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 25 Mar 2024 10:41:32 +0100 Subject: [PATCH 161/902] feat(shacl): :sparkles: expose shape property on SHACL requirement --- rocrate_validator/requirements/shacl/requirements.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index c849ff85..e5cd3cb7 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -42,6 +42,10 @@ def __init_checks__(self): # return checks return checks + @property + def shape(self) -> Shape: + return self._shape + @staticmethod def load(profile: Profile, requirement_type: RequirementType, file_path: Path, publicID: str = None) -> List[Requirement]: From efe70eae9264b13f100c7584b53877804cc89d1a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 25 Mar 2024 10:42:26 +0100 Subject: [PATCH 162/902] refactor(shacl): :mute: remove log --- rocrate_validator/requirements/shacl/checks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index b6bc7be3..1b3d0699 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -19,7 +19,6 @@ def __init__(self, requirement: Requirement, shapeProperty: ShapeProperty = None) -> None: self._shapeProperty = shapeProperty - logger.debug(f"ShapeProperty: {shapeProperty.name} - {shapeProperty.description}") super().__init__(requirement, shapeProperty.name if shapeProperty and shapeProperty.name else None, From 04b6e66b670779ec33868ad252ddf2ee65af4833 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 25 Mar 2024 10:44:36 +0100 Subject: [PATCH 163/902] fix(shacl): :bug: use shape graph if no shape property --- rocrate_validator/requirements/shacl/checks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 1b3d0699..95d12313 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -49,7 +49,8 @@ def check(self): data_graph = self.validator.data_graph # constraint the shapes graph to the current property shape - shapes_graph = self.shapeProperty.shape_property_graph + shapes_graph = self.shapeProperty.shape_property_graph \ + if self.shapeProperty else self.requirement.shape.shape_graph from .validator import Validator as SHACLValidator shacl_validator = SHACLValidator( From a7026d5a1cc296f1b90de21ed2e6fce2e135e311 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 25 Mar 2024 10:45:54 +0100 Subject: [PATCH 164/902] fix(shacl): :bug: set publicID when loading ontologies --- rocrate_validator/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index b7f9f986..7d28501b 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -971,7 +971,8 @@ def load_ontologies_graph(self): # load the graph of ontologies ontologies_graph = Graph() if self.ontologies_path: - ontologies_graph.parse(self.ontologies_path, format="ttl") + ontologies_graph.parse(self.ontologies_path, format="ttl", + publicID=self.publicID) return ontologies_graph def get_ontologies_graph(self, refresh: bool = False): From 706b13b1123dd24594278b617a059067214cb750 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 25 Mar 2024 10:54:52 +0100 Subject: [PATCH 165/902] refactor(profiles): :wastebasket: remove old profiles from develop --- profiles/ro-crate/must/1_file-descriptor.ttl | 26 -- .../ro-crate-metadata.json | 317 ------------------ 2 files changed, 343 deletions(-) delete mode 100644 profiles/ro-crate/must/1_file-descriptor.ttl delete mode 100644 tests/data/crates/invalid/0_file_descriptor/missing_file_descriptor/ro-crate-metadata.json diff --git a/profiles/ro-crate/must/1_file-descriptor.ttl b/profiles/ro-crate/must/1_file-descriptor.ttl deleted file mode 100644 index cc09094c..00000000 --- a/profiles/ro-crate/must/1_file-descriptor.ttl +++ /dev/null @@ -1,26 +0,0 @@ -@prefix : <./> . -@prefix dct: . -@prefix rdf: . -@prefix schema_org: . -@prefix sh: . -@prefix xml1: . -@prefix xsd: . - - -:FileDescriptorExistence - a sh:NodeShape ; - sh:name "RO-Crate Metadata File Descriptor MUST exist" ; - sh:description "The root of the document MUST have a node with ID ro-crate-metadata.json" ; - sh:targetNode :ro-crate-metadata.json ; - sh:property [ - a sh:PropertyShape ; - sh:name "Check metadata file existence" ; - sh:description "Check if the RO-Crate metadata file exists" ; - sh:path rdf:type ; - sh:minCount 1 ; - sh:message "The RO-Crate metadata file descriptor MUST exist" ; - ] . - - - ] . - diff --git a/tests/data/crates/invalid/0_file_descriptor/missing_file_descriptor/ro-crate-metadata.json b/tests/data/crates/invalid/0_file_descriptor/missing_file_descriptor/ro-crate-metadata.json deleted file mode 100644 index e6bf22f4..00000000 --- a/tests/data/crates/invalid/0_file_descriptor/missing_file_descriptor/ro-crate-metadata.json +++ /dev/null @@ -1,317 +0,0 @@ -{ - "@context": "https://w3id.org/ro/crate/1.1/context", - "@graph": [ - { - "@id": "./", - "@type": "Dataset", - "creator": [ - { - "@id": "#macklin" - }, - { - "@id": "#getz" - }, - { - "@id": "#saglam" - }, - { - "@id": "#wang" - }, - { - "@id": "#heiland" - } - ], - "datePublished": "2021-11-09T00:51:55.837325", - "description": "This model simulates replication dynamics of SARS-CoV-2 (coronavirus / COVID19) in a layer of epithelium and several submodels (such as single-cell response, pyroptosis death model, tissue-damage model, lymph node model and immune response). It is being iteratively prototyped and refined with community support", - "hasPart": [ - { - "@id": "changes.md" - }, - { - "@id": "design/" - }, - { - "@id": "LICENSE" - }, - { - "@id": "math/" - }, - { - "@id": "PhysiCell/" - }, - { - "@id": "PhysiCell_V.1.6.1.zip" - }, - { - "@id": "PhysiCell_V.1.7.0.zip" - }, - { - "@id": "PhysiCell_V.1.7.1.zip" - }, - { - "@id": "PhysiCell_V.1.9.0.zip" - }, - { - "@id": "workflow/retropath.knime" - }, - { - "@id": "lots_of_little_files/" - }, - { - "@id": "README.md" - } - ], - "isBasedOn": "https://zenodo.org/record/5595136#.YYlJ9BB_phF", - "name": "pc4covid19/COVID19: Version 0.5.0: draft v5 prototype" - }, - { - "@id": "changes.md", - "@type": "File" - }, - { - "@id": "design/", - "@type": "Dataset" - }, - { - "@id": "lots_of_little_files/", - "@type": "Dataset", - "name": "Too many files", - "description": "This directory contains many small files, that we're not going to describe in detail.", - "distribution": { - "@id": "http://example.com/downloads/2020/lots_of_little_files.zip" - } - }, - { - "@id": "http://example.com/downloads/2020/lots_of_little_files.zip", - "@type": "DataDownload", - "encodingFormat": "application/zip", - "contentSize": "82818928" - }, - { - "@id": "LICENSE", - "@type": "File" - }, - { - "@id": "math/", - "@type": "Dataset", - "author": { "@id": "https://orcid.org/0000-0001-6121-5409" }, - "contentLocation": { - "@id": "http://sws.geonames.org/8152662/" - }, - "temporalCoverage": "1950/1975" - }, - { - "@id": "http://sws.geonames.org/8152662/", - "name": "Catalina Park" - }, - { - "@id": "https://orcid.org/0000-0001-6121-5409", - "@type": "Person", - "contactPoint": { - "@id": "mailto:tim.luckett@uts.edu.au" - }, - "familyName": "Luckett", - "givenName": "Tim", - "identifier": "https://orcid.org/0000-0001-6121-5409", - "name": "Tim Luckett" - }, - { - "@id": "mailto:tim.luckett@uts.edu.au", - "@type": "ContactPoint", - "contactType": "customer service", - "email": "tim.luckett@uts.edu.au", - "identifier": "tim.luckett@uts.edu.au", - "url": "https://orcid.org/0000-0001-6121-5409" - }, - { - "@id": "PhysiCell/", - "@type": "Dataset", - "publisher": { "@id": "https://ror.org/03f0f6041" }, - "author": { "@id": "https://orcid.org/0000-0002-3545-944X" }, - "citation": { "@id": "https://doi.org/10.1109/TCYB.2014.2386282" }, - "funder": { - "@id": "https://eresearch.uts.edu.au/projects/provisioner" - } - }, - { - "@id": "https://eresearch.uts.edu.au/projects/provisioner", - "@type": "Organization", - "description": "The University of Technology Sydney Provisioner project is ...", - "funder": [ - { - "@id": "https://ror.org/03f0f6041" - }, - { - "@id": "https://ands.org.au" - } - ], - "identifier": "https://eresearch.uts.edu.au/projects/provisioner", - "name": "Provisioner" - }, - { - "@id": "https://ror.org/03f0f6041", - "@type": "Organization", - "identifier": "https://ror.org/03f0f6041", - "name": "University of Technology Sydney" - }, - { - "@id": "https://ands.org.au", - "@type": "Organization", - "description": "The core purpose of the Australian National Data Service (ANDS) is ...", - "identifier": "https://ands.org.au", - "name": "Australian National Data Service" - }, - { - "@id": "https://doi.org/10.1109/TCYB.2014.2386282", - "@type": "ScholarlyArticle", - "identifier": "https://doi.org/10.1109/TCYB.2014.2386282", - "issn": "2168-2267", - "name": "Topic Model for Graph Mining", - "journal": "IEEE Transactions on Cybernetics", - "datePublished": "2015" - }, - { - "@id": "https://ror.org/03f0f6041", - "@type": "Organization", - "name": "University of Technology Sydney" - }, - { - "@id": "https://orcid.org/0000-0002-3545-944X", - "@type": "Person", - "affiliation": { "@id": "https://ror.org/03f0f6041" }, - "email": "peter.sefton@uts.edu.au", - "name": "Peter Sefton" - }, - { - "@id": "PhysiCell_V.1.6.1.zip", - "@type": "File", - "license": { - "@id": "https://creativecommons.org/licenses/by/4.0/" - } - }, - { - "@id": "https://creativecommons.org/licenses/by/4.0/", - "@type": "CreativeWork", - "name": "CC BY 4.0", - "description": "Creative Commons Attribution 4.0 International License" - }, - { - "@id": "PhysiCell_V.1.7.0.zip", - "@type": "File" - }, - { - "@id": "PhysiCell_V.1.7.1.zip", - "@type": "File" - }, - { - "@id": "PhysiCell_V.1.9.0.zip", - "@type": "File", - "encodingFormat": ["text/plain", { "@id": "some_extension.md" }] - }, - { - "@id": "some_extension.md", - "@type": "File", - "name": "Common Workflow Language (CWL) Workflow Description, v1.0.2" - }, - { - "@id": "README.md", - "@type": "File", - "contentSize": null, - "encodingFormat": ["text/plain", { "@id": "some_extension/" }], - "url": "https://github.com/pc4covid19/COVID19/tree/0.5.0/README.md" - }, - { - "@id": "some_extension/", - "@type": "Dataset", - "name": "Common Workflow Language (CWL) Workflow Description, v1.0.2" - }, - { - "@id": "workflow/retropath.knime", - "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], - "author": { "@id": "#thomas" }, - "name": "RetroPath Knime workflow", - "description": "Retrosynthesis workflow calculating chemical reactions", - "license": { "@id": "https://spdx.org/licenses/CC-BY-NC-SA-4.0" }, - "programmingLanguage": { "@id": "#knime" } - }, - { - "@id": "https://spdx.org/licenses/CC-BY-NC-SA-4.0", - "@type": "CreativeWork", - "name": "CC BY 4.0", - "description": "Creative Commons Attribution 4.0 International License" - }, - { - "@id": "https://omeka.uws.edu.au/farmstofreeways/api/items/383", - "@type": ["RepositoryObject", "ImageObject"], - "identifier": ["ftf_photo_stapleton1"], - "interviewee": [ - { - "@id": "https://omeka.uws.edu.au/farmstofreeways/api/items/595" - } - ], - "description": ["Photo of Eugenie Stapleton inside her home"], - "hasFile": [ - { - "@id": "files/383/original_c0f1189ec13ca936e8f556161663d4ba.jpg" - }, - { - "@id": "files/383/fullsize_c0f1189ec13ca936e8f556161663d4ba.jpg" - }, - { - "@id": "files/383/thumbnail_c0f1189ec13ca936e8f556161663d4ba.jpg" - }, - { - "@id": "files/383/square_thumbnail_c0f1189ec13ca936e8f556161663d4ba.jpg" - } - ], - "thumbnail": [ - { - "@id": "files/383/thumbnail_cddd0f1189ec13ca936e8f556161663d4ba.jpg" - } - ], - "name": ["Photo of Eugenie Stapleton 1"], - "copyright": ["Copyright University of Western Sydney 2015"] - }, - { - "@type": "File", - "@id": "files/384/original_2ebbe681aa6ec138776343974ce8a3dd.jpg" - }, - { - "@type": "File", - "@id": "files/384/fullsize_2ebbe681aa6ec138776343974ce8a3dd.jpg" - }, - { - "@type": "File", - "@id": "files/384/thumbnail_2ebbe681aa6ec138776343974ce8a3dd.jpg" - }, - { - "@type": "File", - "@id": "files/384/square_thumbnail_2ebbe681aa6ec138776343974ce8a3dd.jpg" - }, - { - "@id": "#macklin", - "@type": "Person", - "name": "Paul Macklin" - }, - { - "@id": "#getz", - "@type": "Person", - "name": "Michael-Getz" - }, - { - "@id": "#saglam", - "@type": "Person", - "name": "Ali Sinan Saglam" - }, - { - "@id": "#wang", - "@type": "Person", - "name": "Yafei Wang" - }, - { - "@id": "#heiland", - "@type": "Person", - "name": "Randy Heiland" - } - ] -} From e21e6959fd3821a63fc825ed74c99e7f27e79bbb Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Tue, 26 Mar 2024 11:15:30 +0100 Subject: [PATCH 166/902] Fix dependencies and README.md file name --- README => README.md | 0 poetry.lock | 248 +++++++++++++++++++++++++++++--------------- pyproject.toml | 3 +- 3 files changed, 167 insertions(+), 84 deletions(-) rename README => README.md (100%) diff --git a/README b/README.md similarity index 100% rename from README rename to README.md diff --git a/poetry.lock b/poetry.lock index 8c0e10af..bbe7fbe6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "appnope" @@ -22,6 +22,9 @@ files = [ {file = "astroid-3.1.0.tar.gz", hash = "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} + [[package]] name = "asttokens" version = "2.4.1" @@ -40,6 +43,17 @@ six = ">=1.12.0" astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +optional = false +python-versions = "*" +files = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] + [[package]] name = "cffi" version = "1.16.0" @@ -129,6 +143,23 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "colorlog" +version = "6.8.2" +description = "Add colours to the output of Python's logging module." +optional = false +python-versions = ">=3.6" +files = [ + {file = "colorlog-6.8.2-py3-none-any.whl", hash = "sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33"}, + {file = "colorlog-6.8.2.tar.gz", hash = "sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +development = ["black", "flake8", "mypy", "pytest", "types-colorama"] + [[package]] name = "comm" version = "0.2.2" @@ -148,65 +179,68 @@ test = ["pytest"] [[package]] name = "coverage" -version = "7.4.3" +version = "7.4.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, - {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, - {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, - {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, - {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, - {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, - {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, - {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, - {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"}, - {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"}, - {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"}, - {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"}, - {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"}, - {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"}, - {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"}, - {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"}, - {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, - {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, + {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, + {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, + {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, + {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, + {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, + {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, + {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, + {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, + {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, + {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, + {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, + {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, ] +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + [package.extras] toml = ["tomli"] @@ -267,6 +301,20 @@ files = [ graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "executing" version = "2.0.1" @@ -320,13 +368,13 @@ lxml = ["lxml"] [[package]] name = "importlib-metadata" -version = "7.0.2" +version = "7.1.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.0.2-py3-none-any.whl", hash = "sha256:f4bc4c0c070c490abf4ce96d715f68e95923320370efb66143df00199bb6c100"}, - {file = "importlib_metadata-7.0.2.tar.gz", hash = "sha256:198f568f3230878cb1b44fbd7975f87906c22336dba2e4a7f05278c281fbd792"}, + {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, + {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, ] [package.dependencies] @@ -335,7 +383,7 @@ zipp = ">=0.5" [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "iniconfig" @@ -383,38 +431,42 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio [[package]] name = "ipython" -version = "8.22.2" +version = "8.12.3" description = "IPython: Productive Interactive Computing" optional = false -python-versions = ">=3.10" +python-versions = ">=3.8" files = [ - {file = "ipython-8.22.2-py3-none-any.whl", hash = "sha256:3c86f284c8f3d8f2b6c662f885c4889a91df7cd52056fd02b7d8d6195d7f56e9"}, - {file = "ipython-8.22.2.tar.gz", hash = "sha256:2dcaad9049f9056f1fef63514f176c7d41f930daa78d05b82a176202818f2c14"}, + {file = "ipython-8.12.3-py3-none-any.whl", hash = "sha256:b0340d46a933d27c657b211a329d0be23793c36595acf9e6ef4164bc01a1804c"}, + {file = "ipython-8.12.3.tar.gz", hash = "sha256:3910c4b54543c2ad73d06579aa771041b7d5707b033bd488669b4cf544e3b363"}, ] [package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" colorama = {version = "*", markers = "sys_platform == \"win32\""} decorator = "*" jedi = ">=0.16" matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} -prompt-toolkit = ">=3.0.41,<3.1.0" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" pygments = ">=2.4.0" stack-data = "*" -traitlets = ">=5.13.0" +traitlets = ">=5" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} [package.extras] -all = ["ipython[black,doc,kernel,nbconvert,nbformat,notebook,parallel,qtconsole,terminal]", "ipython[test,test-extra]"] +all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] black = ["black"] -doc = ["docrepr", "exceptiongroup", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "stack-data", "typing-extensions"] +doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] nbformat = ["nbformat"] notebook = ["ipywidgets", "notebook"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "testpath"] -test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] +test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] [[package]] name = "isodate" @@ -475,6 +527,7 @@ files = [ ] [package.dependencies] +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" python-dateutil = ">=2.8.2" pyzmq = ">=23.0" @@ -592,13 +645,13 @@ rdflib = ">=6.0.2" [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] @@ -630,6 +683,17 @@ files = [ [package.dependencies] ptyprocess = ">=0.5" +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +optional = false +python-versions = "*" +files = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] + [[package]] name = "platformdirs" version = "4.2.0" @@ -807,13 +871,16 @@ files = [ astroid = ">=3.1.0,<=3.2.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\" and python_version < \"3.12\""}, ] isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} tomlkit = ">=0.10.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] spelling = ["pyenchant (>=3.2,<4.0)"] @@ -846,6 +913,7 @@ files = [ [package.dependencies] flake8 = "6.1.0" +tomli = {version = "*", markers = "python_version < \"3.11\""} [[package]] name = "pyshacl" @@ -864,8 +932,8 @@ importlib-metadata = {version = ">6", markers = "python_version < \"3.12\""} owlrl = ">=6.0.2,<7" packaging = ">=21.3" prettytable = [ - {version = ">=3.7.0", markers = "python_version >= \"3.12\""}, {version = ">=3.5.0", markers = "python_version >= \"3.8\" and python_version < \"3.12\""}, + {version = ">=3.7.0", markers = "python_version >= \"3.12\""}, ] rdflib = {version = ">=6.3.2,<8.0", markers = "python_full_version >= \"3.8.1\""} @@ -889,9 +957,11 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=1.4,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] @@ -1091,19 +1161,20 @@ 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)"] [[package]] name = "rich-click" -version = "1.7.3" +version = "1.7.4" description = "Format click help output nicely with rich" optional = false python-versions = ">=3.7" files = [ - {file = "rich-click-1.7.3.tar.gz", hash = "sha256:bced1594c497dc007ab49508ff198bb437c576d01291c13a61658999066481f4"}, - {file = "rich_click-1.7.3-py3-none-any.whl", hash = "sha256:bc4163d4e2a3361e21c4d72d300eca6eb8896dfc978667923cb1d4937b8769a3"}, + {file = "rich-click-1.7.4.tar.gz", hash = "sha256:7ce5de8e4dc0333aec946113529b3eeb349f2e5d2fafee96b9edf8ee36a01395"}, + {file = "rich_click-1.7.4-py3-none-any.whl", hash = "sha256:e363655475c60fec5a3e16a1eb618118ed79e666c365a36006b107c17c93ac4e"}, ] [package.dependencies] @@ -1155,6 +1226,17 @@ files = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + [[package]] name = "tomlkit" version = "0.12.4" @@ -1236,20 +1318,20 @@ files = [ [[package]] name = "zipp" -version = "3.17.0" +version = "3.18.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" -python-versions = "^3.11" -content-hash = "6e52666b5d164ed9a31fcdcd404e23b0a12c3d3410d611ab7bcb47af1f5b3bc2" +python-versions = "^3.8.1" +content-hash = "d48bc23bf1b3a14257ca8af4fc126aee38b66531e492b5216d31f418485f4e19" diff --git a/pyproject.toml b/pyproject.toml index 528ea7d0..5c468f32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,13 +7,14 @@ readme = "README.md" packages = [{ include = "rocrate_validator", from = "." }] [tool.poetry.dependencies] -python = "^3.11" +python = "^3.8.1" rdflib = "^7.0.0" pyshacl = "^0.25.0" click = "^8.1.7" rich = "^13.7.1" toml = "^0.10.2" rich-click = "^1.7.3" +colorlog = "^6.8" [tool.poetry.group.dev.dependencies] pyproject-flake8 = "^6.1.0" From d8c79866782afdbdda32ce2889fb5ae4978cef54 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 27 Mar 2024 15:48:58 +0100 Subject: [PATCH 167/902] fix(utils): :rotating_light: minor changes to suppress linter warnings --- rocrate_validator/errors.py | 20 +++++++++++++++++--- rocrate_validator/utils.py | 25 +++++++++++++------------ 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 85d93c81..608674a8 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -1,9 +1,12 @@ class OutOfValidationContext(Exception): + """Raised when a validation check is called outside of a validation context.""" + def __init__(self, message: str = None): self._message = message @property def message(self) -> str: + """The error message.""" return self._message def __str__(self): @@ -14,11 +17,14 @@ def __repr__(self): class InvalidSerializationFormat(Exception): - def __init__(self, format: str = None): - self._format = format + """Raised when an invalid serialization format is provided.""" + + def __init__(self, serialization_format: str = None): + self._format = serialization_format @property - def format(self): + def serialization_format(self): + """The invalid serialization format.""" return self._format def __str__(self): @@ -29,6 +35,8 @@ def __repr__(self): class ValidationError(Exception): + """Raised when a validation error occurs.""" + def __init__(self, message, path: str = ".", code: int = -1): self._message = message self._path = path @@ -36,14 +44,17 @@ def __init__(self, message, path: str = ".", code: int = -1): @property def message(self) -> str: + """The error message.""" return self._message @property def path(self) -> str: + """The path where the error occurred.""" return self._path @property def code(self) -> int: + """The error code.""" return self._code def __str__(self): @@ -54,12 +65,15 @@ def __repr__(self): class CheckValidationError(ValidationError): + """Raised when a validation check fails.""" + def __init__(self, check, message, path: str = ".", code: int = -1): super().__init__(message, path, code) self._check = check @property def check(self): + """The check that failed.""" return self._check def __repr__(self): diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 16b8682f..c07b8002 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -54,7 +54,7 @@ def get_file_descriptor_path(rocrate_path: Path) -> Path: return Path(rocrate_path) / constants.ROCRATE_METADATA_FILE -def get_format_extension(format: constants.RDF_SERIALIZATION_FORMATS_TYPES) -> str: +def get_format_extension(serialization_format: constants.RDF_SERIALIZATION_FORMATS_TYPES) -> str: """ Get the file extension for the RDF serialization format @@ -64,15 +64,15 @@ def get_format_extension(format: constants.RDF_SERIALIZATION_FORMATS_TYPES) -> s :raises InvalidSerializationFormat: If the format is not valid """ try: - return constants.RDF_SERIALIZATION_FILE_FORMAT_MAP[format] - except KeyError: - logger.error("Invalid RDF serialization format: %s", format) - raise errors.InvalidSerializationFormat(format) + return constants.RDF_SERIALIZATION_FILE_FORMAT_MAP[serialization_format] + except KeyError as exc: + logger.error("Invalid RDF serialization format: %s", serialization_format) + raise errors.InvalidSerializationFormat(serialization_format) from exc def get_all_files( directory: str = '.', - format: constants.RDF_SERIALIZATION_FORMATS_TYPES = "turtle") -> List[str]: + serialization_format: constants.RDF_SERIALIZATION_FORMATS_TYPES = "turtle") -> List[str]: """ Get all the files in the directory matching the format. @@ -84,10 +84,10 @@ def get_all_files( file_paths = [] # extension - extension = get_format_extension(format) + extension = get_format_extension(serialization_format) # iterate through the directory and subdirectories - for root, dirs, files in os.walk(directory): + for root, _, files in os.walk(directory): # iterate through the files for file in files: # check if the file has a .ttl extension @@ -99,7 +99,7 @@ def get_all_files( def get_graphs_paths( - graphs_dir: str = CURRENT_DIR, format="turtle") -> List[str]: + graphs_dir: str = CURRENT_DIR, serialization_format="turtle") -> List[str]: """ Get the paths to all the graphs in the directory @@ -107,12 +107,12 @@ def get_graphs_paths( :param format: The RDF serialization format :return: A list of graph paths """ - return get_all_files(directory=graphs_dir, format=format) + return get_all_files(directory=graphs_dir, serialization_format=serialization_format) def get_full_graph( graphs_dir: str, - format: constants.RDF_SERIALIZATION_FORMATS_TYPES = "turtle", + serialization_format: constants.RDF_SERIALIZATION_FORMATS_TYPES = "turtle", publicID: str = ".") -> Graph: """ Get the full graph from the directory @@ -123,7 +123,7 @@ def get_full_graph( :return: The full graph """ full_graph = Graph() - graphs_paths = get_graphs_paths(graphs_dir, format=format) + graphs_paths = get_graphs_paths(graphs_dir, serialization_format=serialization_format) for graph_path in graphs_paths: full_graph.parse(graph_path, format="turtle", publicID=publicID) logger.debug("Loaded triples from %s", graph_path) @@ -133,6 +133,7 @@ def get_full_graph( def get_classes_from_file(file_path: Path, filter_class: Optional[Type] = None, class_name_suffix: str = None) -> dict: + """Get all classes in a Python file """ # ensure the file path is a Path object assert file_path, "The file path is required" if not isinstance(file_path, Path): From c5cf3dd222b3e97bf314562f32d9512997352332 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 27 Mar 2024 15:51:32 +0100 Subject: [PATCH 168/902] fix(utils): :sparkles: missing function from previous commits --- rocrate_validator/utils.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index c07b8002..857b9919 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -183,6 +183,27 @@ def get_requirement_name_from_file(file: Path, check_name: Optional[str] = None) return base_name +def get_requirement_class_by_name(requirement_name: str) -> Type: + """ + Dynamically load the module of the class and return the class""" + + # Split the requirement name into module and class + module_name, class_name = requirement_name.rsplit(".", 1) + logger.debug("Module: %r", module_name) + logger.debug("Class: %r", class_name) + + # convert the module name to a path + module_path = module_name.replace(".", "/") + # add the path to the system path + sys.path.insert(0, os.path.dirname(module_path)) + + # Import the module + module = import_module(module_name) + + # Get the class from the module + return getattr(module, class_name) + + def to_camel_case(snake_str: str) -> str: """ Convert a snake case string to camel case From 56013c88b2aedff473cb2bcd1ea38db0a1ecc062 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 27 Mar 2024 15:53:39 +0100 Subject: [PATCH 169/902] feat(services): :safety_vest: parse requirement level input --- rocrate_validator/services.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index ee31eaef..8baca6f1 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -31,6 +31,12 @@ def validate( """ Validate a RO-Crate against a profile """ + # parse requirement level + requirement_level = \ + RequirementLevels.get(requirement_level) \ + if isinstance(requirement_level, str) \ + else requirement_level + validator = Validator( rocrate_path=rocrate_path, profiles_path=profiles_path, @@ -43,7 +49,7 @@ def validate( abort_on_first=abort_on_first, allow_infos=allow_infos, allow_warnings=allow_warnings, - requirement_level=RequirementLevels.get(requirement_level), + requirement_level=requirement_level, requirement_level_only=requirement_level_only, serialization_output_path=serialization_output_path, serialization_output_format=serialization_output_format, From 4b5606421752e94bfbf3a29cd78a3771afc053eb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 27 Mar 2024 15:56:04 +0100 Subject: [PATCH 170/902] refactor(services): :art: reorganize imports --- rocrate_validator/services.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 8baca6f1..87052a22 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -4,7 +4,9 @@ from pyshacl.pytypes import GraphLike -from .models import Profile, RequirementLevels, RequirementType, ValidationResult, Validator +from rocrate_validator.models import (Profile, RequirementLevels, + RequirementType, ValidationResult, + Validator) # set up logging logger = logging.getLogger(__name__) From 6978c2866dc825f641f9c894333b091c415a33fc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 10:45:42 +0100 Subject: [PATCH 171/902] feat(profiles/ro-crate): :sparkles: add file descriptor existence requirement --- profiles/ro-crate/must/0_file_descriptor.py | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 profiles/ro-crate/must/0_file_descriptor.py diff --git a/profiles/ro-crate/must/0_file_descriptor.py b/profiles/ro-crate/must/0_file_descriptor.py new file mode 100644 index 00000000..b9d0d3b7 --- /dev/null +++ b/profiles/ro-crate/must/0_file_descriptor.py @@ -0,0 +1,36 @@ +import json +import logging +from typing import Optional, Tuple + +from rocrate_validator.models import RequirementCheck, check + +# set up logging +logger = logging.getLogger(__name__) + + +class FileDescriptorExistence(RequirementCheck): + """The file descriptor MUST be present in the RO-Crate and MUST not be empty.""" + @check(name="File Description Existence") + def test_existence(self) -> bool: + """ + Check if the file descriptor is present in the RO-Crate + """ + if not self.file_descriptor_path.exists(): + self.result.add_error(f'RO-Crate "{self.file_descriptor_path}" file descriptor is not present', self) + return False + return True + + @check(name="File size check") + def test_size(self) -> bool: + """ + Check if the file descriptor is not empty + """ + if not self.file_descriptor_path.exists(): + self.result.add_error( + f'RO-Crate "{self.file_descriptor_path}" is empty: file descriptor is not present', self) + return False + if self.file_descriptor_path.stat().st_size == 0: + self.result.add_error(f'RO-Crate "{self.file_descriptor_path}" file descriptor is empty', self) + return False + return True + From dad7b2af9aaead271fbf29e028b7868d4152bcd4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 10:47:04 +0100 Subject: [PATCH 172/902] test(profiles/ro-crate): :white_check_mark: add test for file descriptor existence requirement --- .../ro-crate/must/test_file_descriptor.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/integration/profiles/ro-crate/must/test_file_descriptor.py diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor.py new file mode 100644 index 00000000..f830e449 --- /dev/null +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor.py @@ -0,0 +1,54 @@ +import logging +from pathlib import Path + +from pytest import fixture + +from rocrate_validator import models, services +from tests.ro_crates import InvalidFileDescriptor + +logger = logging.getLogger(__name__) + + +logger.debug(InvalidFileDescriptor.missing_file_descriptor) + + +@fixture(scope="module") +def paths(): + logger.debug("setup") + cls = InvalidFileDescriptor() + yield cls + logger.debug("teardown") + + +def test_path_initialization(paths): + logger.debug(f"test_path_initialization: {paths.missing_file_descriptor}") + assert paths.missing_file_descriptor, "missing_file_descriptor should be initialized" + assert Path(paths.missing_file_descriptor).exists(), "missing_file_descriptor should exist" + + +def test_missing_file_descriptor(paths): + + with paths.missing_file_descriptor as rocrate_path: + logger.debug(f"test_missing_file_descriptor: {rocrate_path}") + + result: models.ValidationResult = services.validate(rocrate_path, + requirement_level="MUST", abort_on_first=True) + assert not result.passed(), "ro-crate should be invalid" + + # check requirement + failed_requirements = result.failed_requirements + for failed_requirement in failed_requirements: + logger.debug(f"Failed requirement: {failed_requirement}") + assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" + + # set reference to failed requirement + failed_requirement = failed_requirements[0] + logger.debug(f"Failed requirement name: {failed_requirement.name}") + + # check if the failed requirement is the expected one + assert failed_requirement.name == "FileDescriptorExistence", \ + "Unexpected failed requirement" + + for issue in result.get_issues(): + logger.debug(f"Detected issue {type(issue)}: {issue.message}") + From 5595cf0603dba45020c538ebccd2172601f7c342 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 10:48:42 +0100 Subject: [PATCH 173/902] feat(profiles/ro-crate): :sparkles: add JSON format requirement for file descriptor --- profiles/ro-crate/must/0_file_descriptor.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/profiles/ro-crate/must/0_file_descriptor.py b/profiles/ro-crate/must/0_file_descriptor.py index b9d0d3b7..688ea266 100644 --- a/profiles/ro-crate/must/0_file_descriptor.py +++ b/profiles/ro-crate/must/0_file_descriptor.py @@ -34,3 +34,23 @@ def test_size(self) -> bool: return False return True + +class FileDescriptorJsonFormat(RequirementCheck): + """ + The file descriptor MUST be a valid JSON file + """ + @check(name="File Descriptor Format") + def check(self) -> Tuple[int, Optional[str]]: + # check if the file descriptor is in the correct format + try: + with open(self.file_descriptor_path, "r") as file: + json.load(file) + return True + except Exception as e: + self.result.add_error( + f'RO-Crate "{self.file_descriptor_path}" "\ + "file descriptor is not in the correct format', self) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return False + From 58781d42f50be8f62c0dd0adebb0e15e9ba54048 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 11:00:06 +0100 Subject: [PATCH 174/902] test(profiles/ro-crate): :white_check_mark: test for not valid JSON format of file descriptor --- .../ro-crate-metadata.json | 2 ++ .../ro-crate/must/test_file_descriptor.py | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 tests/data/crates/invalid/0_file_descriptor/invalid_json_format/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/0_file_descriptor/invalid_json_format/ro-crate-metadata.json b/tests/data/crates/invalid/0_file_descriptor/invalid_json_format/ro-crate-metadata.json new file mode 100644 index 00000000..0cb1f1e8 --- /dev/null +++ b/tests/data/crates/invalid/0_file_descriptor/invalid_json_format/ro-crate-metadata.json @@ -0,0 +1,2 @@ +This is a not valid JSON file. Please check the file and try again. +} diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor.py index f830e449..808e3dc2 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor.py @@ -52,3 +52,31 @@ def test_missing_file_descriptor(paths): for issue in result.get_issues(): logger.debug(f"Detected issue {type(issue)}: {issue.message}") + +def test_not_valid_json_format(paths): + + rocrate_path = paths.invalid_json_format + logger.debug(f"test_missing_file_descriptor: {rocrate_path}") + + result: models.ValidationResult = services.validate(rocrate_path, + requirement_level="MUST", abort_on_first=True) + assert not result.passed(), "ro-crate should be invalid" + + # check requirement + failed_requirements = result.failed_requirements + for failed_requirement in failed_requirements: + logger.debug(f"Failed requirement: {failed_requirement}") + assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" + + # set reference to failed requirement + failed_requirement = failed_requirements[0] + logger.debug(f"Failed requirement name: {failed_requirement.name}") + + # check if the failed requirement is the expected one + assert failed_requirement.name == "FileDescriptorJsonFormat", \ + "Unexpected failed requirement" + + for issue in result.get_issues(): + logger.debug(f"Detected issue {type(issue)}: {issue.message}") + + From bc7f49aef452a779959412ff5e31385dab46949f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 11:29:45 +0100 Subject: [PATCH 175/902] docs(profiles/ro-crate): :memo: update description of JSON format requirment check --- profiles/ro-crate/must/0_file_descriptor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/profiles/ro-crate/must/0_file_descriptor.py b/profiles/ro-crate/must/0_file_descriptor.py index 688ea266..f3a5e9c1 100644 --- a/profiles/ro-crate/must/0_file_descriptor.py +++ b/profiles/ro-crate/must/0_file_descriptor.py @@ -39,7 +39,7 @@ class FileDescriptorJsonFormat(RequirementCheck): """ The file descriptor MUST be a valid JSON file """ - @check(name="File Descriptor Format") + @check(name="Check JSON Format of the file descriptor") def check(self) -> Tuple[int, Optional[str]]: # check if the file descriptor is in the correct format try: From 20bba0cda4083ee66d55e24dbc691e624687275d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 11:30:45 +0100 Subject: [PATCH 176/902] feat(profiles/ro-crate): :sparkles: add JSON-LD context requirement for file descriptor --- profiles/ro-crate/must/0_file_descriptor.py | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/profiles/ro-crate/must/0_file_descriptor.py b/profiles/ro-crate/must/0_file_descriptor.py index f3a5e9c1..8ae9f3f0 100644 --- a/profiles/ro-crate/must/0_file_descriptor.py +++ b/profiles/ro-crate/must/0_file_descriptor.py @@ -54,3 +54,40 @@ def check(self) -> Tuple[int, Optional[str]]: logger.exception(e) return False + +class FileDescriptorJsonLdFormat(RequirementCheck): + """ + The file descriptor MUST be a valid JSON-LD file + """ + + _json_dict: Optional[dict] = None + + @property + def json_dict(self): + if self._json_dict is None: + self._json_dict = self.get_json_dict() + return self._json_dict + + def get_json_dict(self): + try: + with open(self.file_descriptor_path, "r") as file: + return json.load(file) + except Exception as e: + self.result.add_error( + f'RO-Crate "{self.file_descriptor_path}" "\ + "file descriptor is not in the correct format', self) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return {} + + @check(name="Check JSON-LD Format of the file descriptor") + def check_context(self) -> Tuple[int, Optional[str]]: + # check if the file descriptor is in the correct format + + json_dict = self.json_dict + if "@context" not in json_dict: + self.result.add_error( + f'RO-Crate "{self.file_descriptor_path}" "\ + "file descriptor does not contain a context', self) + return False + return True From e811af5bd24dda845559a11fed48ebd7ec66ce45 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 11:32:00 +0100 Subject: [PATCH 177/902] test(profiles/ro-crate): :white_check_mark: add test for JSON-LD requirement of file descriptor --- .../missing_context/ro-crate-metadata.json | 129 ++++++++++++++++++ .../ro-crate/must/test_file_descriptor.py | 25 ++++ 2 files changed, 154 insertions(+) create mode 100644 tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_context/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_context/ro-crate-metadata.json b/tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_context/ro-crate-metadata.json new file mode 100644 index 00000000..de1793da --- /dev/null +++ b/tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_context/ro-crate-metadata.json @@ -0,0 +1,129 @@ +{ + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-01-22T15:36:43+00:00", + "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": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" + } + ] + }, + { + "@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/must/test_file_descriptor.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor.py index 808e3dc2..22acf81a 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor.py @@ -80,3 +80,28 @@ def test_not_valid_json_format(paths): logger.debug(f"Detected issue {type(issue)}: {issue.message}") +def test_not_valid_jsonld_format_missing_context(paths): + + rocrate_path = f"{paths.invalid_jsonld_format}/missing_context" + logger.debug(f"test_missing_file_descriptor: {rocrate_path}") + + result: models.ValidationResult = services.validate(rocrate_path, + requirement_level="MUST", abort_on_first=True) + assert not result.passed(), "ro-crate should be invalid" + + # check requirement + failed_requirements = result.failed_requirements + for failed_requirement in failed_requirements: + logger.debug(f"Failed requirement: {failed_requirement}") + assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" + + # set reference to failed requirement + failed_requirement = failed_requirements[0] + logger.debug(f"Failed requirement name: {failed_requirement.name}") + + # check if the failed requirement is the expected one + assert failed_requirement.name == "FileDescriptorJsonLdFormat", \ + "Unexpected failed requirement" + + for issue in result.get_issues(): + logger.debug(f"Detected issue {type(issue)}: {issue.message}") From 2f4c6ba711546b0d6401b6b59fbead489baafcf8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 11:44:11 +0100 Subject: [PATCH 178/902] docs(profiles/ro-crate): :memo: update description of check context --- profiles/ro-crate/must/0_file_descriptor.py | 2 +- .../missing_id/ro-crate-metadata.json | 154 ++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_id/ro-crate-metadata.json diff --git a/profiles/ro-crate/must/0_file_descriptor.py b/profiles/ro-crate/must/0_file_descriptor.py index 8ae9f3f0..c22b5ce3 100644 --- a/profiles/ro-crate/must/0_file_descriptor.py +++ b/profiles/ro-crate/must/0_file_descriptor.py @@ -80,7 +80,7 @@ def get_json_dict(self): logger.exception(e) return {} - @check(name="Check JSON-LD Format of the file descriptor") + @check(name="Check if the @context property is present in the file descriptor") def check_context(self) -> Tuple[int, Optional[str]]: # check if the file descriptor is in the correct format diff --git a/tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_id/ro-crate-metadata.json b/tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_id/ro-crate-metadata.json new file mode 100644 index 00000000..c024fd72 --- /dev/null +++ b/tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_id/ro-crate-metadata.json @@ -0,0 +1,154 @@ +{ + "@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": [ + { + "should_be_the_id": "./", + "@type": "Dataset", + "datePublished": "2024-01-22T15:36:43+00:00", + "hasPart": [ + { + "should_be_the_id": "my-workflow.ga" + }, + { + "should_be_the_id": "my-workflow-test.yml" + }, + { + "should_be_the_id": "test-data/" + }, + { + "should_be_the_id": "README.md" + } + ], + "isBasedOn": "https://github.com/kikkomep/myworkflow", + "license": "MIT", + "mainEntity": { + "should_be_the_id": "my-workflow.ga" + }, + "mentions": [ + { + "should_be_the_id": "#1d230a09-a465-411a-82bb-d7d4f3f1be02" + } + ], + "name": "MyWorkflow" + }, + { + "should_be_the_id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "should_be_the_id": "./" + }, + "conformsTo": [ + { + "should_be_the_id": "https://w3id.org/ro/crate/1.1" + }, + { + "should_be_the_id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" + } + ] + }, + { + "should_be_the_id": "my-workflow.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "name": "MyWorkflow", + "programmingLanguage": { + "should_be_the_id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "url": "https://github.com/kikkomep/myworkflow", + "version": "main" + }, + { + "should_be_the_id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "should_be_the_id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "should_be_the_id": "https://galaxyproject.org/" + } + }, + { + "should_be_the_id": "#1d230a09-a465-411a-82bb-d7d4f3f1be02", + "@type": "TestSuite", + "definition": { + "should_be_the_id": "my-workflow-test.yml" + }, + "instance": [ + { + "should_be_the_id": "#350f2567-6ed2-4080-b354-a0921f49a4a9" + } + ], + "mainEntity": { + "should_be_the_id": "my-workflow.ga" + }, + "name": "Test suite for MyWorkflow" + }, + { + "should_be_the_id": "#350f2567-6ed2-4080-b354-a0921f49a4a9", + "@type": "TestInstance", + "name": "GitHub Actions workflow for testing MyWorkflow", + "resource": "repos/kikkomep/myworkflow/actions/workflows/main.yml", + "runsOn": { + "should_be_the_id": "https://w3id.org/ro/terms/test#GithubService" + }, + "url": "https://api.github.com" + }, + { + "should_be_the_id": "https://w3id.org/ro/terms/test#GithubService", + "@type": "TestService", + "name": "Github Actions", + "url": { + "should_be_the_id": "https://github.com" + } + }, + { + "should_be_the_id": "my-workflow-test.yml", + "@type": [ + "File", + "TestDefinition" + ], + "conformsTo": { + "should_be_the_id": "https://w3id.org/ro/terms/test#PlanemoEngine" + } + }, + { + "should_be_the_id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "@type": "SoftwareApplication", + "name": "Planemo", + "url": { + "should_be_the_id": "https://github.com/galaxyproject/planemo" + } + }, + { + "should_be_the_id": "test-data/", + "@type": "Dataset", + "description": "Data files for testing the workflow" + }, + { + "should_be_the_id": "README.md", + "@type": "File", + "description": "Workflow documentation" + } + ] +} From a3cea28677f0cc513aeea380d061a81c740bbf52 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 11:44:47 +0100 Subject: [PATCH 179/902] refactor(profiles/ro-crate): :recycle: reformat multiline strings --- profiles/ro-crate/must/0_file_descriptor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/profiles/ro-crate/must/0_file_descriptor.py b/profiles/ro-crate/must/0_file_descriptor.py index c22b5ce3..8ae3ecf2 100644 --- a/profiles/ro-crate/must/0_file_descriptor.py +++ b/profiles/ro-crate/must/0_file_descriptor.py @@ -74,8 +74,8 @@ def get_json_dict(self): return json.load(file) except Exception as e: self.result.add_error( - f'RO-Crate "{self.file_descriptor_path}" "\ - "file descriptor is not in the correct format', self) + f"RO-Crate \"{self.file_descriptor_path}\" " + "file descriptor is not in the correct format", self) if logger.isEnabledFor(logging.DEBUG): logger.exception(e) return {} @@ -87,7 +87,7 @@ def check_context(self) -> Tuple[int, Optional[str]]: json_dict = self.json_dict if "@context" not in json_dict: self.result.add_error( - f'RO-Crate "{self.file_descriptor_path}" "\ - "file descriptor does not contain a context', self) + f"RO-Crate \"{self.file_descriptor_path}\" " + "file descriptor does not contain a context", self) return False return True From c29111877821224c8cd3ac2a49e12c175f6fd7ba Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 11:46:03 +0100 Subject: [PATCH 180/902] refactor(profiles/ro-crate): :coffin: remove obsolete comment --- profiles/ro-crate/must/0_file_descriptor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/profiles/ro-crate/must/0_file_descriptor.py b/profiles/ro-crate/must/0_file_descriptor.py index 8ae3ecf2..17721127 100644 --- a/profiles/ro-crate/must/0_file_descriptor.py +++ b/profiles/ro-crate/must/0_file_descriptor.py @@ -82,8 +82,6 @@ def get_json_dict(self): @check(name="Check if the @context property is present in the file descriptor") def check_context(self) -> Tuple[int, Optional[str]]: - # check if the file descriptor is in the correct format - json_dict = self.json_dict if "@context" not in json_dict: self.result.add_error( From 3818dafbc884a0aef65ae4867e41caf4c137c8f2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 11:48:18 +0100 Subject: [PATCH 181/902] feat(profiles/ro-crate): :sparkles: check definition of ids of JSON-LD entities --- profiles/ro-crate/must/0_file_descriptor.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/profiles/ro-crate/must/0_file_descriptor.py b/profiles/ro-crate/must/0_file_descriptor.py index 17721127..edcfdf6c 100644 --- a/profiles/ro-crate/must/0_file_descriptor.py +++ b/profiles/ro-crate/must/0_file_descriptor.py @@ -89,3 +89,14 @@ def check_context(self) -> Tuple[int, Optional[str]]: "file descriptor does not contain a context", self) return False return True + + @check(name="Check if descriptor entities have the @id property") + def check_identifiers(self) -> Tuple[int, Optional[str]]: + json_dict = self.json_dict + for entity in json_dict["@graph"]: + if "@id" not in entity: + self.result.add_error( + f"Entity \"{entity.get('name', None) or entity}\" " + f"of RO-Crate \"{self.file_descriptor_path}\" " + "file descriptor does not contain the @id attribute", self) + return False From 82e53e2ca29e0c74adb6c0614b9ec0188974bd95 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 15:58:11 +0100 Subject: [PATCH 182/902] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor(vscode?= =?UTF-8?q?):=20rename=20file=20of=20py=20requirements=20for=20the=20file?= =?UTF-8?q?=20descriptor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ro-crate/must/{0_file_descriptor.py => 1_file_descriptor.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename profiles/ro-crate/must/{0_file_descriptor.py => 1_file_descriptor.py} (100%) diff --git a/profiles/ro-crate/must/0_file_descriptor.py b/profiles/ro-crate/must/1_file_descriptor.py similarity index 100% rename from profiles/ro-crate/must/0_file_descriptor.py rename to profiles/ro-crate/must/1_file_descriptor.py From 220d24643be2db6537eccd19ab3599f739954b85 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 16:16:23 +0100 Subject: [PATCH 183/902] test(profiles/ro-crate): :white_check_mark: test missing @id property --- .../ro-crate/must/test_file_descriptor.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor.py index 22acf81a..fe17324b 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor.py @@ -105,3 +105,32 @@ def test_not_valid_jsonld_format_missing_context(paths): for issue in result.get_issues(): logger.debug(f"Detected issue {type(issue)}: {issue.message}") + + +def test_not_valid_jsonld_format_missing_ids(paths): + + rocrate_path = f"{paths.invalid_jsonld_format}/missing_id" + logger.debug(f"test_missing_file_descriptor: {rocrate_path}") + + result: models.ValidationResult = services.validate(rocrate_path, + requirement_level="MUST", abort_on_first=True) + assert not result.passed(), "ro-crate should be invalid" + + # check requirement + failed_requirements = result.failed_requirements + for failed_requirement in failed_requirements: + logger.debug(f"Failed requirement: {failed_requirement}") + assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" + + # set reference to failed requirement + failed_requirement = failed_requirements[0] + logger.debug(f"Failed requirement name: {failed_requirement.name}") + + # check if the failed requirement is the expected one + assert failed_requirement.name == "FileDescriptorJsonLdFormat", \ + "Unexpected failed requirement" + + for issue in result.get_issues(): + logger.debug(f"Detected issue {type(issue)}: {issue.message}") + + From d041512c830e838d05e9e42ca7ed197cc63f4280 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 16:18:21 +0100 Subject: [PATCH 184/902] feat(profiles/ro-crate): :sparkles: require @type for JSON-LD entities --- profiles/ro-crate/must/1_file_descriptor.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/profiles/ro-crate/must/1_file_descriptor.py b/profiles/ro-crate/must/1_file_descriptor.py index edcfdf6c..6f2da23d 100644 --- a/profiles/ro-crate/must/1_file_descriptor.py +++ b/profiles/ro-crate/must/1_file_descriptor.py @@ -100,3 +100,14 @@ def check_identifiers(self) -> Tuple[int, Optional[str]]: f"of RO-Crate \"{self.file_descriptor_path}\" " "file descriptor does not contain the @id attribute", self) return False + + @check(name="Check if descriptor entities have the @type property") + def check_types(self) -> Tuple[int, Optional[str]]: + json_dict = self.json_dict + for entity in json_dict["@graph"]: + if "@type" not in entity: + self.result.add_error( + f"Entity \"{entity.get('name', None) or entity}\" " + f"of RO-Crate \"{self.file_descriptor_path}\" " + "file descriptor does not contain the @type attribute", self) + return False From ababe67d2b7eb77713d35a71ac627d0764b3c75f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 16:19:11 +0100 Subject: [PATCH 185/902] test(profiles/ro-crate): :white_check_mark: add test for @type requirements of JSON-LD entities --- .../missing_type/ro-crate-metadata.json | 151 ++++++++++++++++++ .../ro-crate/must/test_file_descriptor.py | 25 +++ 2 files changed, 176 insertions(+) create mode 100644 tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_type/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_type/ro-crate-metadata.json b/tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_type/ro-crate-metadata.json new file mode 100644 index 00000000..17f43385 --- /dev/null +++ b/tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_type/ro-crate-metadata.json @@ -0,0 +1,151 @@ +{ + "@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": "./", + "should_be_the_type": "Dataset", + "datePublished": "2024-01-22T15:36:43+00:00", + "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", + "should_be_the_type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" + } + ] + }, + { + "@id": "my-workflow.ga", + "should_be_the_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", + "should_be_the_type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#1d230a09-a465-411a-82bb-d7d4f3f1be02", + "should_be_the_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", + "should_be_the_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", + "should_be_the_type": "TestService", + "name": "Github Actions", + "url": { + "@id": "https://github.com" + } + }, + { + "@id": "my-workflow-test.yml", + "should_be_the_type": ["File", "TestDefinition"], + "conformsTo": { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" + } + }, + { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "should_be_the_type": "SoftwareApplication", + "name": "Planemo", + "url": { + "@id": "https://github.com/galaxyproject/planemo" + } + }, + { + "@id": "test-data/", + "should_be_the_type": "Dataset", + "description": "Data files for testing the workflow" + }, + { + "@id": "README.md", + "should_be_the_type": "File", + "description": "Workflow documentation" + } + ] +} diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor.py index fe17324b..a85296fe 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor.py @@ -134,3 +134,28 @@ def test_not_valid_jsonld_format_missing_ids(paths): logger.debug(f"Detected issue {type(issue)}: {issue.message}") +def test_not_valid_jsonld_format_missing_types(paths): + + rocrate_path = f"{paths.invalid_jsonld_format}/missing_type" + logger.debug(f"test_missing_file_descriptor: {rocrate_path}") + + result: models.ValidationResult = services.validate(rocrate_path, + requirement_level="MUST", abort_on_first=True) + assert not result.passed(), "ro-crate should be invalid" + + # check requirement + failed_requirements = result.failed_requirements + for failed_requirement in failed_requirements: + logger.debug(f"Failed requirement: {failed_requirement}") + assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" + + # set reference to failed requirement + failed_requirement = failed_requirements[0] + logger.debug(f"Failed requirement name: {failed_requirement.name}") + + # check if the failed requirement is the expected one + assert failed_requirement.name == "FileDescriptorJsonLdFormat", \ + "Unexpected failed requirement" + + for issue in result.get_issues(): + logger.debug(f"Detected issue {type(issue)}: {issue.message}") From e6026557ec8af2222ec96a8ff2839bb20ac6b2df Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 16:47:20 +0100 Subject: [PATCH 186/902] refactor(profiles/ro-crate): :truck: rename requirements files --- ...criptor.py => 0_file_descriptor_format.py} | 0 .../must/1_file-descriptor_metadata.ttl | 23 +++++++++++++++++++ 2 files changed, 23 insertions(+) rename profiles/ro-crate/must/{1_file_descriptor.py => 0_file_descriptor_format.py} (100%) create mode 100644 profiles/ro-crate/must/1_file-descriptor_metadata.ttl diff --git a/profiles/ro-crate/must/1_file_descriptor.py b/profiles/ro-crate/must/0_file_descriptor_format.py similarity index 100% rename from profiles/ro-crate/must/1_file_descriptor.py rename to profiles/ro-crate/must/0_file_descriptor_format.py diff --git a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl new file mode 100644 index 00000000..e3511c91 --- /dev/null +++ b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -0,0 +1,23 @@ +@prefix : <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +:FileDescriptorExistence + a sh:NodeShape ; + sh:name "RO-Crate Metadata File Descriptor MUST exist" ; + sh:description "The root of the document MUST have a node with ID ro-crate-metadata.json" ; + sh:targetNode :ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check metadata file existence" ; + sh:description "Check if the RO-Crate metadata file exists" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The RO-Crate metadata file descriptor MUST exist" ; + ] . + From 9badabe9ce1a3c53107a36aabb47a5b5604b98f6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 16:49:08 +0100 Subject: [PATCH 187/902] refactor(profiles/ro-crate): :truck: rename test data --- .../invalid_json_format/ro-crate-metadata.json | 0 .../invalid_jsonld_format/missing_context/ro-crate-metadata.json | 0 .../invalid_jsonld_format/missing_id/ro-crate-metadata.json | 0 .../invalid_jsonld_format/missing_type/ro-crate-metadata.json | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/data/crates/invalid/{0_file_descriptor => 0_file_descriptor_format}/invalid_json_format/ro-crate-metadata.json (100%) rename tests/data/crates/invalid/{0_file_descriptor => 0_file_descriptor_format}/invalid_jsonld_format/missing_context/ro-crate-metadata.json (100%) rename tests/data/crates/invalid/{0_file_descriptor => 0_file_descriptor_format}/invalid_jsonld_format/missing_id/ro-crate-metadata.json (100%) rename tests/data/crates/invalid/{0_file_descriptor => 0_file_descriptor_format}/invalid_jsonld_format/missing_type/ro-crate-metadata.json (100%) diff --git a/tests/data/crates/invalid/0_file_descriptor/invalid_json_format/ro-crate-metadata.json b/tests/data/crates/invalid/0_file_descriptor_format/invalid_json_format/ro-crate-metadata.json similarity index 100% rename from tests/data/crates/invalid/0_file_descriptor/invalid_json_format/ro-crate-metadata.json rename to tests/data/crates/invalid/0_file_descriptor_format/invalid_json_format/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_context/ro-crate-metadata.json b/tests/data/crates/invalid/0_file_descriptor_format/invalid_jsonld_format/missing_context/ro-crate-metadata.json similarity index 100% rename from tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_context/ro-crate-metadata.json rename to tests/data/crates/invalid/0_file_descriptor_format/invalid_jsonld_format/missing_context/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_id/ro-crate-metadata.json b/tests/data/crates/invalid/0_file_descriptor_format/invalid_jsonld_format/missing_id/ro-crate-metadata.json similarity index 100% rename from tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_id/ro-crate-metadata.json rename to tests/data/crates/invalid/0_file_descriptor_format/invalid_jsonld_format/missing_id/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_type/ro-crate-metadata.json b/tests/data/crates/invalid/0_file_descriptor_format/invalid_jsonld_format/missing_type/ro-crate-metadata.json similarity index 100% rename from tests/data/crates/invalid/0_file_descriptor/invalid_jsonld_format/missing_type/ro-crate-metadata.json rename to tests/data/crates/invalid/0_file_descriptor_format/invalid_jsonld_format/missing_type/ro-crate-metadata.json From 58ce164e20aef8c045983ce77beb347e757084a6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 16:51:56 +0100 Subject: [PATCH 188/902] test(profiles/ro-crate): :truck: update config of crates paths --- tests/ro_crates.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 6b01be11..9e9f375e 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -1,6 +1,8 @@ import os +from pathlib import Path from pytest import fixture +from tempfile import TemporaryDirectory import logging @@ -20,8 +22,16 @@ def ro_crates_path(): class InvalidFileDescriptor: - base_path = f"{INVALID_CRATES_DATA_PATH}/0_file_descriptor" + base_path = f"{INVALID_CRATES_DATA_PATH}/0_file_descriptor_format" @property - def missing_file_descriptor(self): - return f"{self.base_path}/missing_file_descriptor" + def missing_file_descriptor(self) -> Path: + return TemporaryDirectory() + + @property + def invalid_json_format(self) -> Path: + return Path(f"{self.base_path}/invalid_json_format") + + @property + def invalid_jsonld_format(self) -> Path: + return Path(f"{self.base_path}/invalid_jsonld_format") From 7d9378f06419ce87f295305ec9f77ba34b8221f6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 18:53:56 +0100 Subject: [PATCH 189/902] feat(profiles/ro-crate): :sparkles: add requirement for Data Root Entity existence --- .../must/2_root_data_entity_metadata.ttl | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 profiles/ro-crate/must/2_root_data_entity_metadata.ttl diff --git a/profiles/ro-crate/must/2_root_data_entity_metadata.ttl b/profiles/ro-crate/must/2_root_data_entity_metadata.ttl new file mode 100644 index 00000000..7ac82e46 --- /dev/null +++ b/profiles/ro-crate/must/2_root_data_entity_metadata.ttl @@ -0,0 +1,24 @@ +@prefix : <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + + +:RootDataEntityExistence + a sh:NodeShape ; + sh:name "RO-Crate Root Data Entity MUST exist" ; + sh:description "The root of the document MUST have a node with ID ./" ; + sh:targetNode : ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check existence of the Root Rata Entity" ; + sh:description "Check if the Root Data Entity exists in the file descriptor" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The file descriptor MUST have a Root Data Entity" ; + ] . + From 3fa298e5beca0a6840046fc6a644c1f564026ac5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 18:55:00 +0100 Subject: [PATCH 190/902] test(profiles/ro-crate): :white_check_mark: test requirement of data root entity existence --- .../missing_root/ro-crate-metadata.json | 117 ++++++++++++++++++ .../ro-crate/must/test_root_data_entity.py | 49 ++++++++ 2 files changed, 166 insertions(+) create mode 100644 tests/data/crates/invalid/1_root_data_entity/missing_root/ro-crate-metadata.json create mode 100644 tests/integration/profiles/ro-crate/must/test_root_data_entity.py diff --git a/tests/data/crates/invalid/1_root_data_entity/missing_root/ro-crate-metadata.json b/tests/data/crates/invalid/1_root_data_entity/missing_root/ro-crate-metadata.json new file mode 100644 index 00000000..ac9b307c --- /dev/null +++ b/tests/data/crates/invalid/1_root_data_entity/missing_root/ro-crate-metadata.json @@ -0,0 +1,117 @@ +{ + "@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": "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": "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/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py new file mode 100644 index 00000000..0ef4655b --- /dev/null +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -0,0 +1,49 @@ +import logging +from pathlib import Path + +from pytest import fixture + +from rocrate_validator import models, services +from tests.ro_crates import InvalidFileDescriptor, InvalidRootDataEntity + +logger = logging.getLogger(__name__) + + +logger.debug(InvalidFileDescriptor.missing_file_descriptor) + + +@fixture(scope="module") +def paths(): + logger.debug("setup") + cls = InvalidRootDataEntity() + yield cls + logger.debug("teardown") + + +def test_missing_root_data_entity(paths): + + rocrate_path = paths.missing_root + logger.debug(f"test_missing_file_descriptor: {rocrate_path}") + + result: models.ValidationResult = services.validate(rocrate_path, + requirement_level="MUST", abort_on_first=True) + assert not result.passed(), "ro-crate should be invalid" + + # check requirement + failed_requirements = result.failed_requirements + for failed_requirement in failed_requirements: + logger.debug(f"Failed requirement: {failed_requirement}") + assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" + + # set reference to failed requirement + failed_requirement = failed_requirements[0] + logger.debug(f"Failed requirement name: {failed_requirement.name}") + + # check if the failed requirement is the expected one + assert failed_requirement.name == "RO-Crate Root Data Entity MUST exist", \ + "Unexpected failed requirement" + + for issue in result.get_issues(): + logger.debug(f"Detected issue {type(issue)}: {issue.message}") + + From aabb3ea595ddc0666b63d29141ae2021caf40527 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 19:24:22 +0100 Subject: [PATCH 191/902] feat(profiles/ro-crate): :sparkles: add requirement for the type of the Root Data Entity --- .../must/2_root_data_entity_metadata.ttl | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/profiles/ro-crate/must/2_root_data_entity_metadata.ttl b/profiles/ro-crate/must/2_root_data_entity_metadata.ttl index 7ac82e46..3cb299c7 100644 --- a/profiles/ro-crate/must/2_root_data_entity_metadata.ttl +++ b/profiles/ro-crate/must/2_root_data_entity_metadata.ttl @@ -22,3 +22,19 @@ sh:message "The file descriptor MUST have a Root Data Entity" ; ] . +:RootDataEntityDirectProperties a sh:NodeShape ; + sh:name "RO-Crate Data Entity definition" ; + sh:description """The Root Data Entity MUST have the properties defined in + """; + sh:targetNode : ; + sh:property [ + a sh:PropertyShape ; + sh:name "Type of the Root Data Entity" ; + sh:description "The type of the Root Data Entity MUST be schema_org:Dataset" ; + sh:minCount 1 ; + sh:maxCount 1 ; + sh:nodeKind sh:IRI ; + sh:path rdf:type ; + sh:hasValue schema_org:Dataset; + sh:message "The Root Data Entity MUST be of type schema_org:Dataset" ; + ] . From 621a07abd4e55413a1c6d891dd5527a005ddf4c8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 19:28:28 +0100 Subject: [PATCH 192/902] test(profiles/ro-crate): :white_check_mark: add test for the root data entity type --- .../invalid_root_type/ro-crate-metadata.json | 154 ++++++++++++++++++ .../ro-crate/must/test_root_data_entity.py | 30 ++++ tests/ro_crates.py | 13 ++ 3 files changed, 197 insertions(+) create mode 100644 tests/data/crates/invalid/1_root_data_entity/invalid_root_type/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_root_data_entity/invalid_root_type/ro-crate-metadata.json b/tests/data/crates/invalid/1_root_data_entity/invalid_root_type/ro-crate-metadata.json new file mode 100644 index 00000000..a965b219 --- /dev/null +++ b/tests/data/crates/invalid/1_root_data_entity/invalid_root_type/ro-crate-metadata.json @@ -0,0 +1,154 @@ +{ + "@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": "WrongDatasetType", + "datePublished": "2024-01-22T15:36:43+00:00", + "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": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" + } + ] + }, + { + "@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/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index 0ef4655b..14ac48ca 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -47,3 +47,33 @@ def test_missing_root_data_entity(paths): logger.debug(f"Detected issue {type(issue)}: {issue.message}") +def test_invalid_root_type(paths): + + rocrate_path = paths.invalid_root_type + logger.debug(f"rocrate path: {rocrate_path}") + + result: models.ValidationResult = services.validate(rocrate_path, + requirement_level="MUST", abort_on_first=True) + assert not result.passed(), "ro-crate should be invalid" + + # check requirement + failed_requirements = result.failed_requirements + for failed_requirement in failed_requirements: + logger.debug(f"Failed requirement: {failed_requirement}") + assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" + + # set reference to failed requirement + failed_requirement = failed_requirements[0] + logger.debug(f"Failed requirement name: {failed_requirement.name}") + + # check if the failed requirement is the expected one + assert failed_requirement.name == "RO-Crate Data Entity definition", \ + "Unexpected failed requirement" + + assert len(result.get_issues()) == 1, "ro-crate should have 1 issue" + + # ย extract issue + issue = result.get_issues()[0] + assert issue.message == "The Root Data Entity MUST be of type schema_org:Dataset", \ + "Unexpected issue message" + diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 9e9f375e..cbcf904c 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -35,3 +35,16 @@ def invalid_json_format(self) -> Path: @property def invalid_jsonld_format(self) -> Path: return Path(f"{self.base_path}/invalid_jsonld_format") + + +class InvalidRootDataEntity: + + base_path = f"{INVALID_CRATES_DATA_PATH}/1_root_data_entity" + + @property + def missing_root(self) -> Path: + return Path(f"{self.base_path}/missing_root") + + @property + def invalid_root_type(self) -> Path: + return Path(f"{self.base_path}/invalid_root_type") From cd6d4489f59e1c14b3810f1e244ca6ea4d920c7c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 22:19:24 +0100 Subject: [PATCH 193/902] refactor(profiles/ro-crate): :truck: rename shape for root data entity --- profiles/ro-crate/must/2_root_data_entity_metadata.ttl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/profiles/ro-crate/must/2_root_data_entity_metadata.ttl b/profiles/ro-crate/must/2_root_data_entity_metadata.ttl index 3cb299c7..23c080f2 100644 --- a/profiles/ro-crate/must/2_root_data_entity_metadata.ttl +++ b/profiles/ro-crate/must/2_root_data_entity_metadata.ttl @@ -22,7 +22,7 @@ sh:message "The file descriptor MUST have a Root Data Entity" ; ] . -:RootDataEntityDirectProperties a sh:NodeShape ; +:RootDataEntityRequiredDirectProperties a sh:NodeShape ; sh:name "RO-Crate Data Entity definition" ; sh:description """The Root Data Entity MUST have the properties defined in """; @@ -32,9 +32,9 @@ sh:name "Type of the Root Data Entity" ; sh:description "The type of the Root Data Entity MUST be schema_org:Dataset" ; sh:minCount 1 ; - sh:maxCount 1 ; sh:nodeKind sh:IRI ; sh:path rdf:type ; - sh:hasValue schema_org:Dataset; + sh:hasValue schema_org:Dataset ; sh:message "The Root Data Entity MUST be of type schema_org:Dataset" ; + ] ; ] . From 1a2cd11fce943725be496da58856aa932455b83e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 22:20:13 +0100 Subject: [PATCH 194/902] feat(profiles/ro-crate): :sparkles: add datePublished requiremnt for Root Data Entity --- profiles/ro-crate/must/2_root_data_entity_metadata.ttl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/profiles/ro-crate/must/2_root_data_entity_metadata.ttl b/profiles/ro-crate/must/2_root_data_entity_metadata.ttl index 23c080f2..bb92e6fd 100644 --- a/profiles/ro-crate/must/2_root_data_entity_metadata.ttl +++ b/profiles/ro-crate/must/2_root_data_entity_metadata.ttl @@ -37,4 +37,13 @@ sh:hasValue schema_org:Dataset ; sh:message "The Root Data Entity MUST be of type schema_org:Dataset" ; ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Date Published of the Root Data Entity" ; + sh:description "The datePublished of the Root Data Entity 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{2}:\\d{2}$" ; + sh:message "The datePublished of the Root Data Entity MUST be a valid ISO 8601 date" ; ] . From 6b245ffc7256ced32d3a6a9123427c233cc3fe59 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 22:22:16 +0100 Subject: [PATCH 195/902] test(profiles/ro-crate): :white_check_mark: add test for the required datePublished property of RDE --- .../invalid_root_date/ro-crate-metadata.json | 154 ++++++++++++++++++ .../ro-crate/must/test_root_data_entity.py | 31 ++++ tests/ro_crates.py | 5 + 3 files changed, 190 insertions(+) create mode 100644 tests/data/crates/invalid/1_root_data_entity/invalid_root_date/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_root_data_entity/invalid_root_date/ro-crate-metadata.json b/tests/data/crates/invalid/1_root_data_entity/invalid_root_date/ro-crate-metadata.json new file mode 100644 index 00000000..a971825c --- /dev/null +++ b/tests/data/crates/invalid/1_root_data_entity/invalid_root_date/ro-crate-metadata.json @@ -0,0 +1,154 @@ +{ + "@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": "2024-01-22", + "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": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" + } + ] + }, + { + "@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/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index 14ac48ca..dae4ca7d 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -77,3 +77,34 @@ def test_invalid_root_type(paths): assert issue.message == "The Root Data Entity MUST be of type schema_org:Dataset", \ "Unexpected issue message" + +def test_invalid_root_date(paths): + + rocrate_path = paths.invalid_root_date + logger.debug(f"rocrate path: {rocrate_path}") + + result: models.ValidationResult = services.validate(rocrate_path, + requirement_level="MUST", abort_on_first=True) + assert not result.passed(), "ro-crate should be invalid" + + # check requirement + failed_requirements = result.failed_requirements + for failed_requirement in failed_requirements: + logger.debug(f"Failed requirement: {failed_requirement}") + assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" + + # set reference to failed requirement + failed_requirement = failed_requirements[0] + logger.debug(f"Failed requirement name: {failed_requirement.name}") + + # check if the failed requirement is the expected one + assert failed_requirement.name == "RO-Crate Data Entity definition", \ + "Unexpected failed requirement" + + assert len(result.get_issues()) == 1, "ro-crate should have 1 issue" + + # ย extract issue + issue = result.get_issues()[0] + assert issue.message == "The datePublished of the Root Data Entity MUST be a valid ISO 8601 date", \ + "Unexpected issue message" + diff --git a/tests/ro_crates.py b/tests/ro_crates.py index cbcf904c..375eb19d 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -48,3 +48,8 @@ def missing_root(self) -> Path: @property def invalid_root_type(self) -> Path: return Path(f"{self.base_path}/invalid_root_type") + + + @property + def invalid_root_date(self) -> Path: + return Path(f"{self.base_path}/invalid_root_date") From ecf23113051f2b3f59c62cfc8e8dce0f09e70b26 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 22:35:02 +0100 Subject: [PATCH 196/902] feat(profiles/ro-crate): :sparkles: recommended name of Root Data Entity --- .../should/2_root_data_entity_metadata.ttl | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 profiles/ro-crate/should/2_root_data_entity_metadata.ttl diff --git a/profiles/ro-crate/should/2_root_data_entity_metadata.ttl b/profiles/ro-crate/should/2_root_data_entity_metadata.ttl new file mode 100644 index 00000000..652717b0 --- /dev/null +++ b/profiles/ro-crate/should/2_root_data_entity_metadata.ttl @@ -0,0 +1,26 @@ +@prefix : <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; + sh:name "RO-Crate Data Entity definition: RECOMMENDED properties" ; + sh:description """The Root Data Entity SHOULD have the properties + `name`, `description` and `license` defined in + """; + sh:targetNode : ; + sh:property [ + a sh:PropertyShape ; + sh:name "Name of the Root Data Entity" ; + sh:description """The Root Data Entity SHOULD have + a schema_org:name to identify the dataset to human well enough + to disanbiguate it from other datasets""" ; + sh:minCount 1 ; + sh:nodeKind sh:Literal ; + sh:path schema_org:name; + sh:message "The Root Data Entity SHOULD have a schema_org:name" ; + ] . From 12b077d9452811d0c1be7c4a4ed45a3aee527b5f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 22:36:48 +0100 Subject: [PATCH 197/902] test(profiles/ro-crate): :white_check_mark: add test of recommended name of root data entity --- .../missing_root_name/ro-crate-metadata.json | 153 ++++++++++++++++++ .../ro-crate/must/test_root_data_entity.py | 31 ++++ tests/ro_crates.py | 5 +- 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 tests/data/crates/invalid/1_root_data_entity/missing_root_name/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_root_data_entity/missing_root_name/ro-crate-metadata.json b/tests/data/crates/invalid/1_root_data_entity/missing_root_name/ro-crate-metadata.json new file mode 100644 index 00000000..3a7ba713 --- /dev/null +++ b/tests/data/crates/invalid/1_root_data_entity/missing_root_name/ro-crate-metadata.json @@ -0,0 +1,153 @@ +{ + "@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": "2024-01-22T15:36:43+00:00", + "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" + } + ] + }, + { + "@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": "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/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index dae4ca7d..487589d5 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -108,3 +108,34 @@ def test_invalid_root_date(paths): assert issue.message == "The datePublished of the Root Data Entity MUST be a valid ISO 8601 date", \ "Unexpected issue message" + +def test_missing_root_name(paths): + + rocrate_path = paths.missing_root_name + logger.debug(f"rocrate path: {rocrate_path}") + + result: models.ValidationResult = services.validate(rocrate_path, + requirement_level="SHOULD", + abort_on_first=True) + assert not result.passed(), "ro-crate should be invalid" + + # check requirement + failed_requirements = result.failed_requirements + for failed_requirement in failed_requirements: + logger.debug(f"Failed requirement: {failed_requirement}") + assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" + + # set reference to failed requirement + failed_requirement = failed_requirements[0] + logger.debug(f"Failed requirement name: {failed_requirement.name}") + + # check if the failed requirement is the expected one + assert failed_requirement.name == "RO-Crate Data Entity definition: RECOMMENDED properties", \ + "Unexpected failed requirement" + + assert len(result.get_issues()) == 1, "ro-crate should have 1 issue" + + # ย extract issue + issue = result.get_issues()[0] + assert issue.message == "The Root Data Entity SHOULD have a schema_org:name", \ + "Unexpected issue message" diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 375eb19d..3c8a557e 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -49,7 +49,10 @@ def missing_root(self) -> Path: def invalid_root_type(self) -> Path: return Path(f"{self.base_path}/invalid_root_type") - @property def invalid_root_date(self) -> Path: return Path(f"{self.base_path}/invalid_root_date") + + @property + def missing_root_name(self) -> Path: + return Path(f"{self.base_path}/missing_root_name") From 481241bacd2b6645180013fe903db5d3232f3a46 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 22:51:28 +0100 Subject: [PATCH 198/902] feat(profiles/ro-crate): :sparkles: recommended description of Root Data Entity --- .../ro-crate/should/2_root_data_entity_metadata.ttl | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/profiles/ro-crate/should/2_root_data_entity_metadata.ttl b/profiles/ro-crate/should/2_root_data_entity_metadata.ttl index 652717b0..74614497 100644 --- a/profiles/ro-crate/should/2_root_data_entity_metadata.ttl +++ b/profiles/ro-crate/should/2_root_data_entity_metadata.ttl @@ -23,4 +23,15 @@ sh:nodeKind sh:Literal ; sh:path schema_org:name; sh:message "The Root Data Entity SHOULD have a schema_org:name" ; - ] . + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Description of the Root Data Entity" ; + sh:description """The Root Data Entity SHOULD have + a schema_org:description to further elaborate on the name + of the dataset and provide a summary in which the dataset is important""" ; + sh:minCount 1 ; + sh:nodeKind sh:Literal ; + sh:path schema_org:description; + sh:message "The Root Data Entity SHOULD have a schema_org:description" ; + ] . From 2f3054c30d9286352262a6c6e2ffd651b743daea Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 22:52:38 +0100 Subject: [PATCH 199/902] test(profiles/ro-crate): :white_check_mark: test recommended description of Root Data Entity --- .../ro-crate-metadata.json | 154 ++++++++++++++++++ .../ro-crate/must/test_root_data_entity.py | 32 ++++ tests/ro_crates.py | 5 + 3 files changed, 191 insertions(+) create mode 100644 tests/data/crates/invalid/1_root_data_entity/missing_root_description/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_root_data_entity/missing_root_description/ro-crate-metadata.json b/tests/data/crates/invalid/1_root_data_entity/missing_root_description/ro-crate-metadata.json new file mode 100644 index 00000000..7adda591 --- /dev/null +++ b/tests/data/crates/invalid/1_root_data_entity/missing_root_description/ro-crate-metadata.json @@ -0,0 +1,154 @@ +{ + "@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", + "name": "MyWorkflow", + "datePublished": "2024-01-22T15:36:43+00:00", + "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" + } + ] + }, + { + "@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": "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/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index 487589d5..f34c0bd8 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -139,3 +139,35 @@ def test_missing_root_name(paths): issue = result.get_issues()[0] assert issue.message == "The Root Data Entity SHOULD have a schema_org:name", \ "Unexpected issue message" + + +def test_missing_root_description(paths): + + rocrate_path = paths.missing_root_description + logger.debug(f"rocrate path: {rocrate_path}") + + result: models.ValidationResult = services.validate(rocrate_path, + requirement_level="SHOULD", + abort_on_first=True) + assert not result.passed(), "ro-crate should be invalid" + + # check requirement + failed_requirements = result.failed_requirements + for failed_requirement in failed_requirements: + logger.debug(f"Failed requirement: {failed_requirement}") + assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" + + # set reference to failed requirement + failed_requirement = failed_requirements[0] + logger.debug(f"Failed requirement name: {failed_requirement.name}") + + # check if the failed requirement is the expected one + assert failed_requirement.name == "RO-Crate Data Entity definition: RECOMMENDED properties", \ + "Unexpected failed requirement" + + assert len(result.get_issues()) == 1, "ro-crate should have 1 issue" + + # ย extract issue + issue = result.get_issues()[0] + assert issue.message == "The Root Data Entity SHOULD have a schema_org:description", \ + "Unexpected issue message" diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 3c8a557e..877748f3 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -56,3 +56,8 @@ def invalid_root_date(self) -> Path: @property def missing_root_name(self) -> Path: return Path(f"{self.base_path}/missing_root_name") + + @property + def missing_root_description(self) -> Path: + return Path(f"{self.base_path}/missing_root_description") + From c3a2570bcfd0019a73b99a51e294dff84f9137e5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 23:26:03 +0100 Subject: [PATCH 200/902] test(profiles/ro-crate): :bug: fix test data --- .../missing_root_name/ro-crate-metadata.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/data/crates/invalid/1_root_data_entity/missing_root_name/ro-crate-metadata.json b/tests/data/crates/invalid/1_root_data_entity/missing_root_name/ro-crate-metadata.json index 3a7ba713..17264e95 100644 --- a/tests/data/crates/invalid/1_root_data_entity/missing_root_name/ro-crate-metadata.json +++ b/tests/data/crates/invalid/1_root_data_entity/missing_root_name/ro-crate-metadata.json @@ -36,8 +36,11 @@ "@id": "README.md" } ], + "description": "RO-Crate for MyWorkflow", "isBasedOn": "https://github.com/kikkomep/myworkflow", - "license": "MIT", + "license": { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + }, "mainEntity": { "@id": "my-workflow.ga" }, From e1d79bb1888bb6a7d3df5922d1ca37c02f3ad231 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 23:27:33 +0100 Subject: [PATCH 201/902] test(profiles/ro-crate/should): :white_check_mark: recommended license of Root Data Entity --- .../ro-crate/should/2_root_data_entity_metadata.ttl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/profiles/ro-crate/should/2_root_data_entity_metadata.ttl b/profiles/ro-crate/should/2_root_data_entity_metadata.ttl index 74614497..3f44df31 100644 --- a/profiles/ro-crate/should/2_root_data_entity_metadata.ttl +++ b/profiles/ro-crate/should/2_root_data_entity_metadata.ttl @@ -34,4 +34,16 @@ sh:nodeKind sh:Literal ; sh:path schema_org:description; sh:message "The Root Data Entity SHOULD have a schema_org:description" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "License of the Root Data Entity" ; + sh:description """The Root Data Entity SHOULD + link to a Contextual Entity in the RO-Crate Metadata File + with a name and description.""" ; + sh:nodeKind sh:IRI ; + sh:path schema_org:license; + sh:minCount 1 ; + sh:message """The Root Data Entity SHOULD have a link + to a Contextual Entity representing a schema_org:license""" ; ] . From 20454c4fb8e2f435986c32e968ab39d7fdd3a5c1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 23:29:35 +0100 Subject: [PATCH 202/902] test(profiles/ro-crate/should): test recommended license for Root Data Entity --- .../ro-crate-metadata.json | 154 ++++++++++++++++++ .../ro-crate/must/test_root_data_entity.py | 32 ++++ tests/ro_crates.py | 3 + 3 files changed, 189 insertions(+) create mode 100644 tests/data/crates/invalid/1_root_data_entity/missing_root_license/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_root_data_entity/missing_root_license/ro-crate-metadata.json b/tests/data/crates/invalid/1_root_data_entity/missing_root_license/ro-crate-metadata.json new file mode 100644 index 00000000..da9ef87e --- /dev/null +++ b/tests/data/crates/invalid/1_root_data_entity/missing_root_license/ro-crate-metadata.json @@ -0,0 +1,154 @@ +{ + "@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", + "name": "MyWorkflow", + "description": "A simple workflow for testing RO-Crate", + "datePublished": "2024-01-22T15:36:43+00:00", + "hasPart": [ + { + "@id": "my-workflow.ga" + }, + { + "@id": "my-workflow-test.yml" + }, + { + "@id": "test-data/" + }, + { + "@id": "README.md" + } + ], + "isBasedOn": "https://github.com/kikkomep/myworkflow", + "mainEntity": { + "@id": "my-workflow.ga" + }, + "mentions": [ + { + "@id": "#1d230a09-a465-411a-82bb-d7d4f3f1be02" + } + ] + }, + { + "@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": "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/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index f34c0bd8..936566f7 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -171,3 +171,35 @@ def test_missing_root_description(paths): issue = result.get_issues()[0] assert issue.message == "The Root Data Entity SHOULD have a schema_org:description", \ "Unexpected issue message" + + +def test_missing_root_license(paths): + + rocrate_path = paths.missing_root_license + logger.debug(f"rocrate path: {rocrate_path}") + + result: models.ValidationResult = services.validate(rocrate_path, + requirement_level="SHOULD", + abort_on_first=True) + assert not result.passed(), "ro-crate should be invalid" + + # check requirement + failed_requirements = result.failed_requirements + for failed_requirement in failed_requirements: + logger.debug(f"Failed requirement: {failed_requirement}") + assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" + + # set reference to failed requirement + failed_requirement = failed_requirements[0] + logger.debug(f"Failed requirement name: {failed_requirement.name}") + + # check if the failed requirement is the expected one + assert failed_requirement.name == "RO-Crate Data Entity definition: RECOMMENDED properties", \ + "Unexpected failed requirement" + + assert len(result.get_issues()) == 1, "ro-crate should have 1 issue" + + # ย extract issue + issue = result.get_issues()[0] + assert "The Root Data Entity SHOULD have a link" in issue.message, \ + "Unexpected issue message" diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 877748f3..3c0a182d 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -61,3 +61,6 @@ def missing_root_name(self) -> Path: def missing_root_description(self) -> Path: return Path(f"{self.base_path}/missing_root_description") + @property + def missing_root_license(self) -> Path: + return Path(f"{self.base_path}/missing_root_license") From 9dccd3b550e742c895900fef6563152780a1bc99 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 24 Mar 2024 23:44:17 +0100 Subject: [PATCH 203/902] feat(profiles/ro-crate/may): :sparkles: requirement for optional license properties --- profiles/ro-crate/may/2_license_entity.ttl | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 profiles/ro-crate/may/2_license_entity.ttl diff --git a/profiles/ro-crate/may/2_license_entity.ttl b/profiles/ro-crate/may/2_license_entity.ttl new file mode 100644 index 00000000..4713f967 --- /dev/null +++ b/profiles/ro-crate/may/2_license_entity.ttl @@ -0,0 +1,34 @@ +@prefix : <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + +:LisenseDefinition a sh:NodeShape ; + sh:name "License definition" ; + sh:description """Contextual Entity + for representing a License with a name and description"""; + sh:targetNode :ro-crate-metadata.json ; + 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" ; + ] . + From e1649e11104f8d2389ab739e0d3d644600a99c54 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 25 Mar 2024 15:02:11 +0100 Subject: [PATCH 204/902] refactor(profiles/ro-crate/may): :memo: update license definition --- profiles/ro-crate/may/2_license_entity.ttl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/profiles/ro-crate/may/2_license_entity.ttl b/profiles/ro-crate/may/2_license_entity.ttl index 4713f967..a6ed1ca1 100644 --- a/profiles/ro-crate/may/2_license_entity.ttl +++ b/profiles/ro-crate/may/2_license_entity.ttl @@ -6,11 +6,10 @@ @prefix xml1: . @prefix xsd: . -:LisenseDefinition a sh:NodeShape ; +:LicenseDefinition a sh:NodeShape ; sh:name "License definition" ; - sh:description """Contextual Entity - for representing a License with a name and description"""; - sh:targetNode :ro-crate-metadata.json ; + 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" ; From 696b8612a1cded2eb643176eaf4a8be4ca18b81e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 25 Mar 2024 15:06:10 +0100 Subject: [PATCH 205/902] refactor(profiles/ro-crate/should): :adhesive_bandage: add missing class type for license entity --- profiles/ro-crate/should/2_root_data_entity_metadata.ttl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/profiles/ro-crate/should/2_root_data_entity_metadata.ttl b/profiles/ro-crate/should/2_root_data_entity_metadata.ttl index 3f44df31..503f38a9 100644 --- a/profiles/ro-crate/should/2_root_data_entity_metadata.ttl +++ b/profiles/ro-crate/should/2_root_data_entity_metadata.ttl @@ -41,9 +41,9 @@ sh:description """The Root Data Entity SHOULD link to a Contextual Entity in the RO-Crate Metadata File with a name and description.""" ; - sh:nodeKind sh:IRI ; + sh:nodeKind sh:BlankNodeOrIRI ; + sh:class schema_org:license ; sh:path schema_org:license; sh:minCount 1 ; - sh:message """The Root Data Entity SHOULD have a link - to a Contextual Entity representing a schema_org:license""" ; + sh:message """The Root Data Entity SHOULD have a link to a Contextual Entity representing the schema_org:license type""" ; ] . From 429b43b35f1b3ad2c1261e7496527e688fc3b3af Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 25 Mar 2024 15:15:12 +0100 Subject: [PATCH 206/902] test(profiles/ro-crate/may): :white_check_mark: test license name requirement --- .../ro-crate-metadata.json | 155 ++++++++++++++++++ .../ro-crate/must/test_root_data_entity.py | 33 +++- tests/ro_crates.py | 5 + 3 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 tests/data/crates/invalid/1_root_data_entity/missing_root_license_name/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_root_data_entity/missing_root_license_name/ro-crate-metadata.json b/tests/data/crates/invalid/1_root_data_entity/missing_root_license_name/ro-crate-metadata.json new file mode 100644 index 00000000..231af095 --- /dev/null +++ b/tests/data/crates/invalid/1_root_data_entity/missing_root_license_name/ro-crate-metadata.json @@ -0,0 +1,155 @@ +{ + "@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": "2024-01-22T15:36:43+00:00", + "hasPart": [ + { + "@id": "my-workflow.ga" + }, + { + "@id": "my-workflow-test.yml" + }, + { + "@id": "test-data/" + }, + { + "@id": "README.md" + } + ], + "name": "MyWorkflow RO-Crate", + "description": "RO-Crate for MyWorkflow", + "isBasedOn": "https://github.com/kikkomep/myworkflow", + "license": { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + }, + "mainEntity": { + "@id": "my-workflow.ga" + }, + "mentions": [ + { + "@id": "#1d230a09-a465-411a-82bb-d7d4f3f1be02" + } + ] + }, + { + "@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": "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" + }, + { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "@type": "license", + "description": "Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Australia" + } + ] +} diff --git a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index 936566f7..959fc0ff 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -1,5 +1,4 @@ import logging -from pathlib import Path from pytest import fixture @@ -203,3 +202,35 @@ def test_missing_root_license(paths): issue = result.get_issues()[0] assert "The Root Data Entity SHOULD have a link" in issue.message, \ "Unexpected issue message" + + +def test_missing_root_license_name(paths): + + rocrate_path = paths.missing_root_license_name + logger.debug(f"rocrate path: {rocrate_path}") + + result: models.ValidationResult = services.validate(rocrate_path, + requirement_level="MAY", + abort_on_first=True) + assert not result.passed(), "ro-crate should be invalid" + + # check requirement + failed_requirements = result.failed_requirements + for failed_requirement in failed_requirements: + logger.debug(f"Failed requirement: {failed_requirement}") + assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" + + # set reference to failed requirement + failed_requirement = failed_requirements[0] + logger.debug(f"Failed requirement name: {failed_requirement.name}") + + # check if the failed requirement is the expected one + assert failed_requirement.name == "License definition", \ + "Unexpected failed requirement" + + assert len(result.get_issues()) == 1, "ro-crate should have 1 issue" + + # ย extract issue + issue = result.get_issues()[0] + assert "Missing license name" in issue.message, \ + "Unexpected issue message" diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 3c0a182d..8b2601e1 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -64,3 +64,8 @@ def missing_root_description(self) -> Path: @property def missing_root_license(self) -> Path: return Path(f"{self.base_path}/missing_root_license") + + @property + def missing_root_license_name(self) -> Path: + return Path(f"{self.base_path}/missing_root_license_name") + From ffd8ea57cf228d294002ded6fa92295a7db4cc49 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 25 Mar 2024 15:17:27 +0100 Subject: [PATCH 207/902] test(profiles/ro-crate/may): :test_tube: test license description requirement --- .../ro-crate-metadata.json | 155 ++++++++++++++++++ .../ro-crate/must/test_root_data_entity.py | 32 ++++ tests/ro_crates.py | 3 + 3 files changed, 190 insertions(+) create mode 100644 tests/data/crates/invalid/1_root_data_entity/missing_root_license_description/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_root_data_entity/missing_root_license_description/ro-crate-metadata.json b/tests/data/crates/invalid/1_root_data_entity/missing_root_license_description/ro-crate-metadata.json new file mode 100644 index 00000000..ac285f0d --- /dev/null +++ b/tests/data/crates/invalid/1_root_data_entity/missing_root_license_description/ro-crate-metadata.json @@ -0,0 +1,155 @@ +{ + "@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": "2024-01-22T15:36:43+00:00", + "hasPart": [ + { + "@id": "my-workflow.ga" + }, + { + "@id": "my-workflow-test.yml" + }, + { + "@id": "test-data/" + }, + { + "@id": "README.md" + } + ], + "name": "MyWorkflow RO-Crate", + "description": "RO-Crate for MyWorkflow", + "isBasedOn": "https://github.com/kikkomep/myworkflow", + "license": { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + }, + "mainEntity": { + "@id": "my-workflow.ga" + }, + "mentions": [ + { + "@id": "#1d230a09-a465-411a-82bb-d7d4f3f1be02" + } + ] + }, + { + "@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": "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" + }, + { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "@type": "license", + "name": "Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Australia" + } + ] +} diff --git a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index 959fc0ff..f67b183a 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -234,3 +234,35 @@ def test_missing_root_license_name(paths): issue = result.get_issues()[0] assert "Missing license name" in issue.message, \ "Unexpected issue message" + + +def test_missing_root_license_description(paths): + + rocrate_path = paths.missing_root_license_description + logger.debug(f"rocrate path: {rocrate_path}") + + result: models.ValidationResult = services.validate(rocrate_path, + requirement_level="MAY", + abort_on_first=True) + assert not result.passed(), "ro-crate should be invalid" + + # check requirement + failed_requirements = result.failed_requirements + for failed_requirement in failed_requirements: + logger.debug(f"Failed requirement: {failed_requirement}") + assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" + + # set reference to failed requirement + failed_requirement = failed_requirements[0] + logger.debug(f"Failed requirement name: {failed_requirement.name}") + + # check if the failed requirement is the expected one + assert failed_requirement.name == "License definition", \ + "Unexpected failed requirement" + + assert len(result.get_issues()) == 1, "ro-crate should have 1 issue" + + # ย extract issue + issue = result.get_issues()[0] + assert "Missing license description" in issue.message, \ + "Unexpected issue message" diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 8b2601e1..2da283dc 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -69,3 +69,6 @@ def missing_root_license(self) -> Path: def missing_root_license_name(self) -> Path: return Path(f"{self.base_path}/missing_root_license_name") + @property + def missing_root_license_description(self) -> Path: + return Path(f"{self.base_path}/missing_root_license_description") From 99f6e235702cfd267948a63e6e1bcd2b32322e14 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 25 Mar 2024 15:33:24 +0100 Subject: [PATCH 208/902] refactor(test-conf): :truck: rename path of tests data --- .../invalid_root_date/ro-crate-metadata.json | 0 .../invalid_root_type/ro-crate-metadata.json | 0 .../missing_root/ro-crate-metadata.json | 0 .../missing_root_description/ro-crate-metadata.json | 0 .../missing_root_license/ro-crate-metadata.json | 0 .../missing_root_license_description/ro-crate-metadata.json | 0 .../missing_root_license_name/ro-crate-metadata.json | 0 .../missing_root_name/ro-crate-metadata.json | 0 tests/ro_crates.py | 2 +- 9 files changed, 1 insertion(+), 1 deletion(-) rename tests/data/crates/invalid/{1_root_data_entity => 2_root_data_entity_metadata}/invalid_root_date/ro-crate-metadata.json (100%) rename tests/data/crates/invalid/{1_root_data_entity => 2_root_data_entity_metadata}/invalid_root_type/ro-crate-metadata.json (100%) rename tests/data/crates/invalid/{1_root_data_entity => 2_root_data_entity_metadata}/missing_root/ro-crate-metadata.json (100%) rename tests/data/crates/invalid/{1_root_data_entity => 2_root_data_entity_metadata}/missing_root_description/ro-crate-metadata.json (100%) rename tests/data/crates/invalid/{1_root_data_entity => 2_root_data_entity_metadata}/missing_root_license/ro-crate-metadata.json (100%) rename tests/data/crates/invalid/{1_root_data_entity => 2_root_data_entity_metadata}/missing_root_license_description/ro-crate-metadata.json (100%) rename tests/data/crates/invalid/{1_root_data_entity => 2_root_data_entity_metadata}/missing_root_license_name/ro-crate-metadata.json (100%) rename tests/data/crates/invalid/{1_root_data_entity => 2_root_data_entity_metadata}/missing_root_name/ro-crate-metadata.json (100%) diff --git a/tests/data/crates/invalid/1_root_data_entity/invalid_root_date/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_date/ro-crate-metadata.json similarity index 100% rename from tests/data/crates/invalid/1_root_data_entity/invalid_root_date/ro-crate-metadata.json rename to tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_date/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_root_data_entity/invalid_root_type/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_type/ro-crate-metadata.json similarity index 100% rename from tests/data/crates/invalid/1_root_data_entity/invalid_root_type/ro-crate-metadata.json rename to tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_type/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_root_data_entity/missing_root/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root/ro-crate-metadata.json similarity index 100% rename from tests/data/crates/invalid/1_root_data_entity/missing_root/ro-crate-metadata.json rename to tests/data/crates/invalid/2_root_data_entity_metadata/missing_root/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_root_data_entity/missing_root_description/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_description/ro-crate-metadata.json similarity index 100% rename from tests/data/crates/invalid/1_root_data_entity/missing_root_description/ro-crate-metadata.json rename to tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_description/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_root_data_entity/missing_root_license/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license/ro-crate-metadata.json similarity index 100% rename from tests/data/crates/invalid/1_root_data_entity/missing_root_license/ro-crate-metadata.json rename to tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_root_data_entity/missing_root_license_description/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license_description/ro-crate-metadata.json similarity index 100% rename from tests/data/crates/invalid/1_root_data_entity/missing_root_license_description/ro-crate-metadata.json rename to tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license_description/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_root_data_entity/missing_root_license_name/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license_name/ro-crate-metadata.json similarity index 100% rename from tests/data/crates/invalid/1_root_data_entity/missing_root_license_name/ro-crate-metadata.json rename to tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license_name/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_root_data_entity/missing_root_name/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_name/ro-crate-metadata.json similarity index 100% rename from tests/data/crates/invalid/1_root_data_entity/missing_root_name/ro-crate-metadata.json rename to tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_name/ro-crate-metadata.json diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 2da283dc..e42ac5ce 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -39,7 +39,7 @@ def invalid_jsonld_format(self) -> Path: class InvalidRootDataEntity: - base_path = f"{INVALID_CRATES_DATA_PATH}/1_root_data_entity" + base_path = f"{INVALID_CRATES_DATA_PATH}/2_root_data_entity_metadata" @property def missing_root(self) -> Path: From 188d46fbe678bc21501c763bf49f11af2b10c9c3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 25 Mar 2024 15:57:46 +0100 Subject: [PATCH 209/902] refactor(test-conf): :truck: rename test data for root data entity --- .../ro-crate-metadata.json | 0 .../profiles/ro-crate/must/test_file_descriptor.py | 6 ------ tests/ro_crates.py | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) rename tests/data/crates/invalid/2_root_data_entity_metadata/{missing_root => missing_root_entity}/ro-crate-metadata.json (100%) diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_entity/ro-crate-metadata.json similarity index 100% rename from tests/data/crates/invalid/2_root_data_entity_metadata/missing_root/ro-crate-metadata.json rename to tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_entity/ro-crate-metadata.json diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor.py index a85296fe..7d2d1f99 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor.py @@ -20,12 +20,6 @@ def paths(): logger.debug("teardown") -def test_path_initialization(paths): - logger.debug(f"test_path_initialization: {paths.missing_file_descriptor}") - assert paths.missing_file_descriptor, "missing_file_descriptor should be initialized" - assert Path(paths.missing_file_descriptor).exists(), "missing_file_descriptor should exist" - - def test_missing_file_descriptor(paths): with paths.missing_file_descriptor as rocrate_path: diff --git a/tests/ro_crates.py b/tests/ro_crates.py index e42ac5ce..b0a21277 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -43,7 +43,7 @@ class InvalidRootDataEntity: @property def missing_root(self) -> Path: - return Path(f"{self.base_path}/missing_root") + return Path(f"{self.base_path}/missing_root_entity") @property def invalid_root_type(self) -> Path: From b07c2a24c485a24506cc726498676ac6ee3eb34d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 26 Mar 2024 10:45:00 +0100 Subject: [PATCH 210/902] refactor(profiles/ro-crate/must): :recycle: redefine shape of Root Data Entity --- .../must/2_root_data_entity_metadata.ttl | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/profiles/ro-crate/must/2_root_data_entity_metadata.ttl b/profiles/ro-crate/must/2_root_data_entity_metadata.ttl index bb92e6fd..704c01cb 100644 --- a/profiles/ro-crate/must/2_root_data_entity_metadata.ttl +++ b/profiles/ro-crate/must/2_root_data_entity_metadata.ttl @@ -7,19 +7,19 @@ @prefix xsd: . - -:RootDataEntityExistence +:RootDataEntityExistenceAndType a sh:NodeShape ; sh:name "RO-Crate Root Data Entity MUST exist" ; - sh:description "The root of the document MUST have a node with ID ./" ; + sh:description "The root of the document MUST be a `Dataset` and must end with /" ; sh:targetNode : ; sh:property [ a sh:PropertyShape ; sh:name "Check existence of the Root Rata Entity" ; sh:description "Check if the Root Data Entity exists in the file descriptor" ; sh:path rdf:type ; + sh:hasValue schema_org:Dataset ; sh:minCount 1 ; - sh:message "The file descriptor MUST have a Root Data Entity" ; + sh:message """The file descriptor MUST have a root data entity of type schema_org:Dataset and ending with /""" ; ] . :RootDataEntityRequiredDirectProperties a sh:NodeShape ; @@ -27,16 +27,6 @@ sh:description """The Root Data Entity MUST have the properties defined in """; sh:targetNode : ; - sh:property [ - a sh:PropertyShape ; - sh:name "Type of the Root Data Entity" ; - sh:description "The type of the Root Data Entity MUST be schema_org:Dataset" ; - sh:minCount 1 ; - sh:nodeKind sh:IRI ; - sh:path rdf:type ; - sh:hasValue schema_org:Dataset ; - sh:message "The Root Data Entity MUST be of type schema_org:Dataset" ; - ] ; sh:property [ a sh:PropertyShape ; sh:name "Date Published of the Root Data Entity" ; From f1472b134ac7f5e8353466a9cb2b7f95f5380edb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 26 Mar 2024 10:46:17 +0100 Subject: [PATCH 211/902] test(profiles/ro-crate): :white_check_mark: update tests for root data entity --- .../ro-crate-metadata.json | 15 +++++---------- .../missing_root_name/ro-crate-metadata.json | 15 ++++++--------- .../ro-crate/must/test_root_data_entity.py | 5 +++-- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_description/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_description/ro-crate-metadata.json index 7adda591..c5601473 100644 --- a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_description/ro-crate-metadata.json +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_description/ro-crate-metadata.json @@ -38,7 +38,9 @@ } ], "isBasedOn": "https://github.com/kikkomep/myworkflow", - "license": "MIT", + "license": { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + }, "mainEntity": { "@id": "my-workflow.ga" }, @@ -65,11 +67,7 @@ }, { "@id": "my-workflow.ga", - "@type": [ - "File", - "SoftwareSourceCode", - "ComputationalWorkflow" - ], + "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], "name": "MyWorkflow", "programmingLanguage": { "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" @@ -124,10 +122,7 @@ }, { "@id": "my-workflow-test.yml", - "@type": [ - "File", - "TestDefinition" - ], + "@type": ["File", "TestDefinition"], "conformsTo": { "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" } diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_name/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_name/ro-crate-metadata.json index 17264e95..7d6612c8 100644 --- a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_name/ro-crate-metadata.json +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_name/ro-crate-metadata.json @@ -67,11 +67,7 @@ }, { "@id": "my-workflow.ga", - "@type": [ - "File", - "SoftwareSourceCode", - "ComputationalWorkflow" - ], + "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], "name": "MyWorkflow", "programmingLanguage": { "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" @@ -126,10 +122,7 @@ }, { "@id": "my-workflow-test.yml", - "@type": [ - "File", - "TestDefinition" - ], + "@type": ["File", "TestDefinition"], "conformsTo": { "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" } @@ -151,6 +144,10 @@ "@id": "README.md", "@type": "File", "description": "Workflow documentation" + }, + { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "@type": "license" } ] } diff --git a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index f67b183a..78d3eb55 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -66,14 +66,15 @@ def test_invalid_root_type(paths): logger.debug(f"Failed requirement name: {failed_requirement.name}") # check if the failed requirement is the expected one - assert failed_requirement.name == "RO-Crate Data Entity definition", \ + assert failed_requirement.name == "RO-Crate Root Data Entity MUST exist", \ "Unexpected failed requirement" assert len(result.get_issues()) == 1, "ro-crate should have 1 issue" # ย extract issue issue = result.get_issues()[0] - assert issue.message == "The Root Data Entity MUST be of type schema_org:Dataset", \ + assert issue.message == "The file descriptor MUST have a root data entity "\ + "of type schema_org:Dataset and ending with /", \ "Unexpected issue message" From f289570296302ccedd1b5ce9b056938bf4df3260 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 26 Mar 2024 10:51:27 +0100 Subject: [PATCH 212/902] test(profiles/ro-crate): :truck: rename test file --- .../{test_file_descriptor.py => test_file_descriptor_format.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/integration/profiles/ro-crate/must/{test_file_descriptor.py => test_file_descriptor_format.py} (100%) diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py similarity index 100% rename from tests/integration/profiles/ro-crate/must/test_file_descriptor.py rename to tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py From 7eb4620c53af11ee6a5d0aae443bccab6bda4f9d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 26 Mar 2024 11:05:03 +0100 Subject: [PATCH 213/902] fix(tests): :coffin: remove unused import --- .../profiles/ro-crate/must/test_file_descriptor_format.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py index 7d2d1f99..8c220989 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py @@ -1,5 +1,4 @@ import logging -from pathlib import Path from pytest import fixture From d5bd4f3fd763f5287d490288d8e90a7f02d403fc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 26 Mar 2024 14:20:59 +0100 Subject: [PATCH 214/902] test(profiles/ro-crate): :white_check_mark: refactor entity tests to avoid code duplication --- .../must/test_file_descriptor_format.py | 168 +++------- .../ro-crate/must/test_root_data_entity.py | 292 ++++-------------- tests/shared.py | 45 +++ 3 files changed, 159 insertions(+), 346 deletions(-) create mode 100644 tests/shared.py diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py index 8c220989..99d9f0ec 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py @@ -2,8 +2,9 @@ from pytest import fixture -from rocrate_validator import models, services +from rocrate_validator import models from tests.ro_crates import InvalidFileDescriptor +from tests.shared import do_entity_test logger = logging.getLogger(__name__) @@ -20,135 +21,62 @@ def paths(): def test_missing_file_descriptor(paths): - + """Test a RO-Crate without a file descriptor.""" with paths.missing_file_descriptor as rocrate_path: - logger.debug(f"test_missing_file_descriptor: {rocrate_path}") - - result: models.ValidationResult = services.validate(rocrate_path, - requirement_level="MUST", abort_on_first=True) - assert not result.passed(), "ro-crate should be invalid" - - # check requirement - failed_requirements = result.failed_requirements - for failed_requirement in failed_requirements: - logger.debug(f"Failed requirement: {failed_requirement}") - assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" - - # set reference to failed requirement - failed_requirement = failed_requirements[0] - logger.debug(f"Failed requirement name: {failed_requirement.name}") - - # check if the failed requirement is the expected one - assert failed_requirement.name == "FileDescriptorExistence", \ - "Unexpected failed requirement" - - for issue in result.get_issues(): - logger.debug(f"Detected issue {type(issue)}: {issue.message}") + do_entity_test( + rocrate_path, + models.RequirementLevels.MUST, + False, + "FileDescriptorExistence", + [] + ) def test_not_valid_json_format(paths): - - rocrate_path = paths.invalid_json_format - logger.debug(f"test_missing_file_descriptor: {rocrate_path}") - - result: models.ValidationResult = services.validate(rocrate_path, - requirement_level="MUST", abort_on_first=True) - assert not result.passed(), "ro-crate should be invalid" - - # check requirement - failed_requirements = result.failed_requirements - for failed_requirement in failed_requirements: - logger.debug(f"Failed requirement: {failed_requirement}") - assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" - - # set reference to failed requirement - failed_requirement = failed_requirements[0] - logger.debug(f"Failed requirement name: {failed_requirement.name}") - - # check if the failed requirement is the expected one - assert failed_requirement.name == "FileDescriptorJsonFormat", \ - "Unexpected failed requirement" - - for issue in result.get_issues(): - logger.debug(f"Detected issue {type(issue)}: {issue.message}") + """Test a RO-Crate with an invalid JSON file descriptor format.""" + do_entity_test( + paths.invalid_json_format, + models.RequirementLevels.MUST, + False, + "FileDescriptorJsonFormat", + [] + ) def test_not_valid_jsonld_format_missing_context(paths): - - rocrate_path = f"{paths.invalid_jsonld_format}/missing_context" - logger.debug(f"test_missing_file_descriptor: {rocrate_path}") - - result: models.ValidationResult = services.validate(rocrate_path, - requirement_level="MUST", abort_on_first=True) - assert not result.passed(), "ro-crate should be invalid" - - # check requirement - failed_requirements = result.failed_requirements - for failed_requirement in failed_requirements: - logger.debug(f"Failed requirement: {failed_requirement}") - assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" - - # set reference to failed requirement - failed_requirement = failed_requirements[0] - logger.debug(f"Failed requirement name: {failed_requirement.name}") - - # check if the failed requirement is the expected one - assert failed_requirement.name == "FileDescriptorJsonLdFormat", \ - "Unexpected failed requirement" - - for issue in result.get_issues(): - logger.debug(f"Detected issue {type(issue)}: {issue.message}") + """Test a RO-Crate with an invalid JSON-LD file descriptor format.""" + do_entity_test( + f"{paths.invalid_jsonld_format}/missing_context", + models.RequirementLevels.MUST, + False, + "FileDescriptorJsonLdFormat", + [] + ) def test_not_valid_jsonld_format_missing_ids(paths): - - rocrate_path = f"{paths.invalid_jsonld_format}/missing_id" - logger.debug(f"test_missing_file_descriptor: {rocrate_path}") - - result: models.ValidationResult = services.validate(rocrate_path, - requirement_level="MUST", abort_on_first=True) - assert not result.passed(), "ro-crate should be invalid" - - # check requirement - failed_requirements = result.failed_requirements - for failed_requirement in failed_requirements: - logger.debug(f"Failed requirement: {failed_requirement}") - assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" - - # set reference to failed requirement - failed_requirement = failed_requirements[0] - logger.debug(f"Failed requirement name: {failed_requirement.name}") - - # check if the failed requirement is the expected one - assert failed_requirement.name == "FileDescriptorJsonLdFormat", \ - "Unexpected failed requirement" - - for issue in result.get_issues(): - logger.debug(f"Detected issue {type(issue)}: {issue.message}") + """ + Test a RO-Crate with an invalid JSON-LD file descriptor format. + One or more entities in the file descriptor do not contain the @id attribute. + """ + do_entity_test( + f"{paths.invalid_jsonld_format}/missing_id", + models.RequirementLevels.MUST, + False, + "FileDescriptorJsonLdFormat", + ["file descriptor does not contain the @id attribute"] + ) def test_not_valid_jsonld_format_missing_types(paths): - - rocrate_path = f"{paths.invalid_jsonld_format}/missing_type" - logger.debug(f"test_missing_file_descriptor: {rocrate_path}") - - result: models.ValidationResult = services.validate(rocrate_path, - requirement_level="MUST", abort_on_first=True) - assert not result.passed(), "ro-crate should be invalid" - - # check requirement - failed_requirements = result.failed_requirements - for failed_requirement in failed_requirements: - logger.debug(f"Failed requirement: {failed_requirement}") - assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" - - # set reference to failed requirement - failed_requirement = failed_requirements[0] - logger.debug(f"Failed requirement name: {failed_requirement.name}") - - # check if the failed requirement is the expected one - assert failed_requirement.name == "FileDescriptorJsonLdFormat", \ - "Unexpected failed requirement" - - for issue in result.get_issues(): - logger.debug(f"Detected issue {type(issue)}: {issue.message}") + """ + Test a RO-Crate with an invalid JSON-LD file descriptor format. + One or more entities in the file descriptor do not contain the @type attribute. + """ + do_entity_test( + f"{paths.invalid_jsonld_format}/missing_type", + models.RequirementLevels.MUST, + False, + "FileDescriptorJsonLdFormat", + ["file descriptor does not contain the @type attribute"] + ) diff --git a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index 78d3eb55..60348e19 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -2,8 +2,9 @@ from pytest import fixture -from rocrate_validator import models, services +from rocrate_validator import models from tests.ro_crates import InvalidFileDescriptor, InvalidRootDataEntity +from tests.shared import do_entity_test logger = logging.getLogger(__name__) @@ -20,250 +21,89 @@ def paths(): def test_missing_root_data_entity(paths): + """Test a RO-Crate without a root data entity.""" - rocrate_path = paths.missing_root - logger.debug(f"test_missing_file_descriptor: {rocrate_path}") - - result: models.ValidationResult = services.validate(rocrate_path, - requirement_level="MUST", abort_on_first=True) - assert not result.passed(), "ro-crate should be invalid" - - # check requirement - failed_requirements = result.failed_requirements - for failed_requirement in failed_requirements: - logger.debug(f"Failed requirement: {failed_requirement}") - assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" - - # set reference to failed requirement - failed_requirement = failed_requirements[0] - logger.debug(f"Failed requirement name: {failed_requirement.name}") - - # check if the failed requirement is the expected one - assert failed_requirement.name == "RO-Crate Root Data Entity MUST exist", \ - "Unexpected failed requirement" - - for issue in result.get_issues(): - logger.debug(f"Detected issue {type(issue)}: {issue.message}") + do_entity_test( + paths.missing_root, + models.RequirementLevels.MUST, + False, + "RO-Crate Root Data Entity MUST exist", + ["RO-Crate Root Data Entity MUST exist"] + ) def test_invalid_root_type(paths): - - rocrate_path = paths.invalid_root_type - logger.debug(f"rocrate path: {rocrate_path}") - - result: models.ValidationResult = services.validate(rocrate_path, - requirement_level="MUST", abort_on_first=True) - assert not result.passed(), "ro-crate should be invalid" - - # check requirement - failed_requirements = result.failed_requirements - for failed_requirement in failed_requirements: - logger.debug(f"Failed requirement: {failed_requirement}") - assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" - - # set reference to failed requirement - failed_requirement = failed_requirements[0] - logger.debug(f"Failed requirement name: {failed_requirement.name}") - - # check if the failed requirement is the expected one - assert failed_requirement.name == "RO-Crate Root Data Entity MUST exist", \ - "Unexpected failed requirement" - - assert len(result.get_issues()) == 1, "ro-crate should have 1 issue" - - # ย extract issue - issue = result.get_issues()[0] - assert issue.message == "The file descriptor MUST have a root data entity "\ - "of type schema_org:Dataset and ending with /", \ - "Unexpected issue message" + """Test a RO-Crate with an invalid root data entity type.""" + do_entity_test( + paths.invalid_root_type, + models.RequirementLevels.MUST, + False, + "RO-Crate Root Data Entity MUST exist", + ["The file descriptor MUST have a root data entity of type schema_org:Dataset and ending with /"] + ) def test_invalid_root_date(paths): - - rocrate_path = paths.invalid_root_date - logger.debug(f"rocrate path: {rocrate_path}") - - result: models.ValidationResult = services.validate(rocrate_path, - requirement_level="MUST", abort_on_first=True) - assert not result.passed(), "ro-crate should be invalid" - - # check requirement - failed_requirements = result.failed_requirements - for failed_requirement in failed_requirements: - logger.debug(f"Failed requirement: {failed_requirement}") - assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" - - # set reference to failed requirement - failed_requirement = failed_requirements[0] - logger.debug(f"Failed requirement name: {failed_requirement.name}") - - # check if the failed requirement is the expected one - assert failed_requirement.name == "RO-Crate Data Entity definition", \ - "Unexpected failed requirement" - - assert len(result.get_issues()) == 1, "ro-crate should have 1 issue" - - # ย extract issue - issue = result.get_issues()[0] - assert issue.message == "The datePublished of the Root Data Entity MUST be a valid ISO 8601 date", \ - "Unexpected issue message" + """Test a RO-Crate with an invalid root data entity date.""" + do_entity_test( + paths.invalid_root_date, + models.RequirementLevels.MUST, + False, + "RO-Crate Data Entity definition", + ["The datePublished of the Root Data Entity MUST be a valid ISO 8601 date"] + ) def test_missing_root_name(paths): - - rocrate_path = paths.missing_root_name - logger.debug(f"rocrate path: {rocrate_path}") - - result: models.ValidationResult = services.validate(rocrate_path, - requirement_level="SHOULD", - abort_on_first=True) - assert not result.passed(), "ro-crate should be invalid" - - # check requirement - failed_requirements = result.failed_requirements - for failed_requirement in failed_requirements: - logger.debug(f"Failed requirement: {failed_requirement}") - assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" - - # set reference to failed requirement - failed_requirement = failed_requirements[0] - logger.debug(f"Failed requirement name: {failed_requirement.name}") - - # check if the failed requirement is the expected one - assert failed_requirement.name == "RO-Crate Data Entity definition: RECOMMENDED properties", \ - "Unexpected failed requirement" - - assert len(result.get_issues()) == 1, "ro-crate should have 1 issue" - - # ย extract issue - issue = result.get_issues()[0] - assert issue.message == "The Root Data Entity SHOULD have a schema_org:name", \ - "Unexpected issue message" + """Test a RO-Crate without a root data entity name.""" + do_entity_test( + paths.missing_root_name, + models.RequirementLevels.SHOULD, + False, + "RO-Crate Data Entity definition: RECOMMENDED properties", + ["The Root Data Entity SHOULD have a schema_org:name"] + ) def test_missing_root_description(paths): - - rocrate_path = paths.missing_root_description - logger.debug(f"rocrate path: {rocrate_path}") - - result: models.ValidationResult = services.validate(rocrate_path, - requirement_level="SHOULD", - abort_on_first=True) - assert not result.passed(), "ro-crate should be invalid" - - # check requirement - failed_requirements = result.failed_requirements - for failed_requirement in failed_requirements: - logger.debug(f"Failed requirement: {failed_requirement}") - assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" - - # set reference to failed requirement - failed_requirement = failed_requirements[0] - logger.debug(f"Failed requirement name: {failed_requirement.name}") - - # check if the failed requirement is the expected one - assert failed_requirement.name == "RO-Crate Data Entity definition: RECOMMENDED properties", \ - "Unexpected failed requirement" - - assert len(result.get_issues()) == 1, "ro-crate should have 1 issue" - - # ย extract issue - issue = result.get_issues()[0] - assert issue.message == "The Root Data Entity SHOULD have a schema_org:description", \ - "Unexpected issue message" + """Test a RO-Crate without a root data entity description.""" + do_entity_test( + paths.missing_root_description, + models.RequirementLevels.SHOULD, + False, + "RO-Crate Data Entity definition: RECOMMENDED properties", + ["The Root Data Entity SHOULD have a schema_org:description"] + ) def test_missing_root_license(paths): - - rocrate_path = paths.missing_root_license - logger.debug(f"rocrate path: {rocrate_path}") - - result: models.ValidationResult = services.validate(rocrate_path, - requirement_level="SHOULD", - abort_on_first=True) - assert not result.passed(), "ro-crate should be invalid" - - # check requirement - failed_requirements = result.failed_requirements - for failed_requirement in failed_requirements: - logger.debug(f"Failed requirement: {failed_requirement}") - assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" - - # set reference to failed requirement - failed_requirement = failed_requirements[0] - logger.debug(f"Failed requirement name: {failed_requirement.name}") - - # check if the failed requirement is the expected one - assert failed_requirement.name == "RO-Crate Data Entity definition: RECOMMENDED properties", \ - "Unexpected failed requirement" - - assert len(result.get_issues()) == 1, "ro-crate should have 1 issue" - - # ย extract issue - issue = result.get_issues()[0] - assert "The Root Data Entity SHOULD have a link" in issue.message, \ - "Unexpected issue message" + """Test a RO-Crate without a root data entity license.""" + do_entity_test( + paths.missing_root_license, + models.RequirementLevels.SHOULD, + False, + "RO-Crate Data Entity definition: RECOMMENDED properties", + ["The Root Data Entity SHOULD have a link"] + ) def test_missing_root_license_name(paths): - - rocrate_path = paths.missing_root_license_name - logger.debug(f"rocrate path: {rocrate_path}") - - result: models.ValidationResult = services.validate(rocrate_path, - requirement_level="MAY", - abort_on_first=True) - assert not result.passed(), "ro-crate should be invalid" - - # check requirement - failed_requirements = result.failed_requirements - for failed_requirement in failed_requirements: - logger.debug(f"Failed requirement: {failed_requirement}") - assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" - - # set reference to failed requirement - failed_requirement = failed_requirements[0] - logger.debug(f"Failed requirement name: {failed_requirement.name}") - - # check if the failed requirement is the expected one - assert failed_requirement.name == "License definition", \ - "Unexpected failed requirement" - - assert len(result.get_issues()) == 1, "ro-crate should have 1 issue" - - # ย extract issue - issue = result.get_issues()[0] - assert "Missing license name" in issue.message, \ - "Unexpected issue message" + """Test a RO-Crate without a root data entity license name.""" + do_entity_test( + paths.missing_root_license_name, + models.RequirementLevels.MAY, + False, + "License definition", + ["Missing license name"] + ) def test_missing_root_license_description(paths): - - rocrate_path = paths.missing_root_license_description - logger.debug(f"rocrate path: {rocrate_path}") - - result: models.ValidationResult = services.validate(rocrate_path, - requirement_level="MAY", - abort_on_first=True) - assert not result.passed(), "ro-crate should be invalid" - - # check requirement - failed_requirements = result.failed_requirements - for failed_requirement in failed_requirements: - logger.debug(f"Failed requirement: {failed_requirement}") - assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" - - # set reference to failed requirement - failed_requirement = failed_requirements[0] - logger.debug(f"Failed requirement name: {failed_requirement.name}") - - # check if the failed requirement is the expected one - assert failed_requirement.name == "License definition", \ - "Unexpected failed requirement" - - assert len(result.get_issues()) == 1, "ro-crate should have 1 issue" - - # ย extract issue - issue = result.get_issues()[0] - assert "Missing license description" in issue.message, \ - "Unexpected issue message" + """Test a RO-Crate without a root data entity license description.""" + do_entity_test( + paths.missing_root_license_description, + models.RequirementLevels.MAY, + False, + "License definition", + ["Missing license description"] + ) diff --git a/tests/shared.py b/tests/shared.py new file mode 100644 index 00000000..848b5f9d --- /dev/null +++ b/tests/shared.py @@ -0,0 +1,45 @@ +import logging +from pathlib import Path +from typing import List + +from rocrate_validator import models, services + +logger = logging.getLogger(__name__) + + +def do_entity_test( + rocrate_path: Path, + requirement_level: models.RequirementType, + expected_validation_result: bool, + expected_triggered_requirement_name: str, + expected_triggered_issues: List[str] +): + logger.debug(f"Testing RO-Crate @ path: {rocrate_path}") + logger.debug(f"Requirement level: {requirement_level}") + + result: models.ValidationResult = \ + services.validate(rocrate_path, + requirement_level=requirement_level, abort_on_first=True) + logger.debug(f"Expected validation result: {expected_validation_result}") + assert result.passed() == expected_validation_result, \ + f"RO-Crate should be {'valid' if expected_validation_result else 'invalid'}" + + # check requirement + failed_requirements = result.failed_requirements + for failed_requirement in failed_requirements: + logger.debug(f"Failed requirement: {failed_requirement}") + assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" + + # set reference to failed requirement + failed_requirement = failed_requirements[0] + logger.debug(f"Failed requirement name: {failed_requirement.name}") + + # check if the failed requirement is the expected one + logger.debug(f"Expected requirement name: {expected_triggered_requirement_name}") + assert failed_requirement.name == expected_triggered_requirement_name, \ + f"Unexpected failed requirement: it MUST be {expected_triggered_requirement_name}" + + # check requirement issues + logger.debug(f"Expected issues: {expected_triggered_issues}") + for issue in result.get_issues(): + logger.debug(f"Detected issue {type(issue)}: {issue.message}") From 8f4defbc1a65fa823b4fe35cc798d1ea26c208ae Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 26 Mar 2024 14:23:02 +0100 Subject: [PATCH 215/902] refactor(profiles/ro-crate): :wrench: update test data --- .../missing_root_description/ro-crate-metadata.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_description/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_description/ro-crate-metadata.json index c5601473..d795a744 100644 --- a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_description/ro-crate-metadata.json +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_description/ro-crate-metadata.json @@ -144,6 +144,10 @@ "@id": "README.md", "@type": "File", "description": "Workflow documentation" + }, + { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "@type": "license" } ] } From e438c9343fdc9b8b70ecce58717960768629b761 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 26 Mar 2024 14:26:31 +0100 Subject: [PATCH 216/902] test(profiles/ro-crate): :white_check_mark: test missing file descriptor entity --- .../missing_entity/ro-crate-metadata.json | 143 ++++++++++++++++++ .../must/test_file_descriptor_entity.py | 27 ++++ tests/ro_crates.py | 9 ++ 3 files changed, 179 insertions(+) create mode 100644 tests/data/crates/invalid/1_file_descriptor_metadata/missing_entity/ro-crate-metadata.json create mode 100644 tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py diff --git a/tests/data/crates/invalid/1_file_descriptor_metadata/missing_entity/ro-crate-metadata.json b/tests/data/crates/invalid/1_file_descriptor_metadata/missing_entity/ro-crate-metadata.json new file mode 100644 index 00000000..0a174f69 --- /dev/null +++ b/tests/data/crates/invalid/1_file_descriptor_metadata/missing_entity/ro-crate-metadata.json @@ -0,0 +1,143 @@ +{ + "@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": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "@type": "license" + }, + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-01-22T15:36:43+00:00", + "description": "RO-Crate for MyWorkflow", + "hasPart": [ + { + "@id": "my-workflow.ga" + }, + { + "@id": "my-workflow-test.yml" + }, + { + "@id": "test-data/" + }, + { + "@id": "README.md" + } + ], + "isBasedOn": "https://github.com/kikkomep/myworkflow", + "license": { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + }, + "mainEntity": { + "@id": "my-workflow.ga" + }, + "mentions": [ + { + "@id": "#1d230a09-a465-411a-82bb-d7d4f3f1be02" + } + ], + "name": "MyWorkflow" + }, + { + "@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" + }, + { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "@type": "license" + } + ] +} diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py new file mode 100644 index 00000000..59aa4bdc --- /dev/null +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py @@ -0,0 +1,27 @@ +import logging + +from pytest import fixture + +from rocrate_validator import models +from tests.ro_crates import InvalidFileDescriptorEntity +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +@fixture(scope="module") +def paths(): + logger.debug("setup") + cls = InvalidFileDescriptorEntity() + yield cls + logger.debug("teardown") + + +def test_missing_entity(paths): + do_entity_test( + paths.missing_entity, + models.RequirementLevels.MUST, + False, + "RO-Crate Metadata File Descriptor entity MUST exist", + ["RO-Crate Metadata File Descriptor entity MUST exist"] + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index b0a21277..ebe83e5f 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -72,3 +72,12 @@ def missing_root_license_name(self) -> Path: @property def missing_root_license_description(self) -> Path: return Path(f"{self.base_path}/missing_root_license_description") + + +class InvalidFileDescriptorEntity: + + base_path = f"{INVALID_CRATES_DATA_PATH}/1_file_descriptor_metadata" + + @property + def missing_entity(self) -> Path: + return Path(f"{self.base_path}/missing_entity") From f2ce8e880e454d91b7221953da01a99fe07c4718 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 26 Mar 2024 15:06:16 +0100 Subject: [PATCH 217/902] docs(tests): :memo: add description comment --- .../profiles/ro-crate/must/test_file_descriptor_entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py index 59aa4bdc..bfa26714 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py @@ -18,6 +18,7 @@ def paths(): def test_missing_entity(paths): + """Test a RO-Crate without a file descriptor entity.""" do_entity_test( paths.missing_entity, models.RequirementLevels.MUST, From ec09f75f81474c42403ff7ff09613b1c530abd31 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 26 Mar 2024 15:08:18 +0100 Subject: [PATCH 218/902] feat(profiles/ro-crate/must): :sparkles: redefine shape to check existence of file descriptor entity --- .../ro-crate/must/1_file-descriptor_metadata.ttl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index e3511c91..0ee7b497 100644 --- a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -9,13 +9,18 @@ :FileDescriptorExistence a sh:NodeShape ; - sh:name "RO-Crate Metadata File Descriptor MUST exist" ; - sh:description "The root of the document MUST have a node with ID ro-crate-metadata.json" ; + sh:name "RO-Crate Metadata File Descriptor entity MUST exist" ; + sh:description "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; sh:targetNode :ro-crate-metadata.json ; sh:property [ a sh:PropertyShape ; - sh:name "Check metadata file existence" ; - sh:description "Check if the RO-Crate metadata file exists" ; + sh:name "Check Metadata File Descriptor entity existence" ; + sh:description "Check if the RO-Crate Metadata File Descriptor entity exists" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + ] . + sh:path rdf:type ; sh:minCount 1 ; sh:message "The RO-Crate metadata file descriptor MUST exist" ; From 25f68de6852625cfe2358fb07214b06d6be92352 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 26 Mar 2024 16:46:46 +0100 Subject: [PATCH 219/902] test(profiles/ro-crate/must): :white_check_mark: test invalide file descriptor type --- .../ro-crate-metadata.json | 154 ++++++++++++++++++ .../must/test_file_descriptor_entity.py | 11 ++ tests/ro_crates.py | 4 + 3 files changed, 169 insertions(+) create mode 100644 tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_type/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_type/ro-crate-metadata.json b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_type/ro-crate-metadata.json new file mode 100644 index 00000000..a4f92d6f --- /dev/null +++ b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_type/ro-crate-metadata.json @@ -0,0 +1,154 @@ +{ + "@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": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "@type": "license" + }, + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-01-22T15:36:43+00:00", + "description": "RO-Crate for MyWorkflow", + "hasPart": [ + { + "@id": "my-workflow.ga" + }, + { + "@id": "my-workflow-test.yml" + }, + { + "@id": "test-data/" + }, + { + "@id": "README.md" + } + ], + "isBasedOn": "https://github.com/kikkomep/myworkflow", + "license": { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + }, + "mainEntity": { + "@id": "my-workflow.ga" + }, + "mentions": [ + { + "@id": "#1d230a09-a465-411a-82bb-d7d4f3f1be02" + } + ], + "name": "MyWorkflow" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWorkInvalid", + "about": { + "@id": "./" + }, + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" + } + ] + }, + { + "@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/must/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py index bfa26714..fb1a86fe 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py @@ -26,3 +26,14 @@ def test_missing_entity(paths): "RO-Crate Metadata File Descriptor entity MUST exist", ["RO-Crate Metadata File Descriptor entity MUST exist"] ) + + +def test_invalid_entity_type(paths): + """Test a RO-Crate with an invalid file descriptor entity type.""" + do_entity_test( + paths.invalid_entity_type, + models.RequirementLevels.MUST, + False, + "RO-Crate Metadata File Descriptor: recommended properties", + ["The RO-Crate metadata file MUST be a CreativeWork, as per schema.org"] + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index ebe83e5f..c1c7873f 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -81,3 +81,7 @@ class InvalidFileDescriptorEntity: @property def missing_entity(self) -> Path: return Path(f"{self.base_path}/missing_entity") + + @property + def invalid_entity_type(self) -> Path: + return Path(f"{self.base_path}/invalid_entity_type") From 7f52b4edbb4cc20e5725305695a7732733437a2a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 26 Mar 2024 17:01:58 +0100 Subject: [PATCH 220/902] feat(profiles/ro-crate/must): :sparkles: add shape to validate type of file descriptor entity --- .../ro-crate/must/1_file-descriptor_metadata.ttl | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index 0ee7b497..17cd375b 100644 --- a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -21,7 +21,22 @@ sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; ] . +:MetadataFileDescriptorDefinition a sh:NodeShape ; + sh:name "RO-Crate Metadata File Descriptor: recommended properties" ; + sh:description """RO-Crate Metadata Descriptor MUST be defined + according with the requirements details defined in + """; + sh:targetNode :ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check if the Metadata File Descriptor is a CreativeWork" ; + sh:description "The RO-Crate metadata file MUST be a 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:minCount 1 ; sh:message "The RO-Crate metadata file descriptor MUST exist" ; ] . From d9e566479fda1d3e478023f9de0075940b0a529d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 26 Mar 2024 17:06:46 +0100 Subject: [PATCH 221/902] feat(profiles/ro-crate/must): :sparkles: add property shape to validate prop `about` of file descriptor entity --- profiles/ro-crate/must/1_file-descriptor_metadata.ttl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index 17cd375b..3a03f086 100644 --- a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -36,6 +36,17 @@ 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 "about property MUST exist" ; + sh:description """The URL of the RO-Crate metadata file itself, which is the root of the RO-Crate. + This is used to identify the RO-Crate, and is the only property that is required to be present in the metadata file.""" ; + sh:maxCount 1; + sh:minCount 1 ; + sh:nodeKind sh:IRI ; + sh:path schema_org:about ; + sh:message "The RO-Crate metadata file descriptor MUST have an `about` property" ; ] . sh:minCount 1 ; sh:message "The RO-Crate metadata file descriptor MUST exist" ; From 03b443ea0e5b5b23823a1b758bf6b6e85f79a3b6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 26 Mar 2024 17:14:34 +0100 Subject: [PATCH 222/902] test(profiles/ro-crate/must): :white_check_mark: tests for the `about` of the file descriptor entity --- .../ro-crate-metadata.json | 155 ++++++++++++++++++ .../ro-crate-metadata.json | 154 +++++++++++++++++ .../must/test_file_descriptor_entity.py | 22 +++ tests/ro_crates.py | 8 + 4 files changed, 339 insertions(+) create mode 100644 tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about_type/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/1_file_descriptor_metadata/missing_entity_about/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about_type/ro-crate-metadata.json b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about_type/ro-crate-metadata.json new file mode 100644 index 00000000..27911616 --- /dev/null +++ b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about_type/ro-crate-metadata.json @@ -0,0 +1,155 @@ +{ + "@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": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "@type": "license" + }, + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-01-22T15:36:43+00:00", + "description": "RO-Crate for MyWorkflow", + "hasPart": [ + { + "@id": "my-workflow.ga" + }, + { + "@id": "my-workflow-test.yml" + }, + { + "@id": "test-data/" + }, + { + "@id": "README.md" + } + ], + "isBasedOn": "https://github.com/kikkomep/myworkflow", + "license": { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + }, + "mainEntity": { + "@id": "my-workflow.ga" + }, + "mentions": [ + { + "@id": "#1d230a09-a465-411a-82bb-d7d4f3f1be02" + } + ], + "name": "MyWorkflow" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWorkInvalid", + "about": { + "@id": "./", + "@type": "CreativeWork" + }, + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" + } + ] + }, + { + "@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/data/crates/invalid/1_file_descriptor_metadata/missing_entity_about/ro-crate-metadata.json b/tests/data/crates/invalid/1_file_descriptor_metadata/missing_entity_about/ro-crate-metadata.json new file mode 100644 index 00000000..ec69b5d9 --- /dev/null +++ b/tests/data/crates/invalid/1_file_descriptor_metadata/missing_entity_about/ro-crate-metadata.json @@ -0,0 +1,154 @@ +{ + "@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": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "@type": "license" + }, + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-01-22T15:36:43+00:00", + "description": "RO-Crate for MyWorkflow", + "hasPart": [ + { + "@id": "my-workflow.ga" + }, + { + "@id": "my-workflow-test.yml" + }, + { + "@id": "test-data/" + }, + { + "@id": "README.md" + } + ], + "isBasedOn": "https://github.com/kikkomep/myworkflow", + "license": { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + }, + "mainEntity": { + "@id": "my-workflow.ga" + }, + "mentions": [ + { + "@id": "#1d230a09-a465-411a-82bb-d7d4f3f1be02" + } + ], + "name": "MyWorkflow" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWorkInvalid", + "missing-about": { + "@id": "./" + }, + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" + } + ] + }, + { + "@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/must/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py index fb1a86fe..c12ec7da 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py @@ -37,3 +37,25 @@ def test_invalid_entity_type(paths): "RO-Crate Metadata File Descriptor: recommended properties", ["The RO-Crate metadata file MUST be a CreativeWork, as per schema.org"] ) + + +def test_missing_entity_about(paths): + """Test a RO-Crate with an invalid file descriptor entity type.""" + do_entity_test( + paths.missing_entity_about, + models.RequirementLevels.MUST, + False, + "RO-Crate Metadata File Descriptor: recommended properties", + ["The RO-Crate metadata file descriptor MUST have an `about` property"] + ) + + +def test_invalid_entity_about_type(paths): + """Test a RO-Crate with an invalid file descriptor entity type.""" + do_entity_test( + paths.invalid_entity_about_type, + models.RequirementLevels.MUST, + False, + "RO-Crate Metadata File Descriptor: recommended properties", + ["The RO-Crate metadata file descriptor MUST have an `about` property"] + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index c1c7873f..938aa2a3 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -85,3 +85,11 @@ def missing_entity(self) -> Path: @property def invalid_entity_type(self) -> Path: return Path(f"{self.base_path}/invalid_entity_type") + + @property + def missing_entity_about(self) -> Path: + return Path(f"{self.base_path}/missing_entity_about") + + @property + def invalid_entity_about_type(self) -> Path: + return Path(f"{self.base_path}/invalid_entity_about_type") From 2d43ec866236107237523215b30344448bf76000 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 26 Mar 2024 17:30:25 +0100 Subject: [PATCH 223/902] test(core): :wrench: configure poetry to run tests --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 5c468f32..b7003aeb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,3 +35,6 @@ rocrate-validator = "rocrate_validator.cli:cli" [tool.rocrate_validator] skip_dirs = [".git", ".github", ".vscode"] + +[tool.pytest.ini_options] +testpaths = ["tests"] From 077aca3e0b7fa5a3f211c6218937c9051005278b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 27 Mar 2024 11:56:05 +0100 Subject: [PATCH 224/902] refactor(tests): :recycle: update shared function to test RO-Crate entities --- .../must/test_file_descriptor_entity.py | 2 +- tests/shared.py | 85 ++++++++++++------- 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py index c12ec7da..7b26b818 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py @@ -24,7 +24,7 @@ def test_missing_entity(paths): models.RequirementLevels.MUST, False, "RO-Crate Metadata File Descriptor entity MUST exist", - ["RO-Crate Metadata File Descriptor entity MUST exist"] + ["The root of the document MUST have an entity with @id `ro-crate-metadata.json`"] ) diff --git a/tests/shared.py b/tests/shared.py index 848b5f9d..26fec02b 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -1,3 +1,7 @@ +""" +Library of shared functions for testing RO-Crate profiles +""" + import logging from pathlib import Path from typing import List @@ -11,35 +15,56 @@ def do_entity_test( rocrate_path: Path, requirement_level: models.RequirementType, expected_validation_result: bool, - expected_triggered_requirement_name: str, + expected_triggered_requirements: List[str], expected_triggered_issues: List[str] ): - logger.debug(f"Testing RO-Crate @ path: {rocrate_path}") - logger.debug(f"Requirement level: {requirement_level}") - - result: models.ValidationResult = \ - services.validate(rocrate_path, - requirement_level=requirement_level, abort_on_first=True) - logger.debug(f"Expected validation result: {expected_validation_result}") - assert result.passed() == expected_validation_result, \ - f"RO-Crate should be {'valid' if expected_validation_result else 'invalid'}" - - # check requirement - failed_requirements = result.failed_requirements - for failed_requirement in failed_requirements: - logger.debug(f"Failed requirement: {failed_requirement}") - assert len(failed_requirements) == 1, "ro-crate should have 1 failed requirement" - - # set reference to failed requirement - failed_requirement = failed_requirements[0] - logger.debug(f"Failed requirement name: {failed_requirement.name}") - - # check if the failed requirement is the expected one - logger.debug(f"Expected requirement name: {expected_triggered_requirement_name}") - assert failed_requirement.name == expected_triggered_requirement_name, \ - f"Unexpected failed requirement: it MUST be {expected_triggered_requirement_name}" - - # check requirement issues - logger.debug(f"Expected issues: {expected_triggered_issues}") - for issue in result.get_issues(): - logger.debug(f"Detected issue {type(issue)}: {issue.message}") + """ + Shared function to test a RO-Crate entity + """ + # declare variables + failed_requirements = None + detected_issues = None + + try: + logger.debug("Testing RO-Crate @ path: %s", rocrate_path) + logger.debug("Requirement level: %s", requirement_level) + + result: models.ValidationResult = \ + services.validate(rocrate_path, + requirement_level=requirement_level, + abort_on_first=len(expected_triggered_requirements) == 1 + or len(expected_triggered_issues) == 1) + logger.debug("Expected validation result: %s", expected_validation_result) + assert result.passed() == expected_validation_result, \ + f"RO-Crate should be {'valid' if expected_validation_result else 'invalid'}" + + # check requirement + failed_requirements = [_.name for _ in result.failed_requirements] + assert len(failed_requirements) == len(expected_triggered_requirements), \ + f"Expected {len(expected_triggered_requirements)} requirements to be "\ + f"triggered, but got {len(failed_requirements)}" + + # check that the expected requirements are triggered + for expected_triggered_requirement in expected_triggered_requirements: + if expected_triggered_requirement not in failed_requirements: + assert False, f"The expected requirement " \ + f"\"{expected_triggered_requirement}\" was not found in the failed requirements" + + # check requirement issues + detected_issues = [issue.message for issue in result.get_issues()] + logger.debug("Detected issues: %s", detected_issues) + logger.debug("Expected issues: %s", expected_triggered_issues) + for expected_issue in expected_triggered_issues: + if not any(expected_issue in issue for issue in detected_issues): # support partial match + assert False, f"The expected issue \"{expected_issue}\" was not found in the detected issues" + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + logger.debug("Failed to validate RO-Crate @ path: %s", rocrate_path) + logger.debug("Requirement level: %s", requirement_level) + logger.debug("Expected validation result: %s", expected_validation_result) + logger.debug("Expected triggered requirements: %s", expected_triggered_requirements) + logger.debug("Expected triggered issues: %s", expected_triggered_issues) + logger.debug("Failed requirements: %s", failed_requirements) + logger.debug("Detected issues: %s", detected_issues) + raise e From 9c92ffcbe66485fbd068e760eb90d2a7fc5d1e7a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 27 Mar 2024 12:32:11 +0100 Subject: [PATCH 225/902] test(profiles/ro-crate): :white_check_mark: refactor tests --- .../must/test_file_descriptor_entity.py | 34 +++++------ .../must/test_file_descriptor_format.py | 33 ++++------- .../ro-crate/must/test_root_data_entity.py | 59 +++++++------------ 3 files changed, 47 insertions(+), 79 deletions(-) diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py index 7b26b818..ddc84336 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py @@ -1,61 +1,57 @@ import logging -from pytest import fixture - from rocrate_validator import models from tests.ro_crates import InvalidFileDescriptorEntity from tests.shared import do_entity_test +# set up logging logger = logging.getLogger(__name__) - -@fixture(scope="module") -def paths(): - logger.debug("setup") - cls = InvalidFileDescriptorEntity() - yield cls - logger.debug("teardown") +# ย Global set up the paths +paths = InvalidFileDescriptorEntity() -def test_missing_entity(paths): +def test_missing_entity(): """Test a RO-Crate without a file descriptor entity.""" do_entity_test( paths.missing_entity, models.RequirementLevels.MUST, False, - "RO-Crate Metadata File Descriptor entity MUST exist", + ["RO-Crate Metadata File Descriptor entity MUST exist"], ["The root of the document MUST have an entity with @id `ro-crate-metadata.json`"] ) -def test_invalid_entity_type(paths): +def test_invalid_entity_type(): """Test a RO-Crate with an invalid file descriptor entity type.""" do_entity_test( paths.invalid_entity_type, models.RequirementLevels.MUST, False, - "RO-Crate Metadata File Descriptor: recommended properties", + ["RO-Crate Metadata File Descriptor: recommended properties"], ["The RO-Crate metadata file MUST be a CreativeWork, as per schema.org"] ) -def test_missing_entity_about(paths): +def test_missing_entity_about(): """Test a RO-Crate with an invalid file descriptor entity type.""" do_entity_test( paths.missing_entity_about, models.RequirementLevels.MUST, False, - "RO-Crate Metadata File Descriptor: recommended properties", - ["The RO-Crate metadata file descriptor MUST have an `about` property"] + ["RO-Crate Metadata File Descriptor: recommended properties"], + ["The RO-Crate metadata file MUST be a CreativeWork, as per schema.org", + "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity"] ) -def test_invalid_entity_about_type(paths): +def test_invalid_entity_about_type(): """Test a RO-Crate with an invalid file descriptor entity type.""" do_entity_test( paths.invalid_entity_about_type, models.RequirementLevels.MUST, False, - "RO-Crate Metadata File Descriptor: recommended properties", - ["The RO-Crate metadata file descriptor MUST have an `about` property"] + ["RO-Crate Metadata File Descriptor: recommended properties"], + ["The RO-Crate metadata file MUST be a CreativeWork, as per schema.org", + "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity"] ) diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py index 99d9f0ec..95993679 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py @@ -1,7 +1,5 @@ import logging -from pytest import fixture - from rocrate_validator import models from tests.ro_crates import InvalidFileDescriptor from tests.shared import do_entity_test @@ -9,52 +7,45 @@ logger = logging.getLogger(__name__) -logger.debug(InvalidFileDescriptor.missing_file_descriptor) - - -@fixture(scope="module") -def paths(): - logger.debug("setup") - cls = InvalidFileDescriptor() - yield cls - logger.debug("teardown") +# ย Global set up the paths +paths = InvalidFileDescriptor() -def test_missing_file_descriptor(paths): +def test_missing_file_descriptor(): """Test a RO-Crate without a file descriptor.""" with paths.missing_file_descriptor as rocrate_path: do_entity_test( rocrate_path, models.RequirementLevels.MUST, False, - "FileDescriptorExistence", + ["FileDescriptorExistence"], [] ) -def test_not_valid_json_format(paths): +def test_not_valid_json_format(): """Test a RO-Crate with an invalid JSON file descriptor format.""" do_entity_test( paths.invalid_json_format, models.RequirementLevels.MUST, False, - "FileDescriptorJsonFormat", + ["FileDescriptorJsonFormat"], [] ) -def test_not_valid_jsonld_format_missing_context(paths): +def test_not_valid_jsonld_format_missing_context(): """Test a RO-Crate with an invalid JSON-LD file descriptor format.""" do_entity_test( f"{paths.invalid_jsonld_format}/missing_context", models.RequirementLevels.MUST, False, - "FileDescriptorJsonLdFormat", + ["FileDescriptorJsonLdFormat"], [] ) -def test_not_valid_jsonld_format_missing_ids(paths): +def test_not_valid_jsonld_format_missing_ids(): """ Test a RO-Crate with an invalid JSON-LD file descriptor format. One or more entities in the file descriptor do not contain the @id attribute. @@ -63,12 +54,12 @@ def test_not_valid_jsonld_format_missing_ids(paths): f"{paths.invalid_jsonld_format}/missing_id", models.RequirementLevels.MUST, False, - "FileDescriptorJsonLdFormat", + ["FileDescriptorJsonLdFormat"], ["file descriptor does not contain the @id attribute"] ) -def test_not_valid_jsonld_format_missing_types(paths): +def test_not_valid_jsonld_format_missing_types(): """ Test a RO-Crate with an invalid JSON-LD file descriptor format. One or more entities in the file descriptor do not contain the @type attribute. @@ -77,6 +68,6 @@ def test_not_valid_jsonld_format_missing_types(paths): f"{paths.invalid_jsonld_format}/missing_type", models.RequirementLevels.MUST, False, - "FileDescriptorJsonLdFormat", + ["FileDescriptorJsonLdFormat"], ["file descriptor does not contain the @type attribute"] ) diff --git a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index 60348e19..a0bdc19d 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -1,109 +1,90 @@ import logging -from pytest import fixture - from rocrate_validator import models -from tests.ro_crates import InvalidFileDescriptor, InvalidRootDataEntity +from tests.ro_crates import InvalidRootDataEntity from tests.shared import do_entity_test +# set up logging logger = logging.getLogger(__name__) -logger.debug(InvalidFileDescriptor.missing_file_descriptor) - - -@fixture(scope="module") -def paths(): - logger.debug("setup") - cls = InvalidRootDataEntity() - yield cls - logger.debug("teardown") +# ย Global set up the paths +paths = InvalidRootDataEntity() -def test_missing_root_data_entity(paths): +def test_missing_root_data_entity(): """Test a RO-Crate without a root data entity.""" - - do_entity_test( - paths.missing_root, - models.RequirementLevels.MUST, - False, - "RO-Crate Root Data Entity MUST exist", - ["RO-Crate Root Data Entity MUST exist"] - ) - - -def test_invalid_root_type(paths): - """Test a RO-Crate with an invalid root data entity type.""" do_entity_test( paths.invalid_root_type, models.RequirementLevels.MUST, False, - "RO-Crate Root Data Entity MUST exist", + ["RO-Crate Root Data Entity MUST exist", + "RO-Crate Metadata File Descriptor: recommended properties"], ["The file descriptor MUST have a root data entity of type schema_org:Dataset and ending with /"] ) -def test_invalid_root_date(paths): +def test_invalid_root_date(): """Test a RO-Crate with an invalid root data entity date.""" do_entity_test( paths.invalid_root_date, models.RequirementLevels.MUST, False, - "RO-Crate Data Entity definition", + ["RO-Crate Data Entity definition"], ["The datePublished of the Root Data Entity MUST be a valid ISO 8601 date"] ) -def test_missing_root_name(paths): +def test_missing_root_name(): """Test a RO-Crate without a root data entity name.""" do_entity_test( paths.missing_root_name, models.RequirementLevels.SHOULD, False, - "RO-Crate Data Entity definition: RECOMMENDED properties", + ["RO-Crate Data Entity definition: RECOMMENDED properties"], ["The Root Data Entity SHOULD have a schema_org:name"] ) -def test_missing_root_description(paths): +def test_missing_root_description(): """Test a RO-Crate without a root data entity description.""" do_entity_test( paths.missing_root_description, models.RequirementLevels.SHOULD, False, - "RO-Crate Data Entity definition: RECOMMENDED properties", + ["RO-Crate Data Entity definition: RECOMMENDED properties"], ["The Root Data Entity SHOULD have a schema_org:description"] ) -def test_missing_root_license(paths): +def test_missing_root_license(): """Test a RO-Crate without a root data entity license.""" do_entity_test( paths.missing_root_license, models.RequirementLevels.SHOULD, False, - "RO-Crate Data Entity definition: RECOMMENDED properties", - ["The Root Data Entity SHOULD have a link"] + ["RO-Crate Data Entity definition: RECOMMENDED properties"], + ["The Root Data Entity SHOULD have a link to a Contextual Entity representing the schema_org:license type"] ) -def test_missing_root_license_name(paths): +def test_missing_root_license_name(): """Test a RO-Crate without a root data entity license name.""" do_entity_test( paths.missing_root_license_name, models.RequirementLevels.MAY, False, - "License definition", + ["License definition"], ["Missing license name"] ) -def test_missing_root_license_description(paths): +def test_missing_root_license_description(): """Test a RO-Crate without a root data entity license description.""" do_entity_test( paths.missing_root_license_description, models.RequirementLevels.MAY, False, - "License definition", + ["License definition"], ["Missing license description"] ) From 1d80fd14b4c9486701cbff90bdb3e0be05a5b0a6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 27 Mar 2024 12:34:13 +0100 Subject: [PATCH 226/902] refactor(profiles/ro-crate/must): :recycle: minor changes to the MetadataFileDescriptorDefinition --- profiles/ro-crate/must/1_file-descriptor_metadata.ttl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index 3a03f086..a542dc09 100644 --- a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -39,13 +39,15 @@ ] ; sh:property [ a sh:PropertyShape ; - sh:name "about property MUST exist" ; + sh:name "Check if the `Metadata File Descriptor` has the `about` property" ; sh:description """The URL of the RO-Crate metadata file itself, which is the root of the RO-Crate. This is used to identify the RO-Crate, and is the only property that is required to be present in the metadata file.""" ; sh:maxCount 1; sh:minCount 1 ; - sh:nodeKind sh:IRI ; + sh:nodeKind sh:NodeIRI ; sh:path schema_org:about ; + sh:class schema_org:Dataset ; + sh:message "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity" ; sh:message "The RO-Crate metadata file descriptor MUST have an `about` property" ; ] . sh:minCount 1 ; From fad53f26dce869ccc28820f1029fb83437f024e3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 27 Mar 2024 14:48:35 +0100 Subject: [PATCH 227/902] fix(profiles/ro-crate/must): :bug: fix nodeKind of about property --- profiles/ro-crate/must/1_file-descriptor_metadata.ttl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index a542dc09..887d6b84 100644 --- a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -44,7 +44,7 @@ This is used to identify the RO-Crate, and is the only property that is required to be present in the metadata file.""" ; sh:maxCount 1; sh:minCount 1 ; - sh:nodeKind sh:NodeIRI ; + sh:nodeKind sh:IRI ; sh:path schema_org:about ; sh:class schema_org:Dataset ; sh:message "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity" ; From 9af60c9cc8dfe404d14fa219a9e1f0ed39ce1784 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 27 Mar 2024 15:04:52 +0100 Subject: [PATCH 228/902] test(utils): :sparkles: add `abort_on_first` parameter on test utility fn --- tests/shared.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/shared.py b/tests/shared.py index 26fec02b..a05e994c 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -16,7 +16,8 @@ def do_entity_test( requirement_level: models.RequirementType, expected_validation_result: bool, expected_triggered_requirements: List[str], - expected_triggered_issues: List[str] + expected_triggered_issues: List[str], + abort_on_first: bool = True ): """ Shared function to test a RO-Crate entity @@ -29,11 +30,16 @@ def do_entity_test( logger.debug("Testing RO-Crate @ path: %s", rocrate_path) logger.debug("Requirement level: %s", requirement_level) + # set abort_on_first to False if there are multiple expected requirements or issues + if len(expected_triggered_requirements) > 1 \ + or len(expected_triggered_issues) > 1: + abort_on_first = False + + # validate RO-Crate result: models.ValidationResult = \ services.validate(rocrate_path, requirement_level=requirement_level, - abort_on_first=len(expected_triggered_requirements) == 1 - or len(expected_triggered_issues) == 1) + abort_on_first=abort_on_first) logger.debug("Expected validation result: %s", expected_validation_result) assert result.passed() == expected_validation_result, \ f"RO-Crate should be {'valid' if expected_validation_result else 'invalid'}" From b00136cf205dae3761e4117c8c3d6fcd1d85bd21 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 27 Mar 2024 15:09:59 +0100 Subject: [PATCH 229/902] feat(shacl): :sparkles: add shape to validate the conformsTo property --- .../ro-crate/must/1_file-descriptor_metadata.ttl | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index 887d6b84..6de4ebdc 100644 --- a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -48,9 +48,15 @@ sh:path schema_org:about ; sh:class schema_org:Dataset ; sh:message "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity" ; - sh:message "The RO-Crate metadata file descriptor MUST have an `about` property" ; - ] . + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "conformsTo property" ; + sh:description "The RO-Crate specification version that this crate conforms to" ; sh:minCount 1 ; - sh:message "The RO-Crate metadata file descriptor MUST exist" ; + sh:nodeKind sh:IRI ; + sh:path dct:conformsTo ; + sh:in ( ) ; + sh:message "The RO-Crate metadata file descriptor MUST have a `conformsTo` property with the RO-Crate specification version" ; ] . - + From cadf5e84be1d0412be7a1091aa083d0eee54d09a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 27 Mar 2024 15:11:49 +0100 Subject: [PATCH 230/902] test(profiles/ro-crate/must): :white_check_mark: test the conformsTo property --- .../ro-crate-metadata.json | 158 ++++++++++++++++++ .../ro-crate-metadata.json | 158 ++++++++++++++++++ .../must/test_file_descriptor_entity.py | 24 +++ tests/ro_crates.py | 8 + 4 files changed, 348 insertions(+) create mode 100644 tests/data/crates/invalid/1_file_descriptor_metadata/invalid_conforms_to/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/1_file_descriptor_metadata/missing_conforms_to/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_conforms_to/ro-crate-metadata.json b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_conforms_to/ro-crate-metadata.json new file mode 100644 index 00000000..cbd1b71d --- /dev/null +++ b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_conforms_to/ro-crate-metadata.json @@ -0,0 +1,158 @@ +{ + "@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": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "@type": "license" + }, + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-01-22T15:36:43+00:00", + "description": "RO-Crate for MyWorkflow", + "hasPart": [ + { + "@id": "my-workflow.ga" + }, + { + "@id": "my-workflow-test.yml" + }, + { + "@id": "test-data/" + }, + { + "@id": "README.md" + } + ], + "isBasedOn": "https://github.com/kikkomep/myworkflow", + "license": { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + }, + "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-invalid" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" + } + ] + }, + { + "@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" + }, + { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "@type": "license" + } + ] +} diff --git a/tests/data/crates/invalid/1_file_descriptor_metadata/missing_conforms_to/ro-crate-metadata.json b/tests/data/crates/invalid/1_file_descriptor_metadata/missing_conforms_to/ro-crate-metadata.json new file mode 100644 index 00000000..e8a9424d --- /dev/null +++ b/tests/data/crates/invalid/1_file_descriptor_metadata/missing_conforms_to/ro-crate-metadata.json @@ -0,0 +1,158 @@ +{ + "@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": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "@type": "license" + }, + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-01-22T15:36:43+00:00", + "description": "RO-Crate for MyWorkflow", + "hasPart": [ + { + "@id": "my-workflow.ga" + }, + { + "@id": "my-workflow-test.yml" + }, + { + "@id": "test-data/" + }, + { + "@id": "README.md" + } + ], + "isBasedOn": "https://github.com/kikkomep/myworkflow", + "license": { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + }, + "mainEntity": { + "@id": "my-workflow.ga" + }, + "mentions": [ + { + "@id": "#1d230a09-a465-411a-82bb-d7d4f3f1be02" + } + ], + "name": "MyWorkflow" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "missing-conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" + } + ] + }, + { + "@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" + }, + { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "@type": "license" + } + ] +} diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py index ddc84336..a704ffe9 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py @@ -55,3 +55,27 @@ def test_invalid_entity_about_type(): ["The RO-Crate metadata file MUST be a CreativeWork, as per schema.org", "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity"] ) + + +def test_missing_conforms_to(): + """Test a RO-Crate with an invalid file descriptor entity type.""" + do_entity_test( + paths.missing_conforms_to, + models.RequirementLevels.MUST, + False, + ["RO-Crate Metadata File Descriptor: recommended properties"], + ["The RO-Crate metadata file descriptor MUST have a `conformsTo` " + "property with the RO-Crate specification version"] + ) + + +def test_invalid_conforms_to(): + """Test a RO-Crate with an invalid file descriptor entity type.""" + do_entity_test( + paths.invalid_conforms_to, + models.RequirementLevels.MUST, + False, + ["RO-Crate Metadata File Descriptor: recommended properties"], + ["The RO-Crate metadata file descriptor MUST have a `conformsTo` " + "property with the RO-Crate specification version"] + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 938aa2a3..63f53e6d 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -93,3 +93,11 @@ def missing_entity_about(self) -> Path: @property def invalid_entity_about_type(self) -> Path: return Path(f"{self.base_path}/invalid_entity_about_type") + + @property + def missing_conforms_to(self) -> Path: + return Path(f"{self.base_path}/missing_conforms_to") + + @property + def invalid_conforms_to(self) -> Path: + return Path(f"{self.base_path}/invalid_conforms_to") From 9d0f6c75fa431483af4236812c4651459ddb1f36 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 27 Mar 2024 15:23:04 +0100 Subject: [PATCH 231/902] test(profiles): :card_file_box: update test data --- .../invalid_conforms_to/ro-crate-metadata.json | 3 --- .../invalid_entity_about_type/ro-crate-metadata.json | 6 +----- .../invalid_entity_type/ro-crate-metadata.json | 3 --- .../missing_conforms_to/ro-crate-metadata.json | 3 --- .../missing_entity_about/ro-crate-metadata.json | 3 --- .../invalid_root_date/ro-crate-metadata.json | 3 --- .../missing_root_description/ro-crate-metadata.json | 3 --- .../missing_root_entity/ro-crate-metadata.json | 3 --- .../missing_root_license/ro-crate-metadata.json | 3 --- .../missing_root_license_description/ro-crate-metadata.json | 3 --- .../missing_root_license_name/ro-crate-metadata.json | 3 --- .../missing_root_name/ro-crate-metadata.json | 3 --- 12 files changed, 1 insertion(+), 38 deletions(-) diff --git a/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_conforms_to/ro-crate-metadata.json b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_conforms_to/ro-crate-metadata.json index cbd1b71d..860a50a6 100644 --- a/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_conforms_to/ro-crate-metadata.json +++ b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_conforms_to/ro-crate-metadata.json @@ -64,9 +64,6 @@ "conformsTo": [ { "@id": "https://w3id.org/ro/crate/1.1-invalid" - }, - { - "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" } ] }, diff --git a/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about_type/ro-crate-metadata.json b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about_type/ro-crate-metadata.json index 27911616..107a46ae 100644 --- a/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about_type/ro-crate-metadata.json +++ b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about_type/ro-crate-metadata.json @@ -59,15 +59,11 @@ "@id": "ro-crate-metadata.json", "@type": "CreativeWorkInvalid", "about": { - "@id": "./", - "@type": "CreativeWork" + "@id": "my-workflow.ga" }, "conformsTo": [ { "@id": "https://w3id.org/ro/crate/1.1" - }, - { - "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" } ] }, diff --git a/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_type/ro-crate-metadata.json b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_type/ro-crate-metadata.json index a4f92d6f..993e7e7d 100644 --- a/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_type/ro-crate-metadata.json +++ b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_type/ro-crate-metadata.json @@ -64,9 +64,6 @@ "conformsTo": [ { "@id": "https://w3id.org/ro/crate/1.1" - }, - { - "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" } ] }, diff --git a/tests/data/crates/invalid/1_file_descriptor_metadata/missing_conforms_to/ro-crate-metadata.json b/tests/data/crates/invalid/1_file_descriptor_metadata/missing_conforms_to/ro-crate-metadata.json index e8a9424d..aac08965 100644 --- a/tests/data/crates/invalid/1_file_descriptor_metadata/missing_conforms_to/ro-crate-metadata.json +++ b/tests/data/crates/invalid/1_file_descriptor_metadata/missing_conforms_to/ro-crate-metadata.json @@ -64,9 +64,6 @@ "missing-conformsTo": [ { "@id": "https://w3id.org/ro/crate/1.1" - }, - { - "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" } ] }, diff --git a/tests/data/crates/invalid/1_file_descriptor_metadata/missing_entity_about/ro-crate-metadata.json b/tests/data/crates/invalid/1_file_descriptor_metadata/missing_entity_about/ro-crate-metadata.json index ec69b5d9..172bb709 100644 --- a/tests/data/crates/invalid/1_file_descriptor_metadata/missing_entity_about/ro-crate-metadata.json +++ b/tests/data/crates/invalid/1_file_descriptor_metadata/missing_entity_about/ro-crate-metadata.json @@ -64,9 +64,6 @@ "conformsTo": [ { "@id": "https://w3id.org/ro/crate/1.1" - }, - { - "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" } ] }, diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_date/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_date/ro-crate-metadata.json index a971825c..649c3317 100644 --- a/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_date/ro-crate-metadata.json +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_date/ro-crate-metadata.json @@ -57,9 +57,6 @@ "conformsTo": [ { "@id": "https://w3id.org/ro/crate/1.1" - }, - { - "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" } ] }, diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_description/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_description/ro-crate-metadata.json index d795a744..7a974b9c 100644 --- a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_description/ro-crate-metadata.json +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_description/ro-crate-metadata.json @@ -59,9 +59,6 @@ "conformsTo": [ { "@id": "https://w3id.org/ro/crate/1.1" - }, - { - "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" } ] }, diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_entity/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_entity/ro-crate-metadata.json index ac9b307c..92325fbd 100644 --- a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_entity/ro-crate-metadata.json +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_entity/ro-crate-metadata.json @@ -27,9 +27,6 @@ "conformsTo": [ { "@id": "https://w3id.org/ro/crate/1.1" - }, - { - "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" } ] }, diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license/ro-crate-metadata.json index da9ef87e..77a5bf31 100644 --- a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license/ro-crate-metadata.json +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license/ro-crate-metadata.json @@ -57,9 +57,6 @@ "conformsTo": [ { "@id": "https://w3id.org/ro/crate/1.1" - }, - { - "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" } ] }, diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license_description/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license_description/ro-crate-metadata.json index ac285f0d..712c91f3 100644 --- a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license_description/ro-crate-metadata.json +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license_description/ro-crate-metadata.json @@ -60,9 +60,6 @@ "conformsTo": [ { "@id": "https://w3id.org/ro/crate/1.1" - }, - { - "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" } ] }, diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license_name/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license_name/ro-crate-metadata.json index 231af095..6487b7d3 100644 --- a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license_name/ro-crate-metadata.json +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_license_name/ro-crate-metadata.json @@ -60,9 +60,6 @@ "conformsTo": [ { "@id": "https://w3id.org/ro/crate/1.1" - }, - { - "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" } ] }, diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_name/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_name/ro-crate-metadata.json index 7d6612c8..32cf0d2b 100644 --- a/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_name/ro-crate-metadata.json +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/missing_root_name/ro-crate-metadata.json @@ -59,9 +59,6 @@ "conformsTo": [ { "@id": "https://w3id.org/ro/crate/1.1" - }, - { - "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" } ] }, From f97bdd510d4715b7a53b9bd459638818f45cc4f7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 27 Mar 2024 17:39:21 +0100 Subject: [PATCH 232/902] docs: :memo: initial README --- README.md | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/README.md b/README.md index e69de29b..3630469d 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,97 @@ +# `rocrate-validator` + +[![Build Status](https://travis-ci.com/crs4/rocrate-validator.svg?branch=main)](https://travis-ci.com/crs4/rocrate-validator) + + + +[![PyPI version](https://badge.fury.io/py/rocrate-validator.svg)](https://badge.fury.io/py/rocrate-validator) +[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) + +A Python package to validate [ROCrate](https://researchobject.github.io/ro-crate/) packages. + +## Setup + +Follow these steps to setup the project: + +1. **Clone the repository** + +```bash +git clone https://github.com/crs4/rocrate-validator.git +cd rocrate-validator +``` + +2. **Setup a Python virtual environment (optional)** + +Setup a Python virtual environment using `venv`: + +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +Or using `virtualenv`: + +```bash +virtualenv .venv +source .venv/bin/activate +``` + +This step, while optional, is recommended for isolating your project dependencies. If skipped, Poetry will automatically create a virtual environment for you. + +3. **Install the project using Poetry** + +Ensure you have Poetry installed. If not, follow the instructions [here](https://python-poetry.org/docs/#installation). Then, install the project: + +```bash +poetry install +``` + +## Usage + +After installation, you can use the main command `rocrate-validator` to validate ROCrates. + +### using Poetry + +Run the validator using the following command: + +```bash +poetry run rocrate-validator +``` + +Replace `` with the path to the ROCrate you want to validate. + +Type `poetry run rocrate-validator --help` for more information. + +### using the installed package on your virtual environment + +Activate the virtual environment: + +```bash +source .venv/bin/activate +``` + +Then, run the validator using the following command: + +```bash +rocrate-validator +``` + +Replace `` with the path to the ROCrate you want to validate. + +Type `rocrate-validator --help` for more information. + +## Running the tests + +To run the tests, use the following command: + +```bash +poetry run pytest +``` + + + +## License + +This project is licensed under the terms of the MIT license. See the [LICENSE](LICENSE) file for details. From 7fb2b5b345930af5c809641d63c6347f4a767284 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 27 Mar 2024 18:11:43 +0100 Subject: [PATCH 233/902] build(tests): :package: move test dependencies to the test group --- poetry.lock | 14 +++++++------- pyproject.toml | 5 +++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/poetry.lock b/poetry.lock index bbe7fbe6..f58b98c3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. [[package]] name = "appnope" @@ -968,13 +968,13 @@ testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygm [[package]] name = "pytest-cov" -version = "4.1.0" +version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, ] [package.dependencies] @@ -982,7 +982,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "python-dateutil" @@ -1334,4 +1334,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "d48bc23bf1b3a14257ca8af4fc126aee38b66531e492b5216d31f418485f4e19" +content-hash = "814f72a53f7fe0ca98bde407c1501fd12201b7c040b1c68fd6737a666988fefd" diff --git a/pyproject.toml b/pyproject.toml index b7003aeb..9cc4bf37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,11 +18,12 @@ colorlog = "^6.8" [tool.poetry.group.dev.dependencies] pyproject-flake8 = "^6.1.0" -pytest = "^8.1.0" -pytest-cov = "^4.1.0" pylint = "^3.1.0" ipykernel = "^6.29.3" +[tool.poetry.group.test.dependencies] +pytest-cov = "^5.0.0" + [tool.flake8] max-line-length = 120 From 8f6b40d016d8bbad11bae0c74c5fa8c560be0c3f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 29 Mar 2024 11:29:41 +0100 Subject: [PATCH 234/902] fix(core): :wrench: fix string to denote owl inference type --- rocrate_validator/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index 5d875932..0c49fb51 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -37,7 +37,7 @@ RDF_SERIALIZATION_FORMATS = typing.get_args(RDF_SERIALIZATION_FORMATS_TYPES) # Define allowed inference options -VALID_INFERENCE_OPTIONS_TYPES = typing.Literal["owl", "rdfs", "both", None] +VALID_INFERENCE_OPTIONS_TYPES = typing.Literal["owlrl", "rdfs", "both", None] VALID_INFERENCE_OPTIONS = typing.get_args(VALID_INFERENCE_OPTIONS_TYPES) # Define allowed requirement levels From c10fded172d43fd451106a4e9a9fa7d72e8b8600 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Tue, 26 Mar 2024 11:37:27 +0100 Subject: [PATCH 235/902] Remove extra escape from console message --- rocrate_validator/cli/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index 6dbea114..ca914bed 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -50,7 +50,8 @@ def cli(ctx, debug: bool = False, version: bool = False): try: cli() except Exception as e: - console.print(f"\n\n[bold]\[[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", style="white") + console.print( + f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", style="white") if logger.isEnabledFor(logging.DEBUG): console.print_exception() else: From 0ea2dbff2f43a35d3d236dc3f737a06fbd84225b Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Tue, 26 Mar 2024 11:40:36 +0100 Subject: [PATCH 236/902] Typing and formatting --- rocrate_validator/cli/main.py | 6 ++++-- rocrate_validator/errors.py | 32 +++++++++++++++++++++----------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index ca914bed..165d67eb 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -2,8 +2,9 @@ import rich_click as click from rich.console import Console -from rocrate_validator.utils import get_version + from rocrate_validator.config import configure_logging +from rocrate_validator.utils import get_version # set up logging logger = logging.getLogger(__name__) @@ -32,7 +33,8 @@ def cli(ctx, debug: bool = False, version: bool = False): # If the version flag is set, print the version and exit if version: - console.print(f"[bold]rocrate-validator [cyan]{get_version()}[/cyan][/bold]") + console.print( + f"[bold]rocrate-validator [cyan]{get_version()}[/cyan][/bold]") exit(0) # Set the log level if debug: diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 608674a8..3d0cfbef 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -1,16 +1,22 @@ + +from typing import Optional + +from .models import RequirementCheck + + class OutOfValidationContext(Exception): """Raised when a validation check is called outside of a validation context.""" - def __init__(self, message: str = None): + def __init__(self, message: Optional[str] = None): self._message = message @property - def message(self) -> str: + def message(self) -> Optional[str]: """The error message.""" return self._message - def __str__(self): - return self._message + def __str__(self) -> str: + return str(self._message) def __repr__(self): return f"OutOfValidationContext({self._message!r})" @@ -19,15 +25,15 @@ def __repr__(self): class InvalidSerializationFormat(Exception): """Raised when an invalid serialization format is provided.""" - def __init__(self, serialization_format: str = None): - self._format = serialization_format + def __init__(self, format: Optional[str] = None): + self._format = format @property - def serialization_format(self): + def serialization_format(self) -> Optional[str]: """The invalid serialization format.""" return self._format - def __str__(self): + def __str__(self) -> str: return f"Invalid serialization format: {self._format!r}" def __repr__(self): @@ -57,7 +63,7 @@ def code(self) -> int: """The error code.""" return self._code - def __str__(self): + def __str__(self) -> str: return self._message def __repr__(self): @@ -67,12 +73,16 @@ def __repr__(self): class CheckValidationError(ValidationError): """Raised when a validation check fails.""" - def __init__(self, check, message, path: str = ".", code: int = -1): + def __init__(self, + check: RequirementCheck, + message, + path: str = ".", + code: int = -1): super().__init__(message, path, code) self._check = check @property - def check(self): + def check(self) -> RequirementCheck: """The check that failed.""" return self._check From d1456542924ad2ac000ac0e9a6b9a85d011f2dd5 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 27 Mar 2024 09:15:10 +0100 Subject: [PATCH 237/902] Initial work redefining Requirement levels as Enums --- rocrate_validator/cli/commands/validate.py | 2 +- rocrate_validator/colors.py | 32 ++- rocrate_validator/errors.py | 16 +- rocrate_validator/models.py | 234 ++++++++++-------- .../requirements/python/__init__.py | 10 +- .../requirements/shacl/requirements.py | 12 +- rocrate_validator/services.py | 9 +- tests/test_models.py | 52 ++++ 8 files changed, 223 insertions(+), 144 deletions(-) create mode 100644 tests/test_models.py diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 16aa8c2d..869f813e 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -130,7 +130,7 @@ def validate(ctx, def __print_validation_result__( result: ValidationResult, - severity: Severity = Severity.WARNING): + severity: Severity = Severity.RECOMMENDED): """ Print the validation result """ diff --git a/rocrate_validator/colors.py b/rocrate_validator/colors.py index 43aae2ff..fde9efbe 100644 --- a/rocrate_validator/colors.py +++ b/rocrate_validator/colors.py @@ -1,6 +1,6 @@ from typing import Union -from .models import Severity +from .models import RequirementLevel, Severity def get_severity_color(severity: Union[str, Severity]) -> str: @@ -10,21 +10,31 @@ def get_severity_color(severity: Union[str, Severity]) -> str: :param severity: The severity :return: The color """ - if severity == Severity.ERROR or severity == "ERROR": + if severity == Severity.REQUIRED or severity == "REQUIRED": return "red" - elif severity == Severity.MUST or severity == "MUST": + elif severity == Severity.RECOMMENDED or severity == "RECOMMENDED": + return "orange" + elif severity == Severity.OPTIONAL or severity == "OPTIONAL": + return "yellow" + else: + return "white" + + +def get_req_level_color(level: RequirementLevel) -> str: + """ + Get the color for a RequirementLevel + + :return: The color + """ + if level in (RequirementLevel.MUST, RequirementLevel.SHALL, RequirementLevel.REQUIRED): return "red" - elif severity == Severity.MUST_NOT or severity == "MUST_NOT": + elif level in (RequirementLevel.MUST_NOT, RequirementLevel.SHALL_NOT): return "purple" - elif severity == Severity.SHOULD or severity == "SHOULD": + elif level in (RequirementLevel.SHOULD, RequirementLevel.RECOMMENDED): return "yellow" - elif severity == Severity.SHOULD_NOT or severity == "SHOULD_NOT": + elif level == RequirementLevel.SHOULD_NOT: return "lightyellow" - elif severity == Severity.MAY or severity == "MAY": + elif level in (RequirementLevel.MAY, RequirementLevel.OPTIONAL): return "orange" - elif severity == Severity.INFO or severity == "INFO": - return "lightblue" - elif severity == Severity.WARNING or severity == "WARNING": - return "yellow green" else: return "white" diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 3d0cfbef..568dc3d1 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -1,10 +1,14 @@ from typing import Optional -from .models import RequirementCheck +# from .models import RequirementCheck -class OutOfValidationContext(Exception): +class ROCValidatorError(Exception): + pass + + +class OutOfValidationContext(ROCValidatorError): """Raised when a validation check is called outside of a validation context.""" def __init__(self, message: Optional[str] = None): @@ -22,7 +26,7 @@ def __repr__(self): return f"OutOfValidationContext({self._message!r})" -class InvalidSerializationFormat(Exception): +class InvalidSerializationFormat(ROCValidatorError): """Raised when an invalid serialization format is provided.""" def __init__(self, format: Optional[str] = None): @@ -40,7 +44,7 @@ def __repr__(self): return f"InvalidSerializationFormat({self._format!r})" -class ValidationError(Exception): +class ValidationError(ROCValidatorError): """Raised when a validation error occurs.""" def __init__(self, message, path: str = ".", code: int = -1): @@ -74,7 +78,7 @@ class CheckValidationError(ValidationError): """Raised when a validation check fails.""" def __init__(self, - check: RequirementCheck, + check, message, path: str = ".", code: int = -1): @@ -82,7 +86,7 @@ def __init__(self, self._check = check @property - def check(self) -> RequirementCheck: + def check(self): """The check that failed.""" return self._check diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 7d28501b..e709b436 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1,10 +1,11 @@ from __future__ import annotations -import inspect +import enum import logging import os from abc import ABC, abstractmethod from dataclasses import dataclass +from functools import total_ordering from pathlib import Path from typing import Callable, Dict, List, Optional, Set, Type, Union @@ -23,100 +24,98 @@ logger = logging.getLogger(__name__) +@enum.unique +@total_ordering +class Severity(enum.Enum): + """Enum ordering "strength" of conditions to be verified""" + OPTIONAL = 0 + RECOMMENDED = 2 + REQUIRED = 4 + + def __lt__(self, other) -> bool: + if isinstance(other, Severity): + return self.value < other.value + else: + raise TypeError(f"Comparison not supported between instances of {type(self)} and {type(other)}") + + +@total_ordering @dataclass class RequirementType: name: str - value: int + value: Severity - def __eq__(self, other): + def __eq__(self, other) -> bool: if not isinstance(other, RequirementType): return False return self.name == other.name and self.value == other.value - def __ne__(self, other): - if not isinstance(other, RequirementType): - return True - return self.name != other.name or self.value != other.value - - def __lt__(self, other): + def __lt__(self, other) -> bool: + # TODO: this ordering is not totally coherent, since for two objects a and b + # with equal Severity but different names you would have + # not a < b, which implies a >= b + # and also a != b and not a > b, which is incoherent with a >= b if not isinstance(other, RequirementType): - raise ValueError(f"Cannot compare RequirementType with {type(other)}") + raise TypeError(f"Cannot compare RequirementType with {type(other)}") return self.value < other.value - def __le__(self, other): - if not isinstance(other, RequirementType): - raise ValueError(f"Cannot compare RequirementType with {type(other)}") - return self.value <= other.value - - def __gt__(self, other): - if not isinstance(other, RequirementType): - raise ValueError(f"Cannot compare RequirementType with {type(other)}") - return self.value > other.value - - def __ge__(self, other): - if not isinstance(other, RequirementType): - raise ValueError(f"Cannot compare RequirementType with {type(other)}") - return self.value >= other.value - - def __hash__(self): + def __hash__(self) -> int: return hash((self.name, self.value)) - def __repr__(self): + def __repr__(self) -> str: return f'RequirementType(name={self.name}, severity={self.value})' - def __str__(self): + def __str__(self) -> str: return self.name - def __int__(self): - return self.value + def __int__(self) -> int: + return self.value.value - def __index__(self): - return self.value + def __index__(self) -> int: + return self.value.value -class RequirementLevels: +@total_ordering +class RequirementLevel(enum.Enum): """ * The keywords MUST, MUST NOT, REQUIRED, * SHALL, SHALL NOT, SHOULD, SHOULD NOT, * RECOMMENDED, MAY, and OPTIONAL in this document * are to be interpreted as described in RFC 2119. """ - MAY = RequirementType('MAY', 1) - OPTIONAL = RequirementType('OPTIONAL', 1) - SHOULD = RequirementType('SHOULD', 2) - SHOULD_NOT = RequirementType('SHOULD_NOT', 2) - REQUIRED = RequirementType('REQUIRED', 3) - MUST = RequirementType('MUST', 3) - MUST_NOT = RequirementType('MUST_NOT', 3) - SHALL = RequirementType('SHALL', 3) - SHALL_NOT = RequirementType('SHALL_NOT', 3) - RECOMMENDED = RequirementType('RECOMMENDED', 3) + MAY = RequirementType('MAY', Severity.OPTIONAL) + OPTIONAL = RequirementType('OPTIONAL', Severity.OPTIONAL) + SHOULD = RequirementType('SHOULD', Severity.RECOMMENDED) + SHOULD_NOT = RequirementType('SHOULD_NOT', Severity.RECOMMENDED) + RECOMMENDED = RequirementType('RECOMMENDED', Severity.RECOMMENDED) + REQUIRED = RequirementType('REQUIRED', Severity.REQUIRED) + MUST = RequirementType('MUST', Severity.REQUIRED) + MUST_NOT = RequirementType('MUST_NOT', Severity.REQUIRED) + SHALL = RequirementType('SHALL', Severity.REQUIRED) + SHALL_NOT = RequirementType('SHALL_NOT', Severity.REQUIRED) - def all() -> Dict[str, RequirementType]: - return {name: member for name, member in inspect.getmembers(RequirementLevels) - if not inspect.isroutine(member) - and not inspect.isdatadescriptor(member) and not name.startswith('__')} + def __lt__(self, other) -> bool: + if not isinstance(other, RequirementLevel): + raise TypeError(f"Cannot compare {type(self)} with {type(other)}") + return self.value < other.value @staticmethod - def get(name: str) -> RequirementType: - return RequirementLevels.all()[name.upper()] - + def all() -> Dict[str, RequirementType]: + return {level.name: level.value for level in RequirementLevel} -class Severity(RequirementLevels): - """Extends the RequirementLevels enum with additional values""" - INFO = RequirementType('INFO', 0) - WARNING = RequirementType('WARNING', 2) - ERROR = RequirementType('ERROR', 4) + @classmethod + def get(cls, name: str) -> RequirementType: + return cls[name.upper()].value class Profile: def __init__(self, name: str, path: Path = None, - requirements: Set[Requirement] = None, + requirements: Optional[List[Requirement]] = None, publicID: str = None): self._path = path self._name = name self._description = None - self._requirements = requirements if requirements else [] + self._requirements = requirements if requirements is not None else [] self._publicID = publicID @property @@ -169,7 +168,7 @@ def load_requirements(self) -> List[Requirement]: for file in sorted(files, key=lambda x: (not x.endswith('.py'), x)): requirement_path = requirement_root / file for requirement in Requirement.load( - self, RequirementLevels.get(requirement_level), + self, RequirementLevel.get(requirement_level), requirement_path, publicID=self.publicID): req_id += 1 requirement._order_number = req_id @@ -183,7 +182,7 @@ def requirements(self) -> List[Requirement]: return self._requirements def get_requirements( - self, severity: RequirementType = RequirementLevels.MUST, + self, severity: RequirementType = RequirementLevel.MUST, exact_match: bool = False) -> List[Requirement]: return [requirement for requirement in self.requirements if not exact_match and requirement.severity >= severity or @@ -192,7 +191,7 @@ def get_requirements( @property def requirements_by_severity_map(self) -> Dict[RequirementType, List[Requirement]]: return {severity: self.get_requirements_by_type(severity) - for severity in RequirementLevels.all().values()} + for severity in RequirementLevel.all().values()} @property def inherited_profiles(self) -> List[Profile]: @@ -216,7 +215,7 @@ def remove_requirement(self, requirement: Requirement): self._requirements.remove(requirement) def validate(self, rocrate_path: Path) -> ValidationResult: - pass + raise NotImplementedError() def __eq__(self, other) -> bool: return self.name == other.name and self.path == other.path and self.requirements == other.requirements @@ -369,12 +368,12 @@ def ro_crate_path(self) -> Path: def issues(self) -> List[CheckIssue]: """Return the issues found during the check""" assert self._result, "Issues not set before the check" - return self._result.get_issues_by_check(self, Severity.INFO) + return self._result.get_issues_by_check(self, Severity.OPTIONAL) - def get_issues(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: + def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> List[CheckIssue]: return self._result.get_issues_by_check(self, severity) - def get_issues_by_severity(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: + def get_issues_by_severity(self, severity: Severity = Severity.RECOMMENDED) -> List[CheckIssue]: return self._result.get_issues_by_check_and_severity(self, severity) def check(self) -> bool: @@ -410,7 +409,7 @@ def __hash__(self): class Requirement(ABC): def __init__(self, - severity: RequirementType, + severity: RequirementLevel, profile: Profile, name: str = None, description: str = None, @@ -421,7 +420,7 @@ def __init__(self, self._severity = severity self._profile = profile self._description = description - self._path = path + self._path = path # path of code implementing the requirement self._checks: List[RequirementCheck] = [] # reference to the current validation context @@ -479,7 +478,7 @@ def path(self) -> Path: return self._path @abstractmethod - def __init_checks__(self): + def __init_checks__(self) -> List[RequirementCheck]: pass def get_checks(self) -> List[RequirementCheck]: @@ -595,7 +594,7 @@ def __str__(self): @staticmethod def load(profile: Profile, - requirement_type: RequirementType, + requirement_level: RequirementLevel, file_path: Path, publicID: str = None) -> List[Requirement]: # initialize the set of requirements @@ -605,23 +604,23 @@ def load(profile: Profile, if isinstance(file_path, str): file_path = Path(file_path) - # TODO: implement a better way to identify the requirement type and class + # TODO: implement a better way to identify the requirement level and class # check if the file is a python file if file_path.suffix == ".py": from rocrate_validator.requirements.python import PyRequirement - py_requirements = PyRequirement.load(profile, requirement_type, file_path) - logger.debug("Loaded Python requirements: %r" % py_requirements) + py_requirements = PyRequirement.load(profile, requirement_level, file_path) + logger.debug("Loaded Python requirements: %r", py_requirements) requirements.extend(py_requirements) - logger.debug("Added Requirement: %r" % py_requirements) + logger.debug("Added Requirement: %r", py_requirements) elif file_path.suffix == ".ttl": # from rocrate_validator.requirements.shacl.checks import SHACLCheck from rocrate_validator.requirements.shacl.requirements import \ SHACLRequirement - shapes_requirements = SHACLRequirement.load(profile, requirement_type, + shapes_requirements = SHACLRequirement.load(profile, requirement_level, file_path, publicID=publicID) - logger.debug("Loaded SHACL requirements: %r" % shapes_requirements) + logger.debug("Loaded SHACL requirements: %r", shapes_requirements) requirements.extend(shapes_requirements) - logger.debug("Added Requirement: %r" % shapes_requirements) + logger.debug("Added Requirement: %r", shapes_requirements) else: logger.warning("Requirement type not supported: %s", file_path.suffix) @@ -635,13 +634,6 @@ def class_decorator(cls): return class_decorator -class Severity(RequirementLevels): - """Extends the RequirementLevels enum with additional values""" - INFO = RequirementType('INFO', 0) - WARNING = RequirementType('WARNING', 2) - ERROR = RequirementType('ERROR', 4) - - class CheckIssue: """ Class to store an issue found during a check @@ -653,11 +645,18 @@ class CheckIssue: check (RequirementCheck): The check that generated the issue """ - def __init__(self, severity: Severity, + # TODO: + # 1. CheckIssue should keep track of the RequirementLevel that was broken, + # instead of the Severity (the Severity can later be retrieved from the level; + # 2. CheckIssue has the check, to it is able to determine the level and the Severity + # without having it provided through an argument. + def __init__(self, requirement_type: RequirementType, message: Optional[str] = None, code: int = None, check: RequirementCheck = None): - self._severity = severity + if not isinstance(requirement_type, RequirementType): + raise TypeError(f"CheckIssue constructed with a requirement_type of type {type(requirement_type)}") + self._level = RequirementLevel(requirement_type) self._message = message self._code = code self._check = check @@ -668,9 +667,18 @@ def message(self) -> str: return self._message @property - def severity(self) -> str: - """The severity of the issue""" - return self._severity + def level(self) -> RequirementLevel: + """The level of the issue""" + return self._level + + @property + def severity(self) -> Severity: + """Severity of the RequirementLevel associated with this check.""" + return self._level.value.value + + @property + def level_name(self) -> str: + return self._level.name @property def check(self) -> RequirementCheck: @@ -689,8 +697,8 @@ def code(self) -> int: - All issues with the same severity should start with the same number. - All codes should be positive numbers. """ - # Concatenate the severity, class name and message into a single string - issue_string = str(self.severity.value) + self.__class__.__name__ + str(self.message) + # Concatenate the level, class name and message into a single string + issue_string = str(self.level.value) + self.__class__.__name__ + str(self.message) # Use the built-in hash function to generate a unique code for this string # The modulo operation ensures that the code is a positive number @@ -772,54 +780,60 @@ def add_issues(self, issues: List[CheckIssue]): # TODO: check if the issues belong to the current validation context self._issues.extend(issues) + def add_check_issue(self, message: str, check: RequirementCheck, code: int = None): + self._issues.append(CheckIssue(check.requirement.severity, message, code, check=check)) + def add_error(self, message: str, check: RequirementCheck, code: int = None): - self._issues.append(CheckIssue(Severity.ERROR, message, code, check=check)) + self.add_check_issue(message, check, code) def add_warning(self, message: str, check: RequirementCheck, code: int = None): - self._issues.append(CheckIssue(Severity.WARNING, message, code, check=check)) - - def add_info(self, message: str, check: RequirementCheck, code: int = None): - self._issues.append(CheckIssue(Severity.INFO, message, code, check=check)) + self.add_check_issue(message, check, code) def add_optional(self, message: str, check: RequirementCheck, code: int = None): - self._issues.append(CheckIssue(Severity.OPTIONAL, message, code, check=check)) + self.add_check_issue(message, check, code) + + def add_info(self, message: str, check: RequirementCheck, code: int = None): + self.add_check_issue(message, check, code) def add_may(self, message: str, check: RequirementCheck, code: int = None): - self._issues.append(CheckIssue(Severity.MAY, message, code, check=check)) + self.add_check_issue(message, check, code) + + def add_recommended(self, message: str, check: RequirementCheck, code: int = None): + self.add_check_issue(message, check, code) def add_should(self, message: str, check: RequirementCheck, code: int = None): - self._issues.append(CheckIssue(Severity.SHOULD, message, code, check=check)) + self.add_check_issue(message, check, code) def add_should_not(self, message: str, check: RequirementCheck, code: int = None): - self._issues.append(CheckIssue(Severity.SHOULD_NOT, message, code, check=check)) + self.add_check_issue(message, check, code) def add_must(self, message: str, check: RequirementCheck, code: int = None): - self._issues.append(CheckIssue(Severity.MUST, message, code, check=check)) + self.add_check_issue(message, check, code) def add_must_not(self, message: str, check: RequirementCheck, code: int = None): - self._issues.append(CheckIssue(Severity.MUST_NOT, message, code, check=check)) + self.add_check_issue(message, check, code) @property def issues(self) -> List[CheckIssue]: return self._issues - def get_issues(self, severity: Severity = Severity.WARNING) -> List[CheckIssue]: - return [issue for issue in self._issues if issue.severity.value >= severity.value] + def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> List[CheckIssue]: + return [issue for issue in self._issues if issue.severity >= severity] def get_issues_by_severity(self, severity: Severity) -> List[CheckIssue]: return [issue for issue in self._issues if issue.severity == severity] - def get_issues_by_check(self, check: RequirementCheck, severity: Severity.WARNING) -> List[CheckIssue]: + def get_issues_by_check(self, check: RequirementCheck, severity: Severity.RECOMMENDED) -> List[CheckIssue]: return [issue for issue in self.issues if issue.check == check and issue.severity.value >= severity.value] def get_issues_by_check_and_severity(self, check: RequirementCheck, severity: Severity) -> List[CheckIssue]: - return [issue for issue in self.issues if issue.check == check and issue.severity.value == severity.value] + return [issue for issue in self.issues if issue.check == check and issue.severity == severity] - def has_issues(self, severity: Severity = Severity.WARNING) -> bool: - return any(issue.severity.value >= severity.value for issue in self._issues) + def has_issues(self, severity: Severity = Severity.RECOMMENDED) -> bool: + return any(issue.severity >= severity for issue in self._issues) - def passed(self, severity: Severity = Severity.WARNING) -> bool: - return not any(issue.severity.value >= severity.value for issue in self._issues) + def passed(self, severity: Severity = Severity.RECOMMENDED) -> bool: + return not any(issue.severity >= severity for issue in self._issues) def __str__(self): return f"Validation result: {len(self._issues)} issues" @@ -835,7 +849,7 @@ def __init__(self, profiles_path: str = "./profiles", profile_name: str = "ro-crate", disable_profile_inheritance: bool = False, - requirement_level: Union[str, RequirementType] = RequirementLevels.MUST, + requirement_level: Union[str, RequirementType] = RequirementLevel.MUST, requirement_level_only: bool = False, ontologies_path: Optional[Path] = None, advanced: Optional[bool] = False, @@ -852,7 +866,7 @@ def __init__(self, self.profile_name = profile_name self.disable_profile_inheritance = disable_profile_inheritance self.requirement_level = \ - RequirementLevels.get(requirement_level) if isinstance(requirement_level, str) else \ + RequirementLevel.get(requirement_level) if isinstance(requirement_level, str) else \ requirement_level self.requirement_level_only = requirement_level_only self.ontologies_path = ontologies_path diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index dbadb0ad..f7316574 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import List -from ...models import Profile, Requirement, RequirementCheck, RequirementType +from ...models import Profile, Requirement, RequirementCheck, RequirementLevel from ...utils import get_classes_from_file # set up logging @@ -13,14 +13,14 @@ class PyRequirement(Requirement): def __init__(self, - type: RequirementType, + severity: RequirementLevel, profile: Profile, name: str = None, description: str = None, path: Path = None, requirement_check_class=None): self.requirement_check_class = requirement_check_class - super().__init__(type, profile, name, description, path, initialize_checks=True) + super().__init__(severity, profile, name, description, path, initialize_checks=True) def __init_checks__(self): # initialize the list of checks @@ -40,7 +40,7 @@ def __init_checks__(self): return checks @classmethod - def load(cls, profile: Profile, requirement_type: RequirementType, file_path: Path): + def load(cls, profile: Profile, requirement_level: RequirementLevel, file_path: Path): # instantiate a list to store the requirements requirements: List[Requirement] = [] @@ -52,7 +52,7 @@ def load(cls, profile: Profile, requirement_type: RequirementType, file_path: Pa for requirement_name, requirement_class in classes.items(): logger.debug("Processing requirement: %r" % requirement_name) r = PyRequirement( - requirement_type, profile, + requirement_level, profile, name=requirement_name.strip() if requirement_name else "", description=requirement_class.__doc__.strip() if requirement_class.__doc__ else "", path=file_path, diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index e5cd3cb7..a21391a9 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Dict, List -from ...models import Profile, Requirement, RequirementType +from ...models import Profile, Requirement, RequirementCheck, RequirementLevel from .checks import SHACLCheck from .models import Shape @@ -13,12 +13,12 @@ class SHACLRequirement(Requirement): def __init__(self, - type: RequirementType, + level: RequirementLevel, shape: Shape, profile: Profile, path: Path): self._shape = shape - super().__init__(type, profile, + super().__init__(level, profile, shape.name if shape.name else "", shape.description if shape.description else "", path) @@ -27,7 +27,7 @@ def __init__(self, # assign check IDs self.__reorder_checks__() - def __init_checks__(self): + def __init_checks__(self) -> List[RequirementCheck]: # assign a check to each property of the shape checks = [] for prop in self._shape.get_properties(): @@ -47,11 +47,11 @@ def shape(self) -> Shape: return self._shape @staticmethod - def load(profile: Profile, requirement_type: RequirementType, + def load(profile: Profile, requirement_level: RequirementLevel, file_path: Path, publicID: str = None) -> List[Requirement]: shapes: Dict[str, Shape] = Shape.load(file_path, publicID=publicID) logger.debug("Loaded shapes: %s" % shapes) requirements = [] for shape in shapes.values(): - requirements.append(SHACLRequirement(requirement_type, shape, profile, file_path)) + requirements.append(SHACLRequirement(requirement_level, shape, profile, file_path)) return requirements diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 87052a22..b3ad16f4 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -4,9 +4,8 @@ from pyshacl.pytypes import GraphLike -from rocrate_validator.models import (Profile, RequirementLevels, - RequirementType, ValidationResult, - Validator) +from .models import (Profile, RequirementLevel, RequirementType, + ValidationResult, Validator) # set up logging logger = logging.getLogger(__name__) @@ -24,7 +23,7 @@ def validate( abort_on_first: Optional[bool] = True, allow_infos: Optional[bool] = False, allow_warnings: Optional[bool] = False, - requirement_level: Union[str, RequirementType] = RequirementLevels.MUST, + requirement_level: Union[str, RequirementType] = RequirementLevel.MUST, requirement_level_only: bool = False, serialization_output_path: str = None, serialization_output_format: str = "turtle", @@ -35,7 +34,7 @@ def validate( """ # parse requirement level requirement_level = \ - RequirementLevels.get(requirement_level) \ + RequirementLevel.get(requirement_level) \ if isinstance(requirement_level, str) \ else requirement_level diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 00000000..691ac0b6 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,52 @@ + +import pytest + +from rocrate_validator.models import (RequirementLevel, RequirementType, + Severity) + + +def test_severity_ordering(): + assert Severity.OPTIONAL < Severity.RECOMMENDED + assert Severity.RECOMMENDED > Severity.OPTIONAL + assert Severity.RECOMMENDED < Severity.REQUIRED + assert Severity.OPTIONAL < Severity.REQUIRED + assert Severity.OPTIONAL == Severity.OPTIONAL + assert Severity.RECOMMENDED <= Severity.REQUIRED + assert Severity.RECOMMENDED >= Severity.OPTIONAL + + +def test_requirement_type_ordering(): + may = RequirementType('MAY', Severity.OPTIONAL) + should = RequirementType('SHOULD', Severity.RECOMMENDED) + assert may < should + assert should > may + assert should != may + assert may == may + assert may != 1 + with pytest.raises(TypeError): + _ = may > 1 + + +def test_requirement_type_basics(): + may = RequirementType('MAY', Severity.OPTIONAL) + assert str(may) == "MAY" + assert int(may) == Severity.OPTIONAL.value + + +def test_requirement_levels(): + assert RequirementLevel.get('may') == RequirementLevel.MAY.value + + # Test ordering + assert RequirementLevel.MAY < RequirementLevel.SHOULD + assert RequirementLevel.SHOULD > RequirementLevel.MAY + assert RequirementLevel.SHOULD != RequirementLevel.MAY + assert RequirementLevel.MAY == RequirementLevel.MAY + + all_levels = RequirementLevel.all() + assert isinstance(all_levels, dict) + assert 10 == len(all_levels) + # Test a few of the keys + assert 'MAY' in all_levels + assert 'SHOULD_NOT' in all_levels + assert 'RECOMMENDED' in all_levels + assert 'REQUIRED' in all_levels From 8bd13b5f3552655846b950758be5940d17558e89 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 27 Mar 2024 11:10:03 +0100 Subject: [PATCH 238/902] Simplify Requirement comparison --- rocrate_validator/models.py | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index e709b436..749739d1 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -541,40 +541,20 @@ def validation_settings(self): raise OutOfValidationContext("Validation context has not been initialized") return self._validation_context.settings - def __eq__(self, other: Requirement): + def __eq__(self, other) -> bool: if not isinstance(other, Requirement): - raise ValueError(f"Cannot compare Requirement with {type(other)}") + raise TypeError(f"Cannot compare {type(self)} with {type(other)}") return self.name == other.name \ and self.severity == other.severity and self.description == other.description \ and self.path == other.path - def __ne__(self, other: Requirement): - if not isinstance(other, Requirement): - raise ValueError(f"Cannot compare Requirement with {type(other)}") - return self.name != other.name \ - or self.severity != other.type \ - or self.description != other.description \ - or self.path != other.path + def __ne__(self, other) -> bool: + return not self.__eq__(other) def __hash__(self): return hash((self.name, self.severity, self.description, self.path)) - def __lt__(self, other: Requirement) -> bool: - if not isinstance(other, Requirement): - raise ValueError(f"Cannot compare Requirement with {type(other)}") - return self.severity < other.severity or self.name < other.name - - def __le__(self, other: Requirement) -> bool: - if not isinstance(other, Requirement): - raise ValueError(f"Cannot compare Requirement with {type(other)}") - return self.severity <= other.severity or self.name <= other.name - - def __gt__(self, other: Requirement) -> bool: - if not isinstance(other, Requirement): - raise ValueError(f"Cannot compare Requirement with {type(other)}") - return self.severity > other.severity or self.name > other.name - - def __ge__(self, other: Requirement) -> bool: + def __lt__(self, other) -> bool: if not isinstance(other, Requirement): raise ValueError(f"Cannot compare Requirement with {type(other)}") return self.severity >= other.severity or self.name >= other.name From 992d3953db21d6e8c29bbd29f74f5e38b27f4c4e Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 27 Mar 2024 17:24:04 +0100 Subject: [PATCH 239/902] Simplify (maybe) representation of requirement levels --- rocrate_validator/cli/commands/validate.py | 41 ++- rocrate_validator/colors.py | 16 +- rocrate_validator/models.py | 398 +++++++++++---------- rocrate_validator/services.py | 28 +- tests/ro-crate-metadata.json | 312 ++++++++++++++++ tests/test_models.py | 37 +- 6 files changed, 574 insertions(+), 258 deletions(-) create mode 100644 tests/ro-crate-metadata.json diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 869f813e..32eea55c 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -1,5 +1,7 @@ import logging import os +from pathlib import Path +from typing import Optional from rich.align import Align @@ -11,7 +13,6 @@ # from rich.markdown import Markdown # from rich.table import Table - # set up logging logger = logging.getLogger(__name__) @@ -51,17 +52,17 @@ ) @click.option( "-l", - "--requirement-level", - type=click.Choice(["MUST", "SHOULD", "MAY"], case_sensitive=False), - default="MUST", + "--requirement-severity", + type=click.Choice([s.name for s in Severity], case_sensitive=False), + default=Severity.REQUIRED.name, show_default=True, - help="Level of the requirements to validate", + help="Severity of the requirements to validate", ) @click.option( '-lo', - '--requirement-level-only', + '--requirement-severity-only', is_flag=True, - help="Validate only the requirements of the specified level (no levels with lower severity)", + help="Validate only the requirements of the specified severity (no requirements with lower severity)", default=False, show_default=True ) @@ -74,22 +75,22 @@ # ) @click.pass_context def validate(ctx, - profiles_path: str = "./profiles", + profiles_path: Path = Path("./profiles"), profile_name: str = "ro-crate", disable_profile_inheritance: bool = False, - requirement_level: str = "MUST", - requirement_level_only: bool = False, - rocrate_path: str = ".", + requirement_severity: str = Severity.REQUIRED.name, + requirement_severity_only: bool = False, + rocrate_path: Path = Path("."), no_fail_fast: bool = False, - ontologies_path: str = None): + ontologies_path: Optional[Path] = None): """ [magenta]rocrate-validator:[/magenta] Validate a RO-Crate against a profile """ # Log the input parameters for debugging logger.debug("profiles_path: %s", os.path.abspath(profiles_path)) logger.debug("profile_name: %s", profile_name) - logger.debug("requirement_level: %s", requirement_level) - logger.debug("requirement_level_only: %s", requirement_level_only) + logger.debug("requirement_severity: %s", requirement_severity) + logger.debug("requirement_severity_only: %s", requirement_severity_only) logger.debug("disable_inheritance: %s", disable_profile_inheritance) logger.debug("rocrate_path: %s", os.path.abspath(rocrate_path)) @@ -102,17 +103,15 @@ def validate(ctx, logger.debug("rocrate_path: %s", os.path.abspath(rocrate_path)) try: - # Validate the RO-Crate result: ValidationResult = services.validate( profiles_path=profiles_path, profile_name=profile_name, - requirement_level=requirement_level, - requirement_level_only=requirement_level_only, + requirement_severity=requirement_severity, + requirement_severity_only=requirement_severity_only, disable_profile_inheritance=disable_profile_inheritance, - rocrate_path=os.path.abspath(rocrate_path), - ontologies_path=os.path.abspath( - ontologies_path) if ontologies_path else None, + rocrate_path=Path(rocrate_path).absolute(), + ontologies_path=Path(ontologies_path).absolute() if ontologies_path else None, abort_on_first=not no_fail_fast ) @@ -162,7 +161,7 @@ def __print_validation_result__( console.print(f"{' '*4}Failed checks:\n", style="white bold") for check in result.get_failed_checks_by_requirement(requirement): - issue_color = get_severity_color(check.severity) + issue_color = get_severity_color(check.level.severity) console.print( f"{' '*4}- " f"[magenta]{check.name}[/magenta]: {check.description}") diff --git a/rocrate_validator/colors.py b/rocrate_validator/colors.py index fde9efbe..aeab707a 100644 --- a/rocrate_validator/colors.py +++ b/rocrate_validator/colors.py @@ -1,6 +1,6 @@ from typing import Union -from .models import RequirementLevel, Severity +from .models import LevelCollection, Severity def get_severity_color(severity: Union[str, Severity]) -> str: @@ -20,21 +20,21 @@ def get_severity_color(severity: Union[str, Severity]) -> str: return "white" -def get_req_level_color(level: RequirementLevel) -> str: +def get_req_level_color(level: LevelCollection) -> str: """ - Get the color for a RequirementLevel + Get the color for a LevelCollection :return: The color """ - if level in (RequirementLevel.MUST, RequirementLevel.SHALL, RequirementLevel.REQUIRED): + if level in (LevelCollection.MUST, LevelCollection.SHALL, LevelCollection.REQUIRED): return "red" - elif level in (RequirementLevel.MUST_NOT, RequirementLevel.SHALL_NOT): + elif level in (LevelCollection.MUST_NOT, LevelCollection.SHALL_NOT): return "purple" - elif level in (RequirementLevel.SHOULD, RequirementLevel.RECOMMENDED): + elif level in (LevelCollection.SHOULD, LevelCollection.RECOMMENDED): return "yellow" - elif level == RequirementLevel.SHOULD_NOT: + elif level == LevelCollection.SHOULD_NOT: return "lightyellow" - elif level in (RequirementLevel.MAY, RequirementLevel.OPTIONAL): + elif level in (LevelCollection.MAY, LevelCollection.OPTIONAL): return "orange" else: return "white" diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 749739d1..686adaf9 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1,6 +1,7 @@ from __future__ import annotations import enum +import inspect import logging import os from abc import ABC, abstractmethod @@ -41,71 +42,72 @@ def __lt__(self, other) -> bool: @total_ordering @dataclass -class RequirementType: +class RequirementLevel: name: str - value: Severity + severity: Severity def __eq__(self, other) -> bool: - if not isinstance(other, RequirementType): + if not isinstance(other, RequirementLevel): return False - return self.name == other.name and self.value == other.value + return self.name == other.name and self.severity == other.severity def __lt__(self, other) -> bool: # TODO: this ordering is not totally coherent, since for two objects a and b # with equal Severity but different names you would have # not a < b, which implies a >= b # and also a != b and not a > b, which is incoherent with a >= b - if not isinstance(other, RequirementType): - raise TypeError(f"Cannot compare RequirementType with {type(other)}") - return self.value < other.value + if not isinstance(other, RequirementLevel): + raise TypeError(f"Cannot compare {type(self)} with {type(other)}") + return self.severity < other.severity def __hash__(self) -> int: - return hash((self.name, self.value)) + return hash((self.name, self.severity)) def __repr__(self) -> str: - return f'RequirementType(name={self.name}, severity={self.value})' + return f'RequirementLevel(name={self.name}, severity={self.severity})' def __str__(self) -> str: return self.name def __int__(self) -> int: - return self.value.value + return self.severity.value def __index__(self) -> int: - return self.value.value + return self.severity.value -@total_ordering -class RequirementLevel(enum.Enum): +class LevelCollection: """ * The keywords MUST, MUST NOT, REQUIRED, * SHALL, SHALL NOT, SHOULD, SHOULD NOT, * RECOMMENDED, MAY, and OPTIONAL in this document * are to be interpreted as described in RFC 2119. """ - MAY = RequirementType('MAY', Severity.OPTIONAL) - OPTIONAL = RequirementType('OPTIONAL', Severity.OPTIONAL) - SHOULD = RequirementType('SHOULD', Severity.RECOMMENDED) - SHOULD_NOT = RequirementType('SHOULD_NOT', Severity.RECOMMENDED) - RECOMMENDED = RequirementType('RECOMMENDED', Severity.RECOMMENDED) - REQUIRED = RequirementType('REQUIRED', Severity.REQUIRED) - MUST = RequirementType('MUST', Severity.REQUIRED) - MUST_NOT = RequirementType('MUST_NOT', Severity.REQUIRED) - SHALL = RequirementType('SHALL', Severity.REQUIRED) - SHALL_NOT = RequirementType('SHALL_NOT', Severity.REQUIRED) + OPTIONAL = RequirementLevel('OPTIONAL', Severity.OPTIONAL) + MAY = RequirementLevel('MAY', Severity.OPTIONAL) - def __lt__(self, other) -> bool: - if not isinstance(other, RequirementLevel): - raise TypeError(f"Cannot compare {type(self)} with {type(other)}") - return self.value < other.value + REQUIRED = RequirementLevel('REQUIRED', Severity.REQUIRED) + SHOULD = RequirementLevel('SHOULD', Severity.RECOMMENDED) + SHOULD_NOT = RequirementLevel('SHOULD_NOT', Severity.RECOMMENDED) + RECOMMENDED = RequirementLevel('RECOMMENDED', Severity.RECOMMENDED) + + MUST = RequirementLevel('MUST', Severity.REQUIRED) + MUST_NOT = RequirementLevel('MUST_NOT', Severity.REQUIRED) + SHALL = RequirementLevel('SHALL', Severity.REQUIRED) + SHALL_NOT = RequirementLevel('SHALL_NOT', Severity.REQUIRED) + + def __init__(self): + raise NotImplementedError(f"{type(self)} can't be instantianted") @staticmethod - def all() -> Dict[str, RequirementType]: - return {level.name: level.value for level in RequirementLevel} + def all() -> List[RequirementLevel]: + return [level for name, level in inspect.getmembers(LevelCollection) + if not inspect.isroutine(level) + and not inspect.isdatadescriptor(level) and not name.startswith('__')] - @classmethod - def get(cls, name: str) -> RequirementType: - return cls[name.upper()].value + @staticmethod + def get(name: str) -> RequirementLevel: + return getattr(LevelCollection, name.upper()) class Profile: @@ -168,7 +170,7 @@ def load_requirements(self) -> List[Requirement]: for file in sorted(files, key=lambda x: (not x.endswith('.py'), x)): requirement_path = requirement_root / file for requirement in Requirement.load( - self, RequirementLevel.get(requirement_level), + self, LevelCollection.get(requirement_level), requirement_path, publicID=self.publicID): req_id += 1 requirement._order_number = req_id @@ -182,16 +184,16 @@ def requirements(self) -> List[Requirement]: return self._requirements def get_requirements( - self, severity: RequirementType = RequirementLevel.MUST, + self, severity: Severity = Severity.REQUIRED, exact_match: bool = False) -> List[Requirement]: return [requirement for requirement in self.requirements if not exact_match and requirement.severity >= severity or exact_match and requirement.severity == severity] - @property - def requirements_by_severity_map(self) -> Dict[RequirementType, List[Requirement]]: - return {severity: self.get_requirements_by_type(severity) - for severity in RequirementLevel.all().values()} + # @property + # def requirements_by_severity_map(self) -> Dict[Severity, List[Requirement]]: + # return {level.severity: self.get_requirements_by_type(level.severity) + # for level in LevelCollection.all()} @property def inherited_profiles(self) -> List[Profile]: @@ -205,8 +207,8 @@ def inherited_profiles(self) -> List[Profile]: def has_requirement(self, name: str) -> bool: return self.get_requirement(name) is not None - def get_requirements_by_type(self, type: RequirementType) -> List[Requirement]: - return [requirement for requirement in self.requirements if requirement.severity == type] + # def get_requirements_by_type(self, type: RequirementLevel) -> List[Requirement]: + # return [requirement for requirement in self.requirements if requirement.severity == type] def add_requirement(self, requirement: Requirement): self._requirements.append(requirement) @@ -218,10 +220,13 @@ def validate(self, rocrate_path: Path) -> ValidationResult: raise NotImplementedError() def __eq__(self, other) -> bool: - return self.name == other.name and self.path == other.path and self.requirements == other.requirements + return isinstance(other, Profile) \ + and self.name == other.name \ + and self.path == other.path \ + and self.requirements == other.requirements def __ne__(self, other) -> bool: - return self.name != other.name or self.path != other.path or self.requirements != other.requirements + return not self.__eq__(other) def __lt__(self, other) -> bool: return self.name < other.name @@ -284,140 +289,18 @@ def decorator(func): return decorator -class RequirementCheck: - - def __init__(self, - requirement: Requirement, - name: str, - check_function: Callable, - description: str = None): - self._requirement: Requirement = requirement - self._order_number = None - self._name = name - self._description = description - self._check_function = check_function - # declare the reference to the validation context - self._validation_context: ValidationContext = None - # declare the reference to the validator - self._validator: Validator = None - # declare the result of the check - self._result: ValidationResult = None - - @property - def order_number(self) -> int: - return self._order_number - - @property - def identifier(self) -> str: - return f"{self.requirement.identifier}.{self.order_number}" - - @property - def name(self) -> str: - if not self._name: - return self.__class__.__name__.replace("Check", "") - return self._name - - @property - def description(self) -> str: - if not self._description: - return self.__class__.__doc__.strip() if self.__class__.__doc__ else f"Check {self.name}" - return self._description - - @property - def requirement(self) -> Requirement: - return self._requirement - - @property - def severity(self) -> RequirementType: - return self.requirement.severity - - @property - def check_function(self) -> Callable: - return self._check_function - - @property - def rocrate_path(self) -> Path: - assert self.validator, "ro-crate path not set before the check" - return self.validation_context.rocrate_path - - @property - def file_descriptor_path(self) -> Path: - return self.rocrate_path / ROCRATE_METADATA_FILE - - @property - def validation_context(self) -> ValidationContext: - assert self._validation_context, "Validation context not set before the check" - return self._validation_context - - @property - def validator(self) -> Validator: - assert self._validator, "Validator not set before the check" - return self._validator - - @property - def result(self) -> ValidationResult: - assert self._result, "Result not set before the check" - return self._result - - @property - def ro_crate_path(self) -> Path: - assert self.validator, "ro-crate path not set before the check" - return self.validator.rocrate_path - - @property - def issues(self) -> List[CheckIssue]: - """Return the issues found during the check""" - assert self._result, "Issues not set before the check" - return self._result.get_issues_by_check(self, Severity.OPTIONAL) - - def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> List[CheckIssue]: - return self._result.get_issues_by_check(self, severity) - - def get_issues_by_severity(self, severity: Severity = Severity.RECOMMENDED) -> List[CheckIssue]: - return self._result.get_issues_by_check_and_severity(self, severity) - - def check(self) -> bool: - return self.check_function(self) - - def __do_check__(self, context: ValidationContext) -> bool: - """ - Internal method to perform the check - """ - # Set the validation context - self._validation_context = context - # Set the validator - self._validator = context.validator - # Set the result - self._result = context.result - # Perform the check - return self.check() - - def __eq__(self, other: RequirementCheck): - if not isinstance(other, RequirementCheck): - raise ValueError(f"Cannot compare RequirementCheck with {type(other)}") - return self.requirement == other.requirement and self.name == other.name - - def __ne__(self, other: RequirementCheck): - if not isinstance(other, RequirementCheck): - raise ValueError(f"Cannot compare RequirementCheck with {type(other)}") - return self.requirement != other.requirement or self.name != other.name - - def __hash__(self): - return hash((self.requirement, self.name or "")) - - class Requirement(ABC): def __init__(self, - severity: RequirementLevel, + level: RequirementLevel, profile: Profile, name: str = None, description: str = None, path: Path = None, initialize_checks: bool = True): - self._order_number = None + self._order_number: int = None self._name = name - self._severity = severity + self._level = level self._profile = profile self._description = description self._path = path # path of code implementing the requirement @@ -445,7 +328,7 @@ def order_number(self) -> int: @property def identifier(self) -> str: - return f"{self.severity}.{self.order_number}" + return f"{self.level.name}.{self.order_number}" @property def name(self) -> str: @@ -454,8 +337,12 @@ def name(self) -> str: return self._name @property - def severity(self) -> RequirementType: - return self._severity + def level(self) -> RequirementLevel: + return self._level + + @property + def severity(self) -> Severity: + return self.level.severity @property def color(self) -> str: @@ -490,7 +377,7 @@ def get_check(self, name: str) -> RequirementCheck: return check return None - def __reorder_checks__(self): + def __reorder_checks__(self) -> None: for i, check in enumerate(self._checks): check._order_number = i + 1 @@ -562,8 +449,9 @@ def __lt__(self, other) -> bool: def __repr__(self): return ( f'ProfileRequirement(' + f'_order_number={self._order_number}, ' f'name={self.name}, ' - f'severity={self.severity}, ' + f'level={self.level}, ' f'description={self.description}' f', path={self.path}, ' if self.path else '' ')' @@ -607,6 +495,128 @@ def load(profile: Profile, return requirements +class RequirementCheck: + + def __init__(self, + requirement: Requirement, + name: str, + check_function: Callable, + description: str = None): + self._requirement: Requirement = requirement + self._order_number: int = None + self._name = name + self._description = description + self._check_function = check_function + # declare the reference to the validation context + self._validation_context: ValidationContext = None + # declare the reference to the validator + self._validator: Validator = None + # declare the result of the check + self._result: ValidationResult = None + + @property + def order_number(self) -> int: + return self._order_number + + @property + def identifier(self) -> str: + return f"{self.requirement.identifier}.{self.order_number}" + + @property + def name(self) -> str: + if not self._name: + return self.__class__.__name__.replace("Check", "") + return self._name + + @property + def description(self) -> str: + if not self._description: + return self.__class__.__doc__.strip() if self.__class__.__doc__ else f"Check {self.name}" + return self._description + + @property + def requirement(self) -> Requirement: + return self._requirement + + @property + def level(self) -> RequirementLevel: + return self.requirement.level + + @property + def check_function(self) -> Callable: + return self._check_function + + @property + def rocrate_path(self) -> Path: + assert self.validator, "ro-crate path not set before the check" + return self.validation_context.rocrate_path + + @property + def file_descriptor_path(self) -> Path: + return self.rocrate_path / ROCRATE_METADATA_FILE + + @property + def validation_context(self) -> ValidationContext: + assert self._validation_context, "Validation context not set before the check" + return self._validation_context + + @property + def validator(self) -> Validator: + assert self._validator, "Validator not set before the check" + return self._validator + + @property + def result(self) -> ValidationResult: + assert self._result, "Result not set before the check" + return self._result + + @property + def ro_crate_path(self) -> Path: + assert self.validator, "ro-crate path not set before the check" + return self.validator.rocrate_path + + @property + def issues(self) -> List[CheckIssue]: + """Return the issues found during the check""" + assert self._result, "Issues not set before the check" + return self._result.get_issues_by_check(self, Severity.OPTIONAL) + + def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> List[CheckIssue]: + return self._result.get_issues_by_check(self, severity) + + def get_issues_by_severity(self, severity: Severity = Severity.RECOMMENDED) -> List[CheckIssue]: + return self._result.get_issues_by_check_and_severity(self, severity) + + def check(self) -> bool: + return self.check_function(self) + + def __do_check__(self, context: ValidationContext) -> bool: + """ + Internal method to perform the check + """ + # Set the validation context + self._validation_context = context + # Set the validator + self._validator = context.validator + # Set the result + self._result = context.result + # Perform the check + return self.check() + + def __eq__(self, other: RequirementCheck): + if not isinstance(other, RequirementCheck): + raise ValueError(f"Cannot compare RequirementCheck with {type(other)}") + return self.requirement == other.requirement and self.name == other.name + + def __ne__(self, other: RequirementCheck): + if not isinstance(other, RequirementCheck): + raise ValueError(f"Cannot compare RequirementCheck with {type(other)}") + return self.requirement != other.requirement or self.name != other.name + + def __hash__(self): + return hash((self.requirement, self.name or "")) + + def issue_types(issues: List[Type[CheckIssue]]) -> Type[RequirementCheck]: def class_decorator(cls): cls.issue_types = issues @@ -626,20 +636,20 @@ class CheckIssue: """ # TODO: - # 1. CheckIssue should keep track of the RequirementLevel that was broken, - # instead of the Severity (the Severity can later be retrieved from the level; + # 1. CheckIssue should keep track of the RequirementCheck that was broken, + # instead of the RequirementLevel; # 2. CheckIssue has the check, to it is able to determine the level and the Severity - # without having it provided through an argument. - def __init__(self, requirement_type: RequirementType, + # without having it provided through an additional argument. + def __init__(self, severity: Severity, message: Optional[str] = None, code: int = None, check: RequirementCheck = None): - if not isinstance(requirement_type, RequirementType): - raise TypeError(f"CheckIssue constructed with a requirement_type of type {type(requirement_type)}") - self._level = RequirementLevel(requirement_type) + if not isinstance(severity, Severity): + raise TypeError(f"CheckIssue constructed with a severity of type {type(severity)}") + self._severity = severity self._message = message self._code = code - self._check = check + self._check: RequirementCheck = check @property def message(self) -> str: @@ -649,16 +659,16 @@ def message(self) -> str: @property def level(self) -> RequirementLevel: """The level of the issue""" - return self._level + return self._check.level @property def severity(self) -> Severity: """Severity of the RequirementLevel associated with this check.""" - return self._level.value.value + return self._severity @property def level_name(self) -> str: - return self._level.name + return self.level.name @property def check(self) -> RequirementCheck: @@ -678,7 +688,7 @@ def code(self) -> int: - All codes should be positive numbers. """ # Concatenate the level, class name and message into a single string - issue_string = str(self.level.value) + self.__class__.__name__ + str(self.message) + issue_string = self.level.name + self.__class__.__name__ + str(self.message) # Use the built-in hash function to generate a unique code for this string # The modulo operation ensures that the code is a positive number @@ -717,7 +727,7 @@ def failed_requirements(self) -> Set[Requirement]: @property def passed_requirements(self) -> Set[Requirement]: - return sorted(self._validated_requirements - self.failed_requirements, key=lambda req: req.number_order) + return sorted(self._validated_requirements - self.failed_requirements, key=lambda req: req.order_number) @property def checks(self) -> Set[RequirementCheck]: @@ -826,11 +836,11 @@ class Validator: def __init__(self, rocrate_path: Path, - profiles_path: str = "./profiles", + profiles_path: Path = Path("./profiles"), profile_name: str = "ro-crate", disable_profile_inheritance: bool = False, - requirement_level: Union[str, RequirementType] = RequirementLevel.MUST, - requirement_level_only: bool = False, + requirement_severity: Severity = Severity.REQUIRED, + requirement_severity_only: bool = False, ontologies_path: Optional[Path] = None, advanced: Optional[bool] = False, inference: Optional[VALID_INFERENCE_OPTIONS_TYPES] = None, @@ -838,20 +848,16 @@ def __init__(self, abort_on_first: Optional[bool] = True, allow_infos: Optional[bool] = False, allow_warnings: Optional[bool] = False, - serialization_output_path: str = None, + serialization_output_path: Optional[Path] = None, serialization_output_format: Optional[RDF_SERIALIZATION_FORMATS_TYPES] = "turtle", **kwargs): self.rocrate_path = rocrate_path self.profiles_path = profiles_path self.profile_name = profile_name + self.requirement_severity = requirement_severity + self.requirement_severity_only = requirement_severity_only self.disable_profile_inheritance = disable_profile_inheritance - self.requirement_level = \ - RequirementLevel.get(requirement_level) if isinstance(requirement_level, str) else \ - requirement_level - self.requirement_level_only = requirement_level_only self.ontologies_path = ontologies_path - self.requirement_level = requirement_level - self.requirement_level_only = requirement_level_only self._validation_settings = { 'advanced': advanced, @@ -1010,7 +1016,7 @@ def __do_validate__(self, requirements: List[Requirement] = None) -> ValidationR # perform the requirements validation if not requirements: requirements = profile.get_requirements( - self.requirement_level, exact_match=self.requirement_level_only) + self.requirement_severity, exact_match=self.requirement_severity_only) for requirement in requirements: logger.debug("Validating Requirement: %s", requirement) result = requirement.__do_validate__(context) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index b3ad16f4..9670d4fb 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -4,7 +4,7 @@ from pyshacl.pytypes import GraphLike -from .models import (Profile, RequirementLevel, RequirementType, +from .models import (LevelCollection, Profile, RequirementLevel, Severity, ValidationResult, Validator) # set up logging @@ -12,31 +12,29 @@ def validate( - rocrate_path: Union[GraphLike, str, bytes], - profiles_path: str = "./profiles", + rocrate_path: Path, + profiles_path: Path = Path("./profiles"), profile_name: str = "ro-crate", inherit_profiles: bool = True, - ontologies_path: Union[GraphLike, str, bytes] = None, + ontologies_path: Optional[Path] = None, advanced: Optional[bool] = False, - inference: Optional[Literal["owl", "rdfs"]] = False, + inference: Optional[Literal["owl", "rdfs"]] = None, inplace: Optional[bool] = False, abort_on_first: Optional[bool] = True, allow_infos: Optional[bool] = False, allow_warnings: Optional[bool] = False, - requirement_level: Union[str, RequirementType] = RequirementLevel.MUST, - requirement_level_only: bool = False, - serialization_output_path: str = None, + requirement_severity: Union[str, Severity] = Severity.REQUIRED, + requirement_severity_only: bool = False, + serialization_output_path: Optional[Path] = None, serialization_output_format: str = "turtle", **kwargs, ) -> ValidationResult: """ Validate a RO-Crate against a profile """ - # parse requirement level - requirement_level = \ - RequirementLevel.get(requirement_level) \ - if isinstance(requirement_level, str) \ - else requirement_level + # if requirement_severity is a str, convert to Severity + if not isinstance(requirement_severity, Severity): + requirement_severity = Severity[requirement_severity] validator = Validator( rocrate_path=rocrate_path, @@ -50,8 +48,8 @@ def validate( abort_on_first=abort_on_first, allow_infos=allow_infos, allow_warnings=allow_warnings, - requirement_level=requirement_level, - requirement_level_only=requirement_level_only, + requirement_severity=requirement_severity, + requirement_severity_only=requirement_severity_only, serialization_output_path=serialization_output_path, serialization_output_format=serialization_output_format, **kwargs, diff --git a/tests/ro-crate-metadata.json b/tests/ro-crate-metadata.json new file mode 100644 index 00000000..8f45fbb5 --- /dev/null +++ b/tests/ro-crate-metadata.json @@ -0,0 +1,312 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2022-10-07T10:01:24+00:00", + "hasPart": [ + { + "@id": "packed.cwl" + }, + { + "@id": "327fc7aedf4f6b69a42a7c8b808dc5a7aff61376" + }, + { + "@id": "b9214658cc453331b62c2282b772a5c063dbd284" + }, + { + "@id": "97fe1b50b4582cebc7d853796ebd62e3e163aa3f" + } + ], + "mainEntity": { + "@id": "packed.cwl" + }, + "mentions": [ + { + "@id": "#7aeba0c9-78f6-4fb7-85d9-fcbe18fce057" + } + ] + }, + { + "@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": "packed.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "hasPart": [ + { + "@id": "packed.cwl#revtool.cwl" + }, + { + "@id": "packed.cwl#sorttool.cwl" + } + ], + "input": [ + { + "@id": "packed.cwl#main/input" + }, + { + "@id": "packed.cwl#main/reverse_sort" + } + ], + "name": "packed.cwl", + "output": [ + { + "@id": "packed.cwl#main/output" + } + ], + "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/v1.0/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.0" + }, + { + "@id": "packed.cwl#main/input", + "@type": "FormalParameter", + "additionalType": "File", + "connectedTo": { + "@id": "packed.cwl#revtool.cwl/input" + }, + "defaultValue": "file:///home/stain/src/cwltool/tests/wf/hello.txt", + "encodingFormat": "https://www.iana.org/assignments/media-types/text/plain", + "name": "main/input" + }, + { + "@id": "packed.cwl#main/reverse_sort", + "@type": "FormalParameter", + "additionalType": "Boolean", + "connectedTo": { + "@id": "packed.cwl#sorttool.cwl/reverse" + }, + "defaultValue": "True", + "name": "main/reverse_sort" + }, + { + "@id": "packed.cwl#main/output", + "@type": "FormalParameter", + "additionalType": "File", + "name": "main/output" + }, + { + "@id": "packed.cwl#revtool.cwl", + "@type": "SoftwareApplication", + "description": "Reverse each line using the `rev` command", + "input": [ + { + "@id": "packed.cwl#revtool.cwl/input" + } + ], + "name": "revtool.cwl", + "output": [ + { + "@id": "packed.cwl#revtool.cwl/output" + } + ] + }, + { + "@id": "packed.cwl#revtool.cwl/input", + "@type": "FormalParameter", + "additionalType": "File", + "name": "revtool.cwl/input" + }, + { + "@id": "packed.cwl#revtool.cwl/output", + "@type": "FormalParameter", + "additionalType": "File", + "connectedTo": { + "@id": "packed.cwl#sorttool.cwl/input" + }, + "name": "revtool.cwl/output" + }, + { + "@id": "packed.cwl#sorttool.cwl", + "@type": "SoftwareApplication", + "description": "Sort lines using the `sort` command", + "input": [ + { + "@id": "packed.cwl#sorttool.cwl/reverse" + }, + { + "@id": "packed.cwl#sorttool.cwl/input" + } + ], + "name": "sorttool.cwl", + "output": [ + { + "@id": "packed.cwl#sorttool.cwl/output" + } + ] + }, + { + "@id": "packed.cwl#sorttool.cwl/reverse", + "@type": "FormalParameter", + "additionalType": "Boolean", + "name": "sorttool.cwl/reverse" + }, + { + "@id": "packed.cwl#sorttool.cwl/input", + "@type": "FormalParameter", + "additionalType": "File", + "name": "sorttool.cwl/input" + }, + { + "@id": "packed.cwl#sorttool.cwl/output", + "@type": "FormalParameter", + "additionalType": "File", + "connectedTo": { + "@id": "packed.cwl#main/output" + }, + "name": "sorttool.cwl/output" + }, + { + "@id": "#7aeba0c9-78f6-4fb7-85d9-fcbe18fce057", + "@type": "CreateAction", + "endTime": "2018-10-25T15:46:43.020168", + "instrument": { + "@id": "packed.cwl" + }, + "name": "Run of workflow/packed.cwl#main", + "object": [ + { + "@id": "327fc7aedf4f6b69a42a7c8b808dc5a7aff61376" + }, + { + "@id": "#pv-main/reverse_sort" + } + ], + "result": [ + { + "@id": "b9214658cc453331b62c2282b772a5c063dbd284" + } + ], + "startTime": "2018-10-25T15:46:35.211153" + }, + { + "@id": "327fc7aedf4f6b69a42a7c8b808dc5a7aff61376", + "@type": "File", + "exampleOfWork": [ + { + "@id": "packed.cwl#main/input" + }, + { + "@id": "packed.cwl#revtool.cwl/input" + } + ] + }, + { + "@id": "#pv-main/reverse_sort", + "@type": "PropertyValue", + "exampleOfWork": { + "@id": "packed.cwl#main/reverse_sort" + }, + "name": "main/reverse_sort", + "value": "True" + }, + { + "@id": "b9214658cc453331b62c2282b772a5c063dbd284", + "@type": "File", + "exampleOfWork": [ + { + "@id": "packed.cwl#main/output" + }, + { + "@id": "packed.cwl#sorttool.cwl/output" + } + ] + }, + { + "@id": "#a439c61f-2378-49fb-a7e5-7258248daaeb", + "@type": "CreateAction", + "endTime": "2018-10-25T15:46:36.967359", + "instrument": { + "@id": "packed.cwl#revtool.cwl" + }, + "name": "Run of workflow/packed.cwl#main/rev", + "object": [ + { + "@id": "327fc7aedf4f6b69a42a7c8b808dc5a7aff61376" + } + ], + "result": [ + { + "@id": "97fe1b50b4582cebc7d853796ebd62e3e163aa3f" + } + ], + "startTime": "2018-10-25T15:46:35.314101" + }, + { + "@id": "97fe1b50b4582cebc7d853796ebd62e3e163aa3f", + "@type": "File", + "exampleOfWork": [ + { + "@id": "packed.cwl#revtool.cwl/output" + }, + { + "@id": "packed.cwl#sorttool.cwl/input" + } + ] + }, + { + "@id": "#4377b674-1c08-4afe-b3a6-df827c03b1c4", + "@type": "CreateAction", + "endTime": "2018-10-25T15:46:38.069110", + "instrument": { + "@id": "packed.cwl#sorttool.cwl" + }, + "name": "Run of workflow/packed.cwl#main/sorted", + "object": [ + { + "@id": "97fe1b50b4582cebc7d853796ebd62e3e163aa3f" + }, + { + "@id": "#pv-main/sorted/reverse" + } + ], + "result": [ + { + "@id": "b9214658cc453331b62c2282b772a5c063dbd284" + } + ], + "startTime": "2018-10-25T15:46:36.975235" + }, + { + "@id": "#pv-main/sorted/reverse", + "@type": "PropertyValue", + "exampleOfWork": { + "@id": "packed.cwl#sorttool.cwl/reverse" + }, + "name": "main/sorted/reverse", + "value": "True" + } + ] +} \ No newline at end of file diff --git a/tests/test_models.py b/tests/test_models.py index 691ac0b6..23aa9a0c 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,7 @@ import pytest -from rocrate_validator.models import (RequirementLevel, RequirementType, +from rocrate_validator.models import (LevelCollection, RequirementLevel, Severity) @@ -15,38 +15,39 @@ def test_severity_ordering(): assert Severity.RECOMMENDED >= Severity.OPTIONAL -def test_requirement_type_ordering(): - may = RequirementType('MAY', Severity.OPTIONAL) - should = RequirementType('SHOULD', Severity.RECOMMENDED) +def test_level_ordering(): + may = RequirementLevel('MAY', Severity.OPTIONAL) + should = RequirementLevel('SHOULD', Severity.RECOMMENDED) assert may < should assert should > may assert should != may assert may == may assert may != 1 + assert may != RequirementLevel('OPTIONAL', Severity.OPTIONAL) with pytest.raises(TypeError): _ = may > 1 -def test_requirement_type_basics(): - may = RequirementType('MAY', Severity.OPTIONAL) +def test_level_basics(): + may = RequirementLevel('MAY', Severity.OPTIONAL) assert str(may) == "MAY" assert int(may) == Severity.OPTIONAL.value -def test_requirement_levels(): - assert RequirementLevel.get('may') == RequirementLevel.MAY.value +def test_level_collection(): + assert LevelCollection.get('may') == LevelCollection.MAY # Test ordering - assert RequirementLevel.MAY < RequirementLevel.SHOULD - assert RequirementLevel.SHOULD > RequirementLevel.MAY - assert RequirementLevel.SHOULD != RequirementLevel.MAY - assert RequirementLevel.MAY == RequirementLevel.MAY + assert LevelCollection.MAY < LevelCollection.SHOULD + assert LevelCollection.SHOULD > LevelCollection.MAY + assert LevelCollection.SHOULD != LevelCollection.MAY + assert LevelCollection.MAY == LevelCollection.MAY - all_levels = RequirementLevel.all() - assert isinstance(all_levels, dict) + all_levels = LevelCollection.all() assert 10 == len(all_levels) + level_names = [level.name for level in all_levels] # Test a few of the keys - assert 'MAY' in all_levels - assert 'SHOULD_NOT' in all_levels - assert 'RECOMMENDED' in all_levels - assert 'REQUIRED' in all_levels + assert 'MAY' in level_names + assert 'SHOULD_NOT' in level_names + assert 'RECOMMENDED' in level_names + assert 'REQUIRED' in level_names From ad9670535c26d64bed0cf1749a0978960065b0c2 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 27 Mar 2024 17:41:21 +0100 Subject: [PATCH 240/902] Update severity/level nomenclature in requirements --- rocrate_validator/requirements/python/__init__.py | 8 ++++---- rocrate_validator/requirements/shacl/checks.py | 10 ++++------ rocrate_validator/requirements/shacl/validator.py | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index f7316574..48aded29 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -13,14 +13,14 @@ class PyRequirement(Requirement): def __init__(self, - severity: RequirementLevel, + level: RequirementLevel, profile: Profile, name: str = None, description: str = None, path: Path = None, requirement_check_class=None): self.requirement_check_class = requirement_check_class - super().__init__(severity, profile, name, description, path, initialize_checks=True) + super().__init__(level, profile, name, description, path, initialize_checks=True) def __init_checks__(self): # initialize the list of checks @@ -50,7 +50,7 @@ def load(cls, profile: Profile, requirement_level: RequirementLevel, file_path: # instantiate a requirement for each class for requirement_name, requirement_class in classes.items(): - logger.debug("Processing requirement: %r" % requirement_name) + logger.debug("Processing requirement: %r", requirement_name) r = PyRequirement( requirement_level, profile, name=requirement_name.strip() if requirement_name else "", @@ -58,7 +58,7 @@ def load(cls, profile: Profile, requirement_level: RequirementLevel, file_path: path=file_path, requirement_check_class=requirement_class ) - logger.debug("Created requirement: %r" % r) + logger.debug("Created requirement: %r", r) requirements.append(r) return requirements diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 95d12313..56d16eef 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -1,11 +1,9 @@ import logging -from ...models import RequirementCheck -from ...models import Requirement +from ...models import Requirement, RequirementCheck from .models import ShapeProperty - logger = logging.getLogger(__name__) @@ -30,9 +28,9 @@ def __init__(self, def shapeProperty(self) -> ShapeProperty: return self._shapeProperty - @property - def severity(self): - return self.requirement.severity + # @property + # def severity(self): + # return self.requirement.severity @classmethod def get_description(cls, requirement: Requirement): diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 9c97e272..31a8319b 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -128,7 +128,7 @@ def description(self): @property def severity(self): # TODO: map the severity to the CheckIssue severity - return Severity.ERROR + return Severity.REQUIRED class ValidationResult: From 3945ab606ccd7026a0b1cf4c72bc762f2556ef37 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 3 Apr 2024 16:56:33 +0200 Subject: [PATCH 241/902] Whitespace --- .../should/2_root_data_entity_metadata.ttl | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/profiles/ro-crate/should/2_root_data_entity_metadata.ttl b/profiles/ro-crate/should/2_root_data_entity_metadata.ttl index 503f38a9..9ba541e6 100644 --- a/profiles/ro-crate/should/2_root_data_entity_metadata.ttl +++ b/profiles/ro-crate/should/2_root_data_entity_metadata.ttl @@ -9,25 +9,25 @@ :RootDataEntityDirectRecommendedProperties a sh:NodeShape ; sh:name "RO-Crate Data Entity definition: RECOMMENDED properties" ; - sh:description """The Root Data Entity SHOULD have the properties - `name`, `description` and `license` defined in + sh:description """The Root Data Entity SHOULD have the properties + `name`, `description` and `license` defined in """; sh:targetNode : ; - sh:property [ + sh:property [ a sh:PropertyShape ; sh:name "Name of the Root Data Entity" ; - sh:description """The Root Data Entity SHOULD have - a schema_org:name to identify the dataset to human well enough + sh:description """The Root Data Entity SHOULD have + a schema_org:name to identify the dataset to human well enough to disanbiguate it from other datasets""" ; sh:minCount 1 ; sh:nodeKind sh:Literal ; sh:path schema_org:name; sh:message "The Root Data Entity SHOULD have a schema_org:name" ; ] ; - sh:property [ + sh:property [ a sh:PropertyShape ; sh:name "Description of the Root Data Entity" ; - sh:description """The Root Data Entity SHOULD have + sh:description """The Root Data Entity SHOULD have a schema_org:description to further elaborate on the name of the dataset and provide a summary in which the dataset is important""" ; sh:minCount 1 ; @@ -35,11 +35,11 @@ sh:path schema_org:description; sh:message "The Root Data Entity SHOULD have a schema_org:description" ; ] ; - sh:property [ + sh:property [ a sh:PropertyShape ; sh:name "License of the Root Data Entity" ; - sh:description """The Root Data Entity SHOULD - link to a Contextual Entity in the RO-Crate Metadata File + sh:description """The Root Data Entity SHOULD + link to a Contextual Entity in the RO-Crate Metadata File with a name and description.""" ; sh:nodeKind sh:BlankNodeOrIRI ; sh:class schema_org:license ; From 60dd04010c91f9fec800824123623daf300a75f4 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 3 Apr 2024 17:03:53 +0200 Subject: [PATCH 242/902] Map RequirementLevels to Severity in tests --- .../ro-crate/must/test_file_descriptor_entity.py | 12 ++++++------ .../ro-crate/must/test_file_descriptor_format.py | 10 +++++----- .../ro-crate/must/test_root_data_entity.py | 14 +++++++------- tests/shared.py | 15 +++++++++------ 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py index a704ffe9..06b197e6 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py @@ -15,7 +15,7 @@ def test_missing_entity(): """Test a RO-Crate without a file descriptor entity.""" do_entity_test( paths.missing_entity, - models.RequirementLevels.MUST, + models.Severity.REQUIRED, False, ["RO-Crate Metadata File Descriptor entity MUST exist"], ["The root of the document MUST have an entity with @id `ro-crate-metadata.json`"] @@ -26,7 +26,7 @@ def test_invalid_entity_type(): """Test a RO-Crate with an invalid file descriptor entity type.""" do_entity_test( paths.invalid_entity_type, - models.RequirementLevels.MUST, + models.Severity.REQUIRED, False, ["RO-Crate Metadata File Descriptor: recommended properties"], ["The RO-Crate metadata file MUST be a CreativeWork, as per schema.org"] @@ -37,7 +37,7 @@ def test_missing_entity_about(): """Test a RO-Crate with an invalid file descriptor entity type.""" do_entity_test( paths.missing_entity_about, - models.RequirementLevels.MUST, + models.Severity.REQUIRED, False, ["RO-Crate Metadata File Descriptor: recommended properties"], ["The RO-Crate metadata file MUST be a CreativeWork, as per schema.org", @@ -49,7 +49,7 @@ def test_invalid_entity_about_type(): """Test a RO-Crate with an invalid file descriptor entity type.""" do_entity_test( paths.invalid_entity_about_type, - models.RequirementLevels.MUST, + models.Severity.REQUIRED, False, ["RO-Crate Metadata File Descriptor: recommended properties"], ["The RO-Crate metadata file MUST be a CreativeWork, as per schema.org", @@ -61,7 +61,7 @@ def test_missing_conforms_to(): """Test a RO-Crate with an invalid file descriptor entity type.""" do_entity_test( paths.missing_conforms_to, - models.RequirementLevels.MUST, + models.Severity.REQUIRED, False, ["RO-Crate Metadata File Descriptor: recommended properties"], ["The RO-Crate metadata file descriptor MUST have a `conformsTo` " @@ -73,7 +73,7 @@ def test_invalid_conforms_to(): """Test a RO-Crate with an invalid file descriptor entity type.""" do_entity_test( paths.invalid_conforms_to, - models.RequirementLevels.MUST, + models.Severity.REQUIRED, False, ["RO-Crate Metadata File Descriptor: recommended properties"], ["The RO-Crate metadata file descriptor MUST have a `conformsTo` " diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py index 95993679..b612dd69 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py @@ -16,7 +16,7 @@ def test_missing_file_descriptor(): with paths.missing_file_descriptor as rocrate_path: do_entity_test( rocrate_path, - models.RequirementLevels.MUST, + models.Severity.REQUIRED, False, ["FileDescriptorExistence"], [] @@ -27,7 +27,7 @@ def test_not_valid_json_format(): """Test a RO-Crate with an invalid JSON file descriptor format.""" do_entity_test( paths.invalid_json_format, - models.RequirementLevels.MUST, + models.Severity.REQUIRED, False, ["FileDescriptorJsonFormat"], [] @@ -38,7 +38,7 @@ def test_not_valid_jsonld_format_missing_context(): """Test a RO-Crate with an invalid JSON-LD file descriptor format.""" do_entity_test( f"{paths.invalid_jsonld_format}/missing_context", - models.RequirementLevels.MUST, + models.Severity.REQUIRED, False, ["FileDescriptorJsonLdFormat"], [] @@ -52,7 +52,7 @@ def test_not_valid_jsonld_format_missing_ids(): """ do_entity_test( f"{paths.invalid_jsonld_format}/missing_id", - models.RequirementLevels.MUST, + models.Severity.REQUIRED, False, ["FileDescriptorJsonLdFormat"], ["file descriptor does not contain the @id attribute"] @@ -66,7 +66,7 @@ def test_not_valid_jsonld_format_missing_types(): """ do_entity_test( f"{paths.invalid_jsonld_format}/missing_type", - models.RequirementLevels.MUST, + models.Severity.REQUIRED, False, ["FileDescriptorJsonLdFormat"], ["file descriptor does not contain the @type attribute"] diff --git a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index a0bdc19d..7f9e9c47 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -16,7 +16,7 @@ def test_missing_root_data_entity(): """Test a RO-Crate without a root data entity.""" do_entity_test( paths.invalid_root_type, - models.RequirementLevels.MUST, + models.Severity.REQUIRED, False, ["RO-Crate Root Data Entity MUST exist", "RO-Crate Metadata File Descriptor: recommended properties"], @@ -28,7 +28,7 @@ def test_invalid_root_date(): """Test a RO-Crate with an invalid root data entity date.""" do_entity_test( paths.invalid_root_date, - models.RequirementLevels.MUST, + models.Severity.REQUIRED, False, ["RO-Crate Data Entity definition"], ["The datePublished of the Root Data Entity MUST be a valid ISO 8601 date"] @@ -39,7 +39,7 @@ def test_missing_root_name(): """Test a RO-Crate without a root data entity name.""" do_entity_test( paths.missing_root_name, - models.RequirementLevels.SHOULD, + models.Severity.RECOMMENDED, False, ["RO-Crate Data Entity definition: RECOMMENDED properties"], ["The Root Data Entity SHOULD have a schema_org:name"] @@ -50,7 +50,7 @@ def test_missing_root_description(): """Test a RO-Crate without a root data entity description.""" do_entity_test( paths.missing_root_description, - models.RequirementLevels.SHOULD, + models.Severity.RECOMMENDED, False, ["RO-Crate Data Entity definition: RECOMMENDED properties"], ["The Root Data Entity SHOULD have a schema_org:description"] @@ -61,7 +61,7 @@ def test_missing_root_license(): """Test a RO-Crate without a root data entity license.""" do_entity_test( paths.missing_root_license, - models.RequirementLevels.SHOULD, + models.Severity.RECOMMENDED, False, ["RO-Crate Data Entity definition: RECOMMENDED properties"], ["The Root Data Entity SHOULD have a link to a Contextual Entity representing the schema_org:license type"] @@ -72,7 +72,7 @@ def test_missing_root_license_name(): """Test a RO-Crate without a root data entity license name.""" do_entity_test( paths.missing_root_license_name, - models.RequirementLevels.MAY, + models.Severity.OPTIONAL, False, ["License definition"], ["Missing license name"] @@ -83,7 +83,7 @@ def test_missing_root_license_description(): """Test a RO-Crate without a root data entity license description.""" do_entity_test( paths.missing_root_license_description, - models.RequirementLevels.MAY, + models.Severity.OPTIONAL, False, ["License definition"], ["Missing license description"] diff --git a/tests/shared.py b/tests/shared.py index a05e994c..84dabfd8 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -4,7 +4,7 @@ import logging from pathlib import Path -from typing import List +from typing import List, Union from rocrate_validator import models, services @@ -12,8 +12,8 @@ def do_entity_test( - rocrate_path: Path, - requirement_level: models.RequirementType, + rocrate_path: Union[Path, str], + requirement_severity: models.Severity, expected_validation_result: bool, expected_triggered_requirements: List[str], expected_triggered_issues: List[str], @@ -26,9 +26,12 @@ def do_entity_test( failed_requirements = None detected_issues = None + if not isinstance(rocrate_path, Path): + rocrate_path = Path(rocrate_path) + try: logger.debug("Testing RO-Crate @ path: %s", rocrate_path) - logger.debug("Requirement level: %s", requirement_level) + logger.debug("Requirement severity: %s", requirement_severity) # set abort_on_first to False if there are multiple expected requirements or issues if len(expected_triggered_requirements) > 1 \ @@ -38,7 +41,7 @@ def do_entity_test( # validate RO-Crate result: models.ValidationResult = \ services.validate(rocrate_path, - requirement_level=requirement_level, + requirement_severity=requirement_severity, abort_on_first=abort_on_first) logger.debug("Expected validation result: %s", expected_validation_result) assert result.passed() == expected_validation_result, \ @@ -67,7 +70,7 @@ def do_entity_test( if logger.isEnabledFor(logging.DEBUG): logger.exception(e) logger.debug("Failed to validate RO-Crate @ path: %s", rocrate_path) - logger.debug("Requirement level: %s", requirement_level) + logger.debug("Requirement severity: %s", requirement_severity) logger.debug("Expected validation result: %s", expected_validation_result) logger.debug("Expected triggered requirements: %s", expected_triggered_requirements) logger.debug("Expected triggered issues: %s", expected_triggered_issues) From ccc85750322fe947ce3b2b2a64ad4e2ca0be3bc4 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 3 Apr 2024 17:04:46 +0200 Subject: [PATCH 243/902] Implement mapping from SHACL severity to our Severity enum --- rocrate_validator/requirements/shacl/validator.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 31a8319b..809e7763 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -75,8 +75,17 @@ def graph(self) -> Graph: return self._graph @property - def resultSeverity(self): - return self.violation_json[f'{SHACL_NS}resultSeverity'][0]['@id'] + def resultSeverity(self) -> Severity: + shacl_severity = self.violation_json[f'{SHACL_NS}resultSeverity'][0]['@id'] + # we need to map the SHACL severity term to our Severity enum values + if 'http://www.w3.org/ns/shacl#Violation' == shacl_severity: + return Severity.REQUIRED + elif 'http://www.w3.org/ns/shacl#Warning' == shacl_severity: + return Severity.RECOMMENDED + elif 'http://www.w3.org/ns/shacl#Info' == shacl_severity: + return Severity.OPTIONAL + else: + raise RuntimeError(f"Unrecognized SHACL severity term {shacl_severity}") @property def focusNode(self): @@ -127,7 +136,6 @@ def description(self): @property def severity(self): - # TODO: map the severity to the CheckIssue severity return Severity.REQUIRED From bc723acbda7f09886394b912e71ac5a323865d0c Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 3 Apr 2024 17:06:09 +0200 Subject: [PATCH 244/902] Fix `conforms` setting in Validator --- .../requirements/shacl/validator.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 809e7763..c8057a75 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -150,21 +150,25 @@ def __init__(self, validator: Validator, results_graph: Graph, assert (None, URIRef(f"{SHACL_NS}conforms"), None) in results_graph, "Invalid ValidationReport" # store the input properties - self._conforms = conforms self.results_graph = results_graph self._text = results_text self._validator = validator # parse the results graph - self._violations = self.parse_results_graph(results_graph) + self._violations = self._parse_results_graph(results_graph) # initialize the conforms property - logger.debug("Validation report: %s" % self._text) - if conforms is not None: + if conforms is None: self._conforms = len(self._violations) == 0 else: - assert self._conforms == len( - self._violations) == 0, "Invalid validation result" + self._conforms = conforms - def parse_results_graph(self, results_graph: Graph): + logger.debug("Validation report. N. violations: %s, Conforms: %s; Text: %s", + len(self._violations), self._conforms, self._text) + + # TODO: why allow setting conforms through an argument if the value is to be + # computed based on the presence of Violations? + assert self._conforms == (len(self._violations) == 0), "Invalid validation result" + + def _parse_results_graph(self, results_graph: Graph): # Query for validation results query = """ SELECT ?subject @@ -213,6 +217,7 @@ def from_serialized_results_graph(file_path: str, format: str = 'turtle'): logger.debug("Graph loaded from file: %s" % file_path) # return the validation result + assert False, "missing Validator argument to constructor call" return ValidationResult(g) From 4d0bc8142c02d6ed761ac5d0d852f7dbe213660e Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 3 Apr 2024 17:07:06 +0200 Subject: [PATCH 245/902] Extra logging, typing, and other small things --- rocrate_validator/models.py | 47 ++++++++++++------- .../requirements/shacl/checks.py | 11 ++--- .../requirements/shacl/requirements.py | 2 +- .../requirements/shacl/validator.py | 8 ++-- tests/test_models.py | 2 + 5 files changed, 41 insertions(+), 29 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 686adaf9..1a496797 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -146,11 +146,11 @@ def description(self) -> str: self._description = "RO-Crate profile" return self._description - def get_requirement(self, name: str) -> Requirement: - for requirement in self.requirements: - if requirement.name == name: - return requirement - return None + # def get_requirement(self, name: str) -> Requirement: + # for requirement in self.requirements: + # if requirement.name == name: + # return requirement + # return None def load_requirements(self) -> List[Requirement]: """ @@ -175,6 +175,8 @@ def load_requirements(self) -> List[Requirement]: req_id += 1 requirement._order_number = req_id self.add_requirement(requirement) + logger.debug("Profile %s loaded %s requiremens: %s", + self.name, len(self._requirements), self._requirements) return self._requirements @property @@ -187,8 +189,8 @@ def get_requirements( self, severity: Severity = Severity.REQUIRED, exact_match: bool = False) -> List[Requirement]: return [requirement for requirement in self.requirements - if not exact_match and requirement.severity >= severity or - exact_match and requirement.severity == severity] + if (not exact_match and requirement.severity >= severity) or + (exact_match and requirement.severity == severity)] # @property # def requirements_by_severity_map(self) -> Dict[Severity, List[Requirement]]: @@ -385,18 +387,27 @@ def __do_validate__(self, context: ValidationContext) -> bool: """ Internal method to perform the validation """ - # Set the validation context + logger.debug("Validating Requirement %s (level=%s) with %s checks", + self.name, self.level, len(self._checks)) + + # TODO: consider refactoring checks to exclusively use the context they receive as an + # argument. Fetching the context from Requirement seems akin to using a global variable + # and complicates encapsulation. self._validation_context = context - logger.debug("Validation context initialized: %r", context) - # Perform the validation try: + logger.debug("Running %s checks for Requirement '%s'", len(self._checks), self.name) for check in self._checks: try: - check.__do_check__(context) + # TODO: if __do_check__ is internal, why are we calling it from here? + logger.debug("Running check '%s' - Desc: %s", check.name, check.description) + result = check.__do_check__(context) + logger.debug("Ran check '%s'. Got result %s", check.name, result) except Exception as e: - self.validation_result.add_error("Unexpected error during check: %s" % e, check=check) + self.validation_result.add_error(f"Unexpected error during check: {e}", check=check) if logger.isEnabledFor(logging.DEBUG): logger.exception(e) + logger.debug("Checks for Requirement '%s' completed. Checks passed? %s", + self.name, self.validation_result.passed()) # Return the result return self.validation_result.passed() finally: @@ -645,7 +656,7 @@ def __init__(self, severity: Severity, code: int = None, check: RequirementCheck = None): if not isinstance(severity, Severity): - raise TypeError(f"CheckIssue constructed with a severity of type {type(severity)}") + raise TypeError(f"CheckIssue constructed with a severity '{severity}' of type {type(severity)}") self._severity = severity self._message = message self._code = code @@ -771,7 +782,8 @@ def add_issues(self, issues: List[CheckIssue]): self._issues.extend(issues) def add_check_issue(self, message: str, check: RequirementCheck, code: int = None): - self._issues.append(CheckIssue(check.requirement.severity, message, code, check=check)) + c = CheckIssue(check.requirement.severity, message, code, check=check) + self._issues.append(c) def add_error(self, message: str, check: RequirementCheck, code: int = None): self.add_check_issue(message, check, code) @@ -1013,12 +1025,14 @@ def __do_validate__(self, requirements: List[Requirement] = None) -> ValidationR # for profile in profiles: + logger.debug("Validating profile %s", profile.name) # perform the requirements validation if not requirements: requirements = profile.get_requirements( self.requirement_severity, exact_match=self.requirement_severity_only) + logger.debug("For profile %s, validating these %s requirements: %s", + profile.name, len(requirements), requirements) for requirement in requirements: - logger.debug("Validating Requirement: %s", requirement) result = requirement.__do_validate__(context) logger.debug("Validation Requirement result: %s", result) if result: @@ -1037,7 +1051,6 @@ class ValidationContext: def __init__(self, validator: Validator, result: ValidationResult): self._validator = validator self._result = result - self._settings = validator.validation_settings @property def validator(self) -> Validator: @@ -1049,7 +1062,7 @@ def result(self) -> ValidationResult: @property def settings(self) -> Dict: - return self._settings + return self.validator.validation_settings @property def rocrate_path(self) -> Path: diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 56d16eef..943d1c80 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -51,13 +51,10 @@ def check(self): if self.shapeProperty else self.requirement.shape.shape_graph from .validator import Validator as SHACLValidator - shacl_validator = SHACLValidator( - self, shapes_graph=shapes_graph, ont_graph=ontology_graph) - result = shacl_validator.validate( - data_graph=data_graph, - **self.validator.validation_settings - ) - logger.debug("Validation conforms: %s", result.conforms) + shacl_validator = SHACLValidator(self, shapes_graph=shapes_graph, ont_graph=ontology_graph) + result = shacl_validator.validate(data_graph=data_graph, **self.validator.validation_settings) + + logger.debug("Validation '%s' conforms: %s", self.name, result.conforms) if not result.conforms: logger.debug("Validation failed") logger.debug("Validation result: %s", result) diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index a21391a9..2c6f77a7 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -50,7 +50,7 @@ def shape(self) -> Shape: def load(profile: Profile, requirement_level: RequirementLevel, file_path: Path, publicID: str = None) -> List[Requirement]: shapes: Dict[str, Shape] = Shape.load(file_path, publicID=publicID) - logger.debug("Loaded shapes: %s" % shapes) + logger.debug("Loaded %s shapes: %s", len(shapes), shapes) requirements = [] for shape in shapes.values(): requirements.append(SHACLRequirement(requirement_level, shape, profile, file_path)) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index c8057a75..d8480065 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -142,7 +142,7 @@ def severity(self): class ValidationResult: def __init__(self, validator: Validator, results_graph: Graph, - conforms: bool = None, results_text: str = None) -> None: + conforms: Optional[bool] = None, results_text: str = None) -> None: # validate the results graph input assert results_graph is not None, "Invalid graph" assert isinstance(results_graph, Graph), "Invalid graph type" @@ -332,9 +332,9 @@ def validate( **kwargs, ) # log the validation results - logger.debug("Conforms: %r", conforms) - logger.debug("Results Graph: %r", results_graph) - logger.debug("Results Text: %r", results_text) + logger.debug("pyshacl.validate result: Conforms: %r", conforms) + logger.debug("pyshacl.validate result: Results Graph: %r", results_graph) + logger.debug("pyshacl.validate result: Results Text: %r", results_text) # serialize the results graph if serialization_output_path: diff --git a/tests/test_models.py b/tests/test_models.py index 23aa9a0c..13ed991b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -6,6 +6,7 @@ def test_severity_ordering(): + assert hash(Severity.OPTIONAL) != 0 # should be ok as long it hash runs assert Severity.OPTIONAL < Severity.RECOMMENDED assert Severity.RECOMMENDED > Severity.OPTIONAL assert Severity.RECOMMENDED < Severity.REQUIRED @@ -32,6 +33,7 @@ def test_level_basics(): may = RequirementLevel('MAY', Severity.OPTIONAL) assert str(may) == "MAY" assert int(may) == Severity.OPTIONAL.value + assert hash(may) != 0 # should be find as long as it runs def test_level_collection(): From d05e7a581bd8f1f95f6d58824a603ffc58024d93 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 3 Apr 2024 19:05:58 +0200 Subject: [PATCH 246/902] Allow testing valid RO-Crates --- tests/shared.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/shared.py b/tests/shared.py index 84dabfd8..b350e845 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -4,7 +4,7 @@ import logging from pathlib import Path -from typing import List, Union +from typing import List, Optional, Union from rocrate_validator import models, services @@ -15,8 +15,8 @@ def do_entity_test( rocrate_path: Union[Path, str], requirement_severity: models.Severity, expected_validation_result: bool, - expected_triggered_requirements: List[str], - expected_triggered_issues: List[str], + expected_triggered_requirements: Optional[List[str]] = None, + expected_triggered_issues: Optional[List[str]] = None, abort_on_first: bool = True ): """ @@ -29,6 +29,11 @@ def do_entity_test( if not isinstance(rocrate_path, Path): rocrate_path = Path(rocrate_path) + if expected_triggered_requirements is None: + expected_triggered_requirements = [] + if expected_triggered_issues is None: + expected_triggered_issues = [] + try: logger.debug("Testing RO-Crate @ path: %s", rocrate_path) logger.debug("Requirement severity: %s", requirement_severity) From 8f5b1b901418abfd4c92ff8e23742aee458df471 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 3 Apr 2024 19:07:44 +0200 Subject: [PATCH 247/902] Test a valid RO-Crate --- .../data/crates/valid/wrroc-paper/index.html | 2530 +++++++++++++++++ .../crates/valid/wrroc-paper/mapping/Makefile | 18 + .../valid/wrroc-paper/mapping/README.md | 37 + .../wrroc-paper/mapping/environment.lock.yml | 111 + .../valid/wrroc-paper/mapping/environment.yml | 10 + .../mapping/prov-mapping-w-metadata.tsv | 46 + .../wrroc-paper/mapping/prov-mapping.json | 610 ++++ .../wrroc-paper/mapping/prov-mapping.rdf | 310 ++ .../wrroc-paper/mapping/prov-mapping.tsv | 26 + .../wrroc-paper/mapping/prov-mapping.ttl | 392 +++ .../wrroc-paper/mapping/prov-mapping.yml | 15 + .../valid/wrroc-paper/ro-crate-metadata.json | 814 ++++++ .../wrroc-paper/ro-crate-metadata.jsonld | 810 ++++++ .../valid/wrroc-paper/ro-crate-preview.html | 2530 +++++++++++++++++ .../profiles/ro-crate/test_valid_ro-crate.py | 30 + tests/ro_crates.py | 11 +- 16 files changed, 8297 insertions(+), 3 deletions(-) create mode 100644 tests/data/crates/valid/wrroc-paper/index.html create mode 100644 tests/data/crates/valid/wrroc-paper/mapping/Makefile create mode 100644 tests/data/crates/valid/wrroc-paper/mapping/README.md create mode 100644 tests/data/crates/valid/wrroc-paper/mapping/environment.lock.yml create mode 100644 tests/data/crates/valid/wrroc-paper/mapping/environment.yml create mode 100644 tests/data/crates/valid/wrroc-paper/mapping/prov-mapping-w-metadata.tsv create mode 100644 tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.json create mode 100644 tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.rdf create mode 100644 tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.tsv create mode 100644 tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.ttl create mode 100644 tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.yml create mode 100644 tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json create mode 100644 tests/data/crates/valid/wrroc-paper/ro-crate-metadata.jsonld create mode 100644 tests/data/crates/valid/wrroc-paper/ro-crate-preview.html create mode 100644 tests/integration/profiles/ro-crate/test_valid_ro-crate.py diff --git a/tests/data/crates/valid/wrroc-paper/index.html b/tests/data/crates/valid/wrroc-paper/index.html new file mode 100644 index 00000000..7a2fc2a8 --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper/index.html @@ -0,0 +1,2530 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+

Recording provenance of workflow runs with RO-Crate (RO-Crate and mapping)

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Simone Leo

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Michael R Crusoe

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Laura Rodrรญguez-Navas

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Raรผl Sirvent

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Alexander Kanitz

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Paul De Geest

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Rudolf Wittner

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Luca Pireddu

+ + + + +
+ + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Daniel Garijo

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Josรฉ Marรญa Fernรกndez,Josรฉ M. Fernรกndez

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Iacopo Colonnelli

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Matej Gallo

+ + + + +
+ + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Tazro Ohta

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Hirotaka Suetake

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Salvador Capella-Gutierrez

+ + + + +
+ + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Renske de Wit

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Bruno P. Kinoshita,Bruno de Paula Kinoshita

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Stian Soiland-Reyes

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: doi

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Workflow Run Crate task force

+ + + + + + +
+


+
+

Go to: Process Run Crate

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Workflow Run Crate

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Provenance Run Crate

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+ +


+
+

Go to: Apache License, Version 2.0

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+ + + + + + + diff --git a/tests/data/crates/valid/wrroc-paper/mapping/Makefile b/tests/data/crates/valid/wrroc-paper/mapping/Makefile new file mode 100644 index 00000000..82719940 --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper/mapping/Makefile @@ -0,0 +1,18 @@ +.PHONY: clean + +all: prov-mapping.rdf prov-mapping.json prov-mapping.ttl + +clean: + rm -f prov-mapping-w-metadata.tsv prov-mapping.rdf prov-mapping.ttl prov-mapping.json + +prov-mapping-w-metadata.tsv: prov-mapping.yml prov-mapping.tsv + sssom parse -m prov-mapping.yml prov-mapping.tsv > prov-mapping-w-metadata.tsv + +prov-mapping.rdf: prov-mapping-w-metadata.tsv + sssom convert prov-mapping-w-metadata.tsv --output-format rdf --output prov-mapping.rdf + +prov-mapping.ttl: prov-mapping-w-metadata.tsv + sssom convert prov-mapping-w-metadata.tsv --output-format owl --output prov-mapping.ttl + +prov-mapping.json: prov-mapping-w-metadata.tsv + sssom convert prov-mapping-w-metadata.tsv --output-format json --output prov-mapping.json diff --git a/tests/data/crates/valid/wrroc-paper/mapping/README.md b/tests/data/crates/valid/wrroc-paper/mapping/README.md new file mode 100644 index 00000000..49bef88a --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper/mapping/README.md @@ -0,0 +1,37 @@ +# SSSOM mapping from PROV to Workflow Run Crate + +## About SSSOM + +SSSOM is a way to specify semantic mappings, typically based on SKOS. + +* https://mapping-commons.github.io/sssom/spec/ +* https://mapping-commons.github.io/sssom/tutorial/ +* https://www.w3.org/TR/skos-primer/ + +SSSOM mapping ar typically edited collaboratively as tab-separated text files, which can then be converted to OWL assertions using the [SSSOM toolkit](https://mapping-commons.github.io/sssom-py/). + + + +## Editing + +Please edit [prov-mapping.tsv](prov-mapping.tsv) taking care not to break the tabular characters. You may use the _Rainbow CSV_ extension in Visual Studio Code, or a spreadsheet software. + +The metadata headers are maintained in [prov-mapping.yml](prov-mapping.yml) as well as in [ro-crate-metadata.json](../ro-crate-metadata.json). + +## Validating/converting + +Install the [SSSOM toolkit](https://mapping-commons.github.io/sssom-py/installation.html). + +If you use Conda, you can use: + +``` +conda env create -f environment.yml +conda activate sssom +``` + +Then to generate the converted file formats `prov-mapping.rdf prov-mapping.json prov-mapping.ttl` run: + +``` +make +``` + diff --git a/tests/data/crates/valid/wrroc-paper/mapping/environment.lock.yml b/tests/data/crates/valid/wrroc-paper/mapping/environment.lock.yml new file mode 100644 index 00000000..81c27174 --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper/mapping/environment.lock.yml @@ -0,0 +1,111 @@ +name: sssom +channels: + - conda-forge + - bioconda + - defaults +dependencies: + - _libgcc_mutex=0.1=conda_forge + - _openmp_mutex=4.5=2_gnu + - bzip2=1.0.8=h7f98852_4 + - ca-certificates=2023.7.22=hbcca054_0 + - ld_impl_linux-64=2.40=h41732ed_0 + - libexpat=2.5.0=hcb278e6_1 + - libffi=3.4.2=h7f98852_5 + - libgcc-ng=13.2.0=h807b86a_2 + - libgomp=13.2.0=h807b86a_2 + - libnsl=2.0.1=hd590300_0 + - libsqlite=3.43.2=h2797004_0 + - libuuid=2.38.1=h0b41bf4_0 + - libzlib=1.2.13=hd590300_5 + - ncurses=6.4=hcb278e6_0 + - openssl=3.1.3=hd590300_0 + - pip=23.3.1=pyhd8ed1ab_0 + - python=3.12.0=hab00c5b_0_cpython + - readline=8.2=h8228510_1 + - setuptools=68.2.2=pyhd8ed1ab_0 + - tk=8.6.13=h2797004_0 + - wheel=0.41.2=pyhd8ed1ab_0 + - xz=5.2.6=h166bdaf_0 + - pip: + - attrs==23.1.0 + - babel==2.13.0 + - beautifulsoup4==4.12.2 + - cachetools==5.3.1 + - certifi==2023.7.22 + - chardet==5.2.0 + - charset-normalizer==3.3.1 + - click==8.1.7 + - colorama==0.4.6 + - curies==0.6.6 + - deprecated==1.2.14 + - deprecation==2.1.0 + - distlib==0.3.7 + - editorconfig==0.12.3 + - filelock==3.12.4 + - ghp-import==2.1.0 + - greenlet==3.0.0 + - hbreader==0.9.1 + - idna==3.4 + - importlib-metadata==6.8.0 + - iniconfig==2.0.0 + - isodate==0.6.1 + - jinja2==3.1.2 + - jsbeautifier==1.14.9 + - json-flattener==0.1.9 + - jsonasobj2==1.0.4 + - jsonschema==4.19.1 + - jsonschema-specifications==2023.7.1 + - linkml-runtime==1.6.0 + - markdown==3.5 + - markupsafe==2.1.3 + - mergedeep==1.3.4 + - mkdocs==1.5.3 + - mkdocs-material==9.4.6 + - mkdocs-material-extensions==1.3 + - mkdocs-mermaid2-plugin==0.6.0 + - networkx==3.2 + - numpy==1.26.1 + - packaging==23.2 + - paginate==0.5.6 + - pandas==2.1.1 + - pansql==0.0.1 + - pathspec==0.11.2 + - platformdirs==3.11.0 + - pluggy==1.3.0 + - prefixcommons==0.1.12 + - prefixmaps==0.1.7 + - pydantic==1.10.13 + - pygments==2.16.1 + - pymdown-extensions==10.3.1 + - pyparsing==3.1.1 + - pyproject-api==1.6.1 + - pytest==7.4.2 + - pytest-logging==2015.11.4 + - python-dateutil==2.8.2 + - pytrie==0.4.0 + - pytz==2023.3.post1 + - pyyaml==6.0.1 + - pyyaml-env-tag==0.1 + - rdflib==7.0.0 + - referencing==0.30.2 + - regex==2023.10.3 + - requests==2.31.0 + - rpds-py==0.10.6 + - scipy==1.11.3 + - six==1.16.0 + - sortedcontainers==2.4.0 + - soupsieve==2.5 + - sparqlwrapper==2.0.0 + - sqlalchemy==2.0.22 + - sssom==0.3.41 + - sssom-schema==0.15.0 + - tox==4.11.3 + - typing-extensions==4.8.0 + - tzdata==2023.3 + - urllib3==2.0.7 + - validators==0.22.0 + - virtualenv==20.24.5 + - watchdog==3.0.0 + - wrapt==1.15.0 + - zipp==3.17.0 +prefix: /home/stain/miniconda3/envs/sssom diff --git a/tests/data/crates/valid/wrroc-paper/mapping/environment.yml b/tests/data/crates/valid/wrroc-paper/mapping/environment.yml new file mode 100644 index 00000000..9e6d1209 --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper/mapping/environment.yml @@ -0,0 +1,10 @@ +name: sssom +channels: + - conda-forge + - bioconda + - defaults +dependencies: + - python>3.8 + - pip + - pip: + - sssom>0.3 diff --git a/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping-w-metadata.tsv b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping-w-metadata.tsv new file mode 100644 index 00000000..cc71ad32 --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping-w-metadata.tsv @@ -0,0 +1,46 @@ +# curie_map: +# bioschema: https://bioschemas.org/ +# orcid: https://orcid.org/ +# owl: http://www.w3.org/2002/07/owl# +# prov: http://www.w3.org/ns/prov# +# rdf: http://www.w3.org/1999/02/22-rdf-syntax-ns# +# rdfs: http://www.w3.org/2000/01/rdf-schema# +# schema: http://schema.org/ +# semapv: https://w3id.org/semapv/vocab/ +# skos: http://www.w3.org/2004/02/skos/core# +# sssom: https://w3id.org/sssom/ +# license: https://creativecommons.org/publicdomain/zero/1.0/ +# mapping_set_group: researchobject.org +# mapping_set_id: prov_wfrun +# mapping_set_title: Mapping PROV to Workflow Run RO-Crate +# object_source: https://w3id.org/ro/wfrun/provenance/0.3 +# object_source_version: 0.3 +# subject_source: http://www.w3.org/ns/prov-o# +# subject_source_version: 20130430 +subject_id subject_label predicate_id object_id object_label mapping_justification creator_id author_id mapping_date confidence comment +prov:Activity Activity skos:narrowMatch schema:CreateAction Create action semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 Assuming activity is workflow/process execution +prov:Activity Activity skos:narrowMatch schema:OrganizeAction Organize action semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 Assuming activity is workflow/process execution +prov:Agent Agent skos:narrowMatch schema:Organization Organization semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0001-9842-9718 2023-10-22 1.0 +prov:Agent Agent skos:narrowMatch schema:Person Person semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0001-9842-9718 2023-10-22 1.0 +prov:Agent Agent skos:relatedMatch schema:SoftwareApplication Software application semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0001-9842-9718 2023-10-22 0.75 +prov:Entity Entity skos:narrowMatch schema:Dataset Dataset semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 Assuming non-Plan entity +prov:Entity Entity skos:narrowMatch schema:MediaObject File (Media object) semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 Assuming non-Plan entity +prov:Entity Entity skos:narrowMatch schema:PropertyValue Property value semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 Assuming non-Plan entity +prov:Organization Organization skos:exactMatch schema:Organization Organization semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 +prov:Person Person skos:exactMatch schema:Person Person semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 +prov:Plan Plan skos:narrowMatch bioschema:ComputationalWorkflow Computational workflow semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 +prov:Plan Plan skos:narrowMatch schema:HowTo How-to semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 +prov:Plan Plan skos:narrowMatch schema:SoftwareApplication Software application semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 +prov:SoftwareAgent Software agent skos:relatedMatch schema:SoftwareApplication Software application semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 +prov:agent agent skos:narrowMatch schema:instrument instrument semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 Assuming agent is a workflow management system +prov:agent agent skos:relatedMatch schema:agent agent semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 Complex mapping: an agent implies the existence of a qualified association (prov:Association) linked to a prov:Agent through prov:agent +prov:endedAtTime ended at time skos:closeMatch schema:endTime end time semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0001-9842-9718 2023-10-22 0.95 +prov:hadPlan hadPlan skos:relatedMatch schema:instrument instrument semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 Complex mapping: an instrument implies the existence of a qualified association (prov:Association) linked to a prov:Plan through prov:hadPlan +prov:startedAtTime started at time skos:closeMatch schema:startTime start time semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0001-9842-9718 2023-10-22 0.95 +prov:used used skos:exactMatch schema:object object semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 +prov:wasAssociatedWith was associated with skos:narrowMatch schema:agent agent semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 +prov:wasAssociatedWith was associated with skos:narrowMatch schema:instrument instrument semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 +prov:wasEndedBy was ended by skos:relatedMatch schema:agent agent semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145|orcid:0000-0001-9842-9718 2023-10-22 0.95 +prov:wasGeneratedBy was generated by skos:closeMatch schema:result object_label semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 2023-10-22 0.95 Note inverse properties: :ent prov:wasGeneratedBy :act vs :act schema:result :ent +prov:wasStartedBy was started by skos:relatedMatch schema:agent agent semapv:ManualMappingCuration orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145|orcid:0000-0001-9842-9718 2023-10-22 0.95 + diff --git a/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.json b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.json new file mode 100644 index 00000000..d8592e21 --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.json @@ -0,0 +1,610 @@ +{ + "mapping_set_id": "prov_wfrun", + "license": "https://creativecommons.org/publicdomain/zero/1.0/", + "mappings": [ + { + "subject_id": "prov:Activity", + "predicate_id": "skos:narrowMatch", + "object_id": "schema:CreateAction", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "Activity", + "object_label": "Create action", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95, + "comment": "Assuming activity is workflow/process execution" + }, + { + "subject_id": "prov:Activity", + "predicate_id": "skos:narrowMatch", + "object_id": "schema:OrganizeAction", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "Activity", + "object_label": "Organize action", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95, + "comment": "Assuming activity is workflow/process execution" + }, + { + "subject_id": "prov:Agent", + "predicate_id": "skos:narrowMatch", + "object_id": "schema:Organization", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "Agent", + "object_label": "Organization", + "author_id": [ + "orcid:0000-0001-9842-9718" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 1.0 + }, + { + "subject_id": "prov:Agent", + "predicate_id": "skos:narrowMatch", + "object_id": "schema:Person", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "Agent", + "object_label": "Person", + "author_id": [ + "orcid:0000-0001-9842-9718" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 1.0 + }, + { + "subject_id": "prov:Agent", + "predicate_id": "skos:relatedMatch", + "object_id": "schema:SoftwareApplication", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "Agent", + "object_label": "Software application", + "author_id": [ + "orcid:0000-0001-9842-9718" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.75 + }, + { + "subject_id": "prov:Entity", + "predicate_id": "skos:narrowMatch", + "object_id": "schema:Dataset", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "Entity", + "object_label": "Dataset", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95, + "comment": "Assuming non-Plan entity" + }, + { + "subject_id": "prov:Entity", + "predicate_id": "skos:narrowMatch", + "object_id": "schema:MediaObject", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "Entity", + "object_label": "File (Media object)", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95, + "comment": "Assuming non-Plan entity" + }, + { + "subject_id": "prov:Entity", + "predicate_id": "skos:narrowMatch", + "object_id": "schema:PropertyValue", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "Entity", + "object_label": "Property value", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95, + "comment": "Assuming non-Plan entity" + }, + { + "subject_id": "prov:Organization", + "predicate_id": "skos:exactMatch", + "object_id": "schema:Organization", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "Organization", + "object_label": "Organization", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95 + }, + { + "subject_id": "prov:Person", + "predicate_id": "skos:exactMatch", + "object_id": "schema:Person", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "Person", + "object_label": "Person", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95 + }, + { + "subject_id": "prov:Plan", + "predicate_id": "skos:narrowMatch", + "object_id": "bioschema:ComputationalWorkflow", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "Plan", + "object_label": "Computational workflow", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95 + }, + { + "subject_id": "prov:Plan", + "predicate_id": "skos:narrowMatch", + "object_id": "schema:HowTo", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "Plan", + "object_label": "How-to", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95 + }, + { + "subject_id": "prov:Plan", + "predicate_id": "skos:narrowMatch", + "object_id": "schema:SoftwareApplication", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "Plan", + "object_label": "Software application", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95 + }, + { + "subject_id": "prov:SoftwareAgent", + "predicate_id": "skos:relatedMatch", + "object_id": "schema:SoftwareApplication", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "Software agent", + "object_label": "Software application", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95 + }, + { + "subject_id": "prov:agent", + "predicate_id": "skos:narrowMatch", + "object_id": "schema:instrument", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "agent", + "object_label": "instrument", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95, + "comment": "Assuming agent is a workflow management system" + }, + { + "subject_id": "prov:agent", + "predicate_id": "skos:relatedMatch", + "object_id": "schema:agent", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "agent", + "object_label": "agent", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95, + "comment": "Complex mapping: an agent implies the existence of a qualified association (prov:Association) linked to a prov:Agent through prov:agent" + }, + { + "subject_id": "prov:endedAtTime", + "predicate_id": "skos:closeMatch", + "object_id": "schema:endTime", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "ended at time", + "object_label": "end time", + "author_id": [ + "orcid:0000-0001-9842-9718" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95 + }, + { + "subject_id": "prov:hadPlan", + "predicate_id": "skos:relatedMatch", + "object_id": "schema:instrument", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "hadPlan", + "object_label": "instrument", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95, + "comment": "Complex mapping: an instrument implies the existence of a qualified association (prov:Association) linked to a prov:Plan through prov:hadPlan" + }, + { + "subject_id": "prov:startedAtTime", + "predicate_id": "skos:closeMatch", + "object_id": "schema:startTime", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "started at time", + "object_label": "start time", + "author_id": [ + "orcid:0000-0001-9842-9718" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95 + }, + { + "subject_id": "prov:used", + "predicate_id": "skos:exactMatch", + "object_id": "schema:object", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "used", + "object_label": "object", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95 + }, + { + "subject_id": "prov:wasAssociatedWith", + "predicate_id": "skos:narrowMatch", + "object_id": "schema:agent", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "was associated with", + "object_label": "agent", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95 + }, + { + "subject_id": "prov:wasAssociatedWith", + "predicate_id": "skos:narrowMatch", + "object_id": "schema:instrument", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "was associated with", + "object_label": "instrument", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95 + }, + { + "subject_id": "prov:wasEndedBy", + "predicate_id": "skos:relatedMatch", + "object_id": "schema:agent", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "was ended by", + "object_label": "agent", + "author_id": [ + "orcid:0000-0003-0454-7145", + "orcid:0000-0001-9842-9718" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95 + }, + { + "subject_id": "prov:wasGeneratedBy", + "predicate_id": "skos:closeMatch", + "object_id": "schema:result", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "was generated by", + "object_label": "object_label", + "author_id": [ + "orcid:0000-0003-0454-7145" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95, + "comment": "Note inverse properties: :ent prov:wasGeneratedBy :act vs :act schema:result :ent" + }, + { + "subject_id": "prov:wasStartedBy", + "predicate_id": "skos:relatedMatch", + "object_id": "schema:agent", + "mapping_justification": "semapv:ManualMappingCuration", + "subject_label": "was started by", + "object_label": "agent", + "author_id": [ + "orcid:0000-0003-0454-7145", + "orcid:0000-0001-9842-9718" + ], + "creator_id": [ + "orcid:0000-0001-9842-9718" + ], + "mapping_date": "2023-10-22", + "confidence": 0.95 + } + ], + "mapping_set_title": "Mapping PROV to Workflow Run RO-Crate", + "subject_source": "http://www.w3.org/ns/prov-o#", + "subject_source_version": 20130430, + "object_source": "https://w3id.org/ro/wfrun/provenance/0.3", + "object_source_version": 0.3, + "mapping_set_group": "researchobject.org", + "@type": "MappingSet", + "@context": { + "dcterms": "http://purl.org/dc/terms/", + "linkml": "https://w3id.org/linkml/", + "oboInOwl": "http://www.geneontology.org/formats/oboInOwl#", + "owl": "http://www.w3.org/2002/07/owl#", + "pav": "http://purl.org/pav/", + "prov": "http://www.w3.org/ns/prov#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "semapv": "https://w3id.org/semapv/vocab/", + "skos": "http://www.w3.org/2004/02/skos/core#", + "sssom": "https://w3id.org/sssom/", + "@vocab": "https://w3id.org/sssom/", + "author_id": { + "@type": "rdfs:Resource", + "@id": "pav:authoredBy" + }, + "comment": { + "@id": "rdfs:comment" + }, + "confidence": { + "@type": "xsd:double" + }, + "creator_id": { + "@type": "rdfs:Resource", + "@id": "dcterms:creator" + }, + "curation_rule": { + "@type": "rdfs:Resource" + }, + "documentation": { + "@type": "@id" + }, + "homepage": { + "@type": "@id" + }, + "imports": { + "@type": "@id" + }, + "issue_tracker": { + "@type": "@id" + }, + "issue_tracker_item": { + "@type": "rdfs:Resource" + }, + "last_updated": { + "@type": "xsd:date" + }, + "license": { + "@type": "@id", + "@id": "dcterms:license" + }, + "mapping_cardinality": { + "@context": { + "@vocab": "@null", + "text": "skos:notation", + "description": "skos:prefLabel", + "meaning": "@id" + } + }, + "mapping_date": { + "@type": "xsd:date", + "@id": "pav:authoredOn" + }, + "mapping_justification": { + "@type": "rdfs:Resource" + }, + "mapping_provider": { + "@type": "@id" + }, + "mapping_registry_id": { + "@type": "rdfs:Resource" + }, + "mapping_set_description": { + "@id": "dcterms:description" + }, + "mapping_set_id": { + "@type": "@id" + }, + "mapping_set_references": { + "@type": "@id" + }, + "mapping_set_source": { + "@type": "@id", + "@id": "prov:wasDerivedFrom" + }, + "mapping_set_title": { + "@id": "dcterms:title" + }, + "mapping_set_version": { + "@id": "owl:versionInfo" + }, + "mapping_source": { + "@type": "rdfs:Resource" + }, + "mappings": { + "@type": "@id" + }, + "mirror_from": { + "@type": "@id" + }, + "object_id": { + "@type": "rdfs:Resource", + "@id": "owl:annotatedTarget" + }, + "object_match_field": { + "@type": "rdfs:Resource" + }, + "object_preprocessing": { + "@type": "rdfs:Resource" + }, + "object_source": { + "@type": "rdfs:Resource" + }, + "object_type": { + "@context": { + "@vocab": "@null", + "text": "skos:notation", + "description": "skos:prefLabel", + "meaning": "@id" + } + }, + "predicate_id": { + "@type": "rdfs:Resource", + "@id": "owl:annotatedProperty" + }, + "predicate_modifier": { + "@context": { + "@vocab": "@null", + "text": "skos:notation", + "description": "skos:prefLabel", + "meaning": "@id" + } + }, + "predicate_type": { + "@context": { + "@vocab": "@null", + "text": "skos:notation", + "description": "skos:prefLabel", + "meaning": "@id" + } + }, + "publication_date": { + "@type": "xsd:date", + "@id": "dcterms:created" + }, + "registry_confidence": { + "@type": "xsd:double" + }, + "reviewer_id": { + "@type": "rdfs:Resource" + }, + "see_also": { + "@id": "rdfs:seeAlso" + }, + "semantic_similarity_score": { + "@type": "xsd:double" + }, + "subject_id": { + "@type": "rdfs:Resource", + "@id": "owl:annotatedSource" + }, + "subject_match_field": { + "@type": "rdfs:Resource" + }, + "subject_preprocessing": { + "@type": "rdfs:Resource" + }, + "subject_source": { + "@type": "rdfs:Resource" + }, + "subject_type": { + "@context": { + "@vocab": "@null", + "text": "skos:notation", + "description": "skos:prefLabel", + "meaning": "@id" + } + }, + "Mapping": { + "@id": "owl:Axiom" + }, + "bioschema": "https://bioschemas.org/", + "orcid": "https://orcid.org/", + "schema": "http://schema.org/" + } +} \ No newline at end of file diff --git a/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.rdf b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.rdf new file mode 100644 index 00000000..6311784d --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.rdf @@ -0,0 +1,310 @@ +@prefix bioschema: . +@prefix dc1: . +@prefix orcid: . +@prefix owl: . +@prefix pav: . +@prefix prov: . +@prefix rdfs: . +@prefix schema1: . +@prefix semapv: . +@prefix skos: . +@prefix sssom: . +@prefix xsd: . + +[] a sssom:MappingSet ; + dc1:license "https://creativecommons.org/publicdomain/zero/1.0/"^^xsd:anyURI ; + dc1:title "Mapping PROV to Workflow Run RO-Crate" ; + sssom:mapping_set_group "researchobject.org" ; + sssom:mapping_set_id "prov_wfrun"^^xsd:anyURI ; + sssom:mappings [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:exactMatch ; + owl:annotatedSource prov:Organization ; + owl:annotatedTarget schema1:Organization ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Organization" ; + sssom:subject_label "Organization" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Complex mapping: an instrument implies the existence of a qualified association (prov:Association) linked to a prov:Plan through prov:hadPlan" ; + owl:annotatedProperty skos:relatedMatch ; + owl:annotatedSource prov:hadPlan ; + owl:annotatedTarget schema1:instrument ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "instrument" ; + sssom:subject_label "hadPlan" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Assuming non-Plan entity" ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Entity ; + owl:annotatedTarget schema1:MediaObject ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "File (Media object)" ; + sssom:subject_label "Entity" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0001-9842-9718 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Agent ; + owl:annotatedTarget schema1:Organization ; + sssom:confidence 1e+00 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Organization" ; + sssom:subject_label "Agent" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Note inverse properties: :ent prov:wasGeneratedBy :act vs :act schema:result :ent" ; + owl:annotatedProperty skos:closeMatch ; + owl:annotatedSource prov:wasGeneratedBy ; + owl:annotatedTarget schema1:result ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "object_label" ; + sssom:subject_label "was generated by" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0001-9842-9718 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:closeMatch ; + owl:annotatedSource prov:endedAtTime ; + owl:annotatedTarget schema1:endTime ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "end time" ; + sssom:subject_label "ended at time" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0001-9842-9718, + orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:relatedMatch ; + owl:annotatedSource prov:wasStartedBy ; + owl:annotatedTarget schema1:agent ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "agent" ; + sssom:subject_label "was started by" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0001-9842-9718 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:relatedMatch ; + owl:annotatedSource prov:Agent ; + owl:annotatedTarget schema1:SoftwareApplication ; + sssom:confidence 7.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Software application" ; + sssom:subject_label "Agent" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Plan ; + owl:annotatedTarget schema1:HowTo ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "How-to" ; + sssom:subject_label "Plan" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:exactMatch ; + owl:annotatedSource prov:Person ; + owl:annotatedTarget schema1:Person ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Person" ; + sssom:subject_label "Person" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:relatedMatch ; + owl:annotatedSource prov:SoftwareAgent ; + owl:annotatedTarget schema1:SoftwareApplication ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Software application" ; + sssom:subject_label "Software agent" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Assuming agent is a workflow management system" ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:agent ; + owl:annotatedTarget schema1:instrument ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "instrument" ; + sssom:subject_label "agent" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Complex mapping: an agent implies the existence of a qualified association (prov:Association) linked to a prov:Agent through prov:agent" ; + owl:annotatedProperty skos:relatedMatch ; + owl:annotatedSource prov:agent ; + owl:annotatedTarget schema1:agent ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "agent" ; + sssom:subject_label "agent" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0001-9842-9718, + orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:relatedMatch ; + owl:annotatedSource prov:wasEndedBy ; + owl:annotatedTarget schema1:agent ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "agent" ; + sssom:subject_label "was ended by" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Assuming activity is workflow/process execution" ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Activity ; + owl:annotatedTarget schema1:OrganizeAction ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Organize action" ; + sssom:subject_label "Activity" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0001-9842-9718 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Agent ; + owl:annotatedTarget schema1:Person ; + sssom:confidence 1e+00 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Person" ; + sssom:subject_label "Agent" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Plan ; + owl:annotatedTarget bioschema:ComputationalWorkflow ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Computational workflow" ; + sssom:subject_label "Plan" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:exactMatch ; + owl:annotatedSource prov:used ; + owl:annotatedTarget schema1:object ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "object" ; + sssom:subject_label "used" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Assuming non-Plan entity" ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Entity ; + owl:annotatedTarget schema1:Dataset ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Dataset" ; + sssom:subject_label "Entity" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Assuming non-Plan entity" ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Entity ; + owl:annotatedTarget schema1:PropertyValue ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Property value" ; + sssom:subject_label "Entity" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Assuming activity is workflow/process execution" ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Activity ; + owl:annotatedTarget schema1:CreateAction ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Create action" ; + sssom:subject_label "Activity" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:wasAssociatedWith ; + owl:annotatedTarget schema1:instrument ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "instrument" ; + sssom:subject_label "was associated with" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0001-9842-9718 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:closeMatch ; + owl:annotatedSource prov:startedAtTime ; + owl:annotatedTarget schema1:startTime ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "start time" ; + sssom:subject_label "started at time" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:wasAssociatedWith ; + owl:annotatedTarget schema1:agent ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "agent" ; + sssom:subject_label "was associated with" ], + [ a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Plan ; + owl:annotatedTarget schema1:SoftwareApplication ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Software application" ; + sssom:subject_label "Plan" ] ; + sssom:object_source ; + sssom:object_source_version 3e-01 ; + sssom:subject_source ; + sssom:subject_source_version 20130430 . + + diff --git a/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.tsv b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.tsv new file mode 100644 index 00000000..3444964d --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.tsv @@ -0,0 +1,26 @@ +subject_id subject_label predicate_id object_id object_label mapping_justification mapping_date creator_id author_id confidence comment +prov:Activity Activity skos:narrowMatch schema:CreateAction Create action semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 Assuming activity is workflow/process execution +prov:Activity Activity skos:narrowMatch schema:OrganizeAction Organize action semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 Assuming activity is workflow/process execution +prov:agent agent skos:narrowMatch schema:instrument instrument semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 Assuming agent is a workflow management system +prov:Agent Agent skos:narrowMatch schema:Person Person semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0001-9842-9718 1 +prov:Agent Agent skos:narrowMatch schema:Organization Organization semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0001-9842-9718 1 +prov:Agent Agent skos:relatedMatch schema:SoftwareApplication Software application semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0001-9842-9718 0.75 +prov:Person Person skos:exactMatch schema:Person Person semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 +prov:Organization Organization skos:exactMatch schema:Organization Organization semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 +prov:SoftwareAgent Software agent skos:relatedMatch schema:SoftwareApplication Software application semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 +prov:Plan Plan skos:narrowMatch bioschema:ComputationalWorkflow Computational workflow semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 +prov:Plan Plan skos:narrowMatch schema:SoftwareApplication Software application semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 +prov:Plan Plan skos:narrowMatch schema:HowTo How-to semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 +prov:Entity Entity skos:narrowMatch schema:MediaObject File (Media object) semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 Assuming non-Plan entity +prov:Entity Entity skos:narrowMatch schema:Dataset Dataset semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 Assuming non-Plan entity +prov:Entity Entity skos:narrowMatch schema:PropertyValue Property value semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 Assuming non-Plan entity +prov:wasStartedBy was started by skos:relatedMatch schema:agent agent semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145|orcid:0000-0001-9842-9718 0.95 +prov:startedAtTime started at time skos:closeMatch schema:startTime start time semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0001-9842-9718 0.95 +prov:wasEndedBy was ended by skos:relatedMatch schema:agent agent semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145|orcid:0000-0001-9842-9718 0.95 +prov:endedAtTime ended at time skos:closeMatch schema:endTime end time semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0001-9842-9718 0.95 +prov:wasAssociatedWith was associated with skos:narrowMatch schema:agent agent semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 +prov:wasAssociatedWith was associated with skos:narrowMatch schema:instrument instrument semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 +prov:hadPlan hadPlan skos:relatedMatch schema:instrument instrument semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 Complex mapping: an instrument implies the existence of a qualified association (prov:Association) linked to a prov:Plan through prov:hadPlan +prov:agent agent skos:relatedMatch schema:agent agent semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 Complex mapping: an agent implies the existence of a qualified association (prov:Association) linked to a prov:Agent through prov:agent +prov:used used skos:exactMatch schema:object object semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 +prov:wasGeneratedBy was generated by skos:closeMatch schema:result object_label semapv:ManualMappingCuration 2023-10-22 orcid:0000-0001-9842-9718 orcid:0000-0003-0454-7145 0.95 Note inverse properties: :ent prov:wasGeneratedBy :act vs :act schema:result :ent diff --git a/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.ttl b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.ttl new file mode 100644 index 00000000..3a482647 --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.ttl @@ -0,0 +1,392 @@ +@prefix bioschema: . +@prefix dc1: . +@prefix orcid: . +@prefix owl: . +@prefix pav: . +@prefix prov: . +@prefix rdfs: . +@prefix schema1: . +@prefix semapv: . +@prefix skos: . +@prefix sssom: . +@prefix xsd: . + +dc1:creator a owl:AnnotationProperty . + +pav:authoredBy a owl:AnnotationProperty . + +pav:authoredOn a owl:AnnotationProperty . + +rdfs:comment a owl:AnnotationProperty . + +sssom:confidence a owl:AnnotationProperty . + +sssom:mapping_justification a owl:AnnotationProperty . + +sssom:object_label a owl:AnnotationProperty . + +sssom:subject_label a owl:AnnotationProperty . + +prov:Organization skos:exactMatch schema1:Organization . + +prov:Person skos:exactMatch schema1:Person . + +prov:SoftwareAgent skos:relatedMatch schema1:SoftwareApplication . + +prov:endedAtTime skos:closeMatch schema1:endTime . + +prov:hadPlan skos:relatedMatch schema1:instrument . + +prov:startedAtTime skos:closeMatch schema1:startTime . + +prov:used skos:exactMatch schema1:object . + +prov:wasEndedBy skos:relatedMatch schema1:agent . + +prov:wasGeneratedBy skos:closeMatch schema1:result . + +prov:wasStartedBy skos:relatedMatch schema1:agent . + +prov:Activity skos:narrowMatch schema1:CreateAction, + schema1:OrganizeAction . + +prov:agent skos:narrowMatch schema1:instrument ; + skos:relatedMatch schema1:agent . + +prov:wasAssociatedWith skos:narrowMatch schema1:agent, + schema1:instrument . + +prov:Agent skos:narrowMatch schema1:Organization, + schema1:Person ; + skos:relatedMatch schema1:SoftwareApplication . + +prov:Entity skos:narrowMatch schema1:Dataset, + schema1:MediaObject, + schema1:PropertyValue . + +prov:Plan skos:narrowMatch schema1:HowTo, + schema1:SoftwareApplication, + bioschema:ComputationalWorkflow . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:wasAssociatedWith ; + owl:annotatedTarget schema1:instrument ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "instrument" ; + sssom:subject_label "was associated with" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0001-9842-9718 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Agent ; + owl:annotatedTarget schema1:Person ; + sssom:confidence 1e+00 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Person" ; + sssom:subject_label "Agent" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:exactMatch ; + owl:annotatedSource prov:Person ; + owl:annotatedTarget schema1:Person ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Person" ; + sssom:subject_label "Person" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:exactMatch ; + owl:annotatedSource prov:used ; + owl:annotatedTarget schema1:object ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "object" ; + sssom:subject_label "used" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Assuming non-Plan entity" ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Entity ; + owl:annotatedTarget schema1:PropertyValue ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Property value" ; + sssom:subject_label "Entity" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Note inverse properties: :ent prov:wasGeneratedBy :act vs :act schema:result :ent" ; + owl:annotatedProperty skos:closeMatch ; + owl:annotatedSource prov:wasGeneratedBy ; + owl:annotatedTarget schema1:result ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "object_label" ; + sssom:subject_label "was generated by" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0001-9842-9718 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:closeMatch ; + owl:annotatedSource prov:startedAtTime ; + owl:annotatedTarget schema1:startTime ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "start time" ; + sssom:subject_label "started at time" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0001-9842-9718 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:closeMatch ; + owl:annotatedSource prov:endedAtTime ; + owl:annotatedTarget schema1:endTime ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "end time" ; + sssom:subject_label "ended at time" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Assuming activity is workflow/process execution" ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Activity ; + owl:annotatedTarget schema1:OrganizeAction ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Organize action" ; + sssom:subject_label "Activity" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:wasAssociatedWith ; + owl:annotatedTarget schema1:agent ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "agent" ; + sssom:subject_label "was associated with" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0001-9842-9718 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:relatedMatch ; + owl:annotatedSource prov:Agent ; + owl:annotatedTarget schema1:SoftwareApplication ; + sssom:confidence 7.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Software application" ; + sssom:subject_label "Agent" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:exactMatch ; + owl:annotatedSource prov:Organization ; + owl:annotatedTarget schema1:Organization ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Organization" ; + sssom:subject_label "Organization" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0001-9842-9718 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Agent ; + owl:annotatedTarget schema1:Organization ; + sssom:confidence 1e+00 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Organization" ; + sssom:subject_label "Agent" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:relatedMatch ; + owl:annotatedSource prov:SoftwareAgent ; + owl:annotatedTarget schema1:SoftwareApplication ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Software application" ; + sssom:subject_label "Software agent" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Assuming activity is workflow/process execution" ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Activity ; + owl:annotatedTarget schema1:CreateAction ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Create action" ; + sssom:subject_label "Activity" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Complex mapping: an agent implies the existence of a qualified association (prov:Association) linked to a prov:Agent through prov:agent" ; + owl:annotatedProperty skos:relatedMatch ; + owl:annotatedSource prov:agent ; + owl:annotatedTarget schema1:agent ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "agent" ; + sssom:subject_label "agent" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Plan ; + owl:annotatedTarget schema1:SoftwareApplication ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Software application" ; + sssom:subject_label "Plan" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Assuming non-Plan entity" ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Entity ; + owl:annotatedTarget schema1:Dataset ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Dataset" ; + sssom:subject_label "Entity" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Assuming non-Plan entity" ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Entity ; + owl:annotatedTarget schema1:MediaObject ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "File (Media object)" ; + sssom:subject_label "Entity" . + +[] a owl:Ontology ; + dc1:license "https://creativecommons.org/publicdomain/zero/1.0/"^^xsd:anyURI ; + dc1:title "Mapping PROV to Workflow Run RO-Crate" ; + sssom:mapping_set_group "researchobject.org" ; + sssom:mapping_set_id "prov_wfrun"^^xsd:anyURI ; + sssom:object_source ; + sssom:object_source_version 3e-01 ; + sssom:subject_source ; + sssom:subject_source_version 20130430 . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0001-9842-9718, + orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:relatedMatch ; + owl:annotatedSource prov:wasEndedBy ; + owl:annotatedTarget schema1:agent ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "agent" ; + sssom:subject_label "was ended by" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Plan ; + owl:annotatedTarget schema1:HowTo ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "How-to" ; + sssom:subject_label "Plan" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Assuming agent is a workflow management system" ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:agent ; + owl:annotatedTarget schema1:instrument ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "instrument" ; + sssom:subject_label "agent" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:narrowMatch ; + owl:annotatedSource prov:Plan ; + owl:annotatedTarget bioschema:ComputationalWorkflow ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "Computational workflow" ; + sssom:subject_label "Plan" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + rdfs:comment "Complex mapping: an instrument implies the existence of a qualified association (prov:Association) linked to a prov:Plan through prov:hadPlan" ; + owl:annotatedProperty skos:relatedMatch ; + owl:annotatedSource prov:hadPlan ; + owl:annotatedTarget schema1:instrument ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "instrument" ; + sssom:subject_label "hadPlan" . + +[] a owl:Axiom ; + dc1:creator orcid:0000-0001-9842-9718 ; + pav:authoredBy orcid:0000-0001-9842-9718, + orcid:0000-0003-0454-7145 ; + pav:authoredOn "2023-10-22"^^xsd:date ; + owl:annotatedProperty skos:relatedMatch ; + owl:annotatedSource prov:wasStartedBy ; + owl:annotatedTarget schema1:agent ; + sssom:confidence 9.5e-01 ; + sssom:mapping_justification semapv:ManualMappingCuration ; + sssom:object_label "agent" ; + sssom:subject_label "was started by" . + + diff --git a/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.yml b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.yml new file mode 100644 index 00000000..9529edb2 --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.yml @@ -0,0 +1,15 @@ +curie_map: + prov: http://www.w3.org/ns/prov# + schema: http://schema.org/ + wfrun: https://w3id.org/ro/terms/workflow-run + skos: http://www.w3.org/2004/02/skos/core# + bioschema: https://bioschemas.org/ + semapv: https://w3id.org/semapv/vocab/ +license: https://creativecommons.org/publicdomain/zero/1.0/ +mapping_set_group: researchobject.org +mapping_set_id: prov_wfrun +mapping_set_title: 'Mapping PROV to Workflow Run RO-Crate' +object_source: https://w3id.org/ro/wfrun/provenance/0.3 +object_source_version: 0.3 +subject_source: http://www.w3.org/ns/prov-o# +subject_source_version: 20130430 \ No newline at end of file diff --git a/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json b/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json new file mode 100644 index 00000000..9be1c375 --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json @@ -0,0 +1,814 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + { + "Standard": "http://purl.org/dc/terms/Standard", + "Profile": "http://www.w3.org/ns/dx/prof/Profile", + "MappingSet": "https://w3id.org/sssom/schema/MappingSet" + }, + { + "copyrightNotice": "http://schema.org/copyrightNotice", + "interpretedAsClaim": "http://schema.org/interpretedAsClaim", + "archivedAt": "http://schema.org/archivedAt", + "creditText": "http://schema.org/creditText" + } + ], + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.1" + }, + "about": { + "@id": "./" + }, + "author": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + } + }, + { + "@id": "./", + "identifier": { + "@id": "https://doi.org/10.5281/zenodo.10368990" + }, + "url": "https://w3id.org/ro/doi/10.5281/zenodo.10368989", + "@type": "Dataset", + "about": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "author": [ + { + "@id": "https://orcid.org/0000-0001-8271-5429" + }, + { + "@id": "https://orcid.org/0000-0002-2961-9670" + }, + { + "@id": "https://orcid.org/0000-0003-4929-1219" + }, + { + "@id": "https://orcid.org/0000-0003-0606-2512" + }, + { + "@id": "https://orcid.org/0000-0002-3468-0652" + }, + { + "@id": "https://orcid.org/0000-0002-8940-4946" + }, + { + "@id": "https://orcid.org/0000-0002-0003-2024" + }, + { + "@id": "https://orcid.org/0000-0002-4663-5613" + }, + { + "@id": "https://orcid.org/0000-0003-0454-7145" + }, + { + "@id": "https://orcid.org/0000-0002-4806-5140" + }, + { + "@id": "https://orcid.org/0000-0001-9290-2017" + }, + { + "@id": "https://orcid.org/0000-0002-1119-1792" + }, + { + "@id": "https://orcid.org/0000-0003-3777-5945" + }, + { + "@id": "https://orcid.org/0000-0003-2765-0049" + }, + { + "@id": "https://orcid.org/0000-0002-0309-604X" + }, + { + "@id": "https://orcid.org/0000-0003-0902-0086" + }, + { + "@id": "https://orcid.org/0000-0001-8250-4074" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718" + } + ], + "description": "RO-Crate for the manuscript that describes Workflow Run Crate, includes mapping to PROV using SKOS/SSSOM", + "hasPart": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/workflow/0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/provenance/0.3" + }, + { + "@id": "mapping/" + } + ], + "name": "Recording provenance of workflow runs with RO-Crate (RO-Crate and mapping)", + "datePublished": "2023-12-12", + "license": { + "@id": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "creator": [] + }, + { + "@id": "https://doi.org/10.5281/zenodo.10368990", + "@type": "PropertyValue", + "name": "doi", + "propertyID": "https://registry.identifiers.org/registry/doi", + "value": "doi:10.5281/zenodo.10368990", + "url": "https://doi.org/10.5281/zenodo.10368990" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718", + "@type": "Person", + "affiliation": [ + "Department of Computer Science, The University of Manchester, Manchester, United Kingdom", + "Informatics Institute, University of Amsterdam, Amsterdam, The Netherlands" + ], + "name": "Stian Soiland-Reyes" + }, + { + "@id": "https://researchobject.org/workflow-run-crate/", + "@type": "Project", + "member": [ + { + "@id": "https://orcid.org/0000-0001-8271-5429" + }, + { + "@id": "https://orcid.org/0000-0003-4929-1219" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + { + "@id": "https://orcid.org/0000-0002-5432-2748" + }, + { + "@id": "https://orcid.org/0000-0002-4806-5140" + }, + { + "@id": "https://orcid.org/0000-0003-3156-2105" + }, + { + "@id": "https://orcid.org/0000-0002-6190-122X" + }, + { + "@id": "https://orcid.org/0000-0003-0454-7145" + }, + { + "@id": "https://orcid.org/0000-0002-8940-4946" + }, + { + "@id": "https://orcid.org/0000-0003-0606-2512" + }, + { + "@id": "https://orcid.org/0000-0002-3468-0652" + }, + { + "@id": "https://orcid.org/0000-0002-2961-9670" + }, + { + "@id": "https://orcid.org/0000-0003-3986-0510" + }, + { + "@id": "https://orcid.org/0000-0002-0003-2024" + }, + { + "@id": "https://orcid.org/0000-0002-9464-6640" + }, + { + "@id": "https://orcid.org/0000-0001-5845-8880" + }, + { + "@id": "https://orcid.org/0000-0003-4894-4660" + }, + { + "@id": "https://orcid.org/0000-0002-4405-6802" + }, + { + "@id": "https://orcid.org/0000-0001-9290-2017" + }, + { + "@id": "https://orcid.org/0000-0003-0617-9219" + }, + { + "@id": "https://orcid.org/0000-0001-9228-2882" + }, + { + "@id": "https://orcid.org/0000-0003-3898-9451" + }, + { + "@id": "https://orcid.org/0000-0003-3777-5945" + }, + { + "@id": "https://orcid.org/0000-0003-2765-0049" + }, + { + "@id": "https://orcid.org/0000-0001-9818-9320" + }, + { + "@id": "https://orcid.org/0000-0002-8122-9522" + }, + { + "@id": "https://orcid.org/0000-0002-8330-4071" + }, + { + "@id": "https://orcid.org/0000-0003-4073-7456" + }, + { + "@id": "https://orcid.org/0000-0003-1361-7301" + }, + { + "@id": "https://orcid.org/0000-0002-5358-616X" + }, + { + "@id": "https://orcid.org/0000-0002-5477-287X" + }, + { + "@id": "https://orcid.org/0000-0001-8250-4074" + }, + { + "@id": "https://orcid.org/0000-0003-0902-0086" + }, + { + "@id": "https://orcid.org/0000-0001-8172-8981" + }, + { + "@id": "https://orcid.org/0000-0001-6740-9212" + } + ], + "name": "Workflow Run Crate task force", + "parentOrganization": { + "@id": "https://www.researchobject.org/ro-crate/community" + } + }, + { + "@id": "https://orcid.org/0000-0001-8271-5429", + "@type": "Person", + "affiliation": "Center for Advanced Studies, Research, and Development in Sardinia (CRS4), Pula, Sardinia, Italy", + "name": "Simone Leo" + }, + { + "@id": "https://orcid.org/0000-0002-2961-9670", + "@type": "Person", + "affiliation": [ + "Vrije Universiteit Amsterdam, Amsterdam, The Netherlands", + "DTL Projects, The Netherlands", + "Forschungszentrum Jรผlich, Germany" + ], + "name": "Michael R Crusoe" + }, + { + "@id": "https://orcid.org/0000-0003-4929-1219", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": "Laura Rodrรญguez-Navas" + }, + { + "@id": "https://orcid.org/0000-0003-0606-2512", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": "Raรผl Sirvent" + }, + { + "@id": "https://orcid.org/0000-0002-3468-0652", + "@type": "Person", + "affiliation": [ + "Biozentrum, University of Basel, Basel, Switzerland", + "Swiss Institute of Bioinformatics, Lausanne, Switzerland" + ], + "name": "Alexander Kanitz" + }, + { + "@id": "https://orcid.org/0000-0002-8940-4946", + "@type": "Person", + "affiliation": "VIB-UGent Center for Plant Systems Biology, Gent, Belgium", + "name": "Paul De Geest" + }, + { + "@id": "https://orcid.org/0000-0002-0003-2024", + "@type": "Person", + "affiliation": [ + "Faculty of Informatics, Masaryk Universit, Brno, Czech Republic", + "Institute of Computer Science, Masaryk University, Brno, Czech Republic", + "BBMRI-ERIC, Graz, Austria" + ], + "name": "Rudolf Wittner" + }, + { + "@id": "https://orcid.org/0000-0002-4663-5613", + "@type": "Person", + "affiliation": "Center for Advanced Studies, Research, and Development in Sardinia (CRS4), Pula, Sardinia, Italy", + "name": "Luca Pireddu" + }, + { + "@id": "https://orcid.org/0000-0003-0454-7145", + "@type": "Person", + "affiliation": "Ontology Engineering Group, Universidad Politรฉcnica de Madrid, Madrid, Spain", + "name": "Daniel Garijo" + }, + { + "@id": "https://orcid.org/0000-0002-4806-5140", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": [ + "Josรฉ Marรญa Fernรกndez", + "Josรฉ M. Fernรกndez" + ] + }, + { + "@id": "https://orcid.org/0000-0001-9290-2017", + "@type": "Person", + "affiliation": "Computer Science Dept., Universitร  degli Studi di Torino, Torino, Italy", + "name": "Iacopo Colonnelli" + }, + { + "@id": "https://orcid.org/0000-0002-1119-1792", + "@type": "Person", + "affiliation": "Faculty of Informatics, Masaryk University, Brno, Czech Republic", + "name": "Matej Gallo" + }, + { + "@id": "https://orcid.org/0000-0003-3777-5945", + "@type": "Person", + "affiliation": [ + "Database Center for Life Science, Joint Support-Center for Data Science Research, Research Organization of Information and Systems, Shizuoka, Japan", + "Institute for Advanced Academic Research, Chiba University, Chiba, Japan" + ], + "name": "Tazro Ohta" + }, + { + "@id": "https://orcid.org/0000-0003-2765-0049", + "@type": "Person", + "affiliation": "Sator Inc., Tokyo, Japan", + "name": "Hirotaka Suetake" + }, + { + "@id": "https://orcid.org/0000-0002-0309-604X", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": "Salvador Capella-Gutierrez" + }, + { + "@id": "https://orcid.org/0000-0003-0902-0086", + "@type": "Person", + "affiliation": "Vrije Universiteit Amsterdam, Amsterdam, The Netherlands", + "name": "Renske de Wit" + }, + { + "@id": "https://orcid.org/0000-0001-8250-4074", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": [ + "Bruno P. Kinoshita", + "Bruno de Paula Kinoshita" + ] + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.3", + "@type": "Profile", + "author": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "name": "Process Run Crate", + "version": "0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/workflow/0.3", + "@type": "Profile", + "author": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "name": "Workflow Run Crate", + "version": "0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/provenance/0.3", + "@type": "Profile", + "author": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "name": "Provenance Run Crate", + "version": "0.3" + }, + { + "@id": "mapping/environment.yml", + "@type": "File", + "conformsTo": { + "@id": "https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually" + }, + "encodingFormat": [ + "application/yaml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818" + } + ], + "name": "Conda environment for sssom" + }, + { + "@id": "mapping/environment.lock.yml", + "@type": "File", + "conformsTo": { + "@id": "https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually" + }, + "encodingFormat": [ + "application/yaml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818" + } + ], + "name": "Conda environment for sssom, version-pinned" + }, + { + "@id": "mapping/prov-mapping.tsv", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": [ + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#tsv" + } + ], + "author": [ + { + "@id": "https://orcid.org/0000-0003-0454-7145" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718" + } + ], + "creator": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "encodingFormat": [ + "text/tab-separated-values", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/x-fmt/13" + } + ], + "name": "PROV mapping to Workflow Run Crate (SSSOM TSV)" + }, + { + "@id": "mapping/prov-mapping.yml", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": { + "@id": "https://w3id.org/sssom/" + }, + "encodingFormat": [ + "text/yaml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818" + } + ], + "name": "PROV mapping to Workflow Run Crate (SSSOM metadata)" + }, + { + "@id": "mapping/prov-mapping-w-metadata.tsv", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": [ + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#tsv" + } + ], + "encodingFormat": "text/tab-separated-values", + "isBasedOn": [ + { + "@id": "mapping/prov-mapping.tsv" + }, + { + "@id": "mapping/prov-mapping.yml" + } + ], + "name": "PROV mapping to Workflow Run Crate (SSSOM TSV with metadata)" + }, + { + "@id": "mapping/prov-mapping.ttl", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": [ + { + "@id": "http://www.w3.org/TR/owl2-mapping-to-rdf/" + }, + { + "@id": "http://www.w3.org/TR/skos-reference" + } + ], + "encodingFormat": [ + "text/turtle", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/874" + } + ], + "isBasedOn": { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + "name": "PROV to Workflow Run Crate (SKOS and SSSOM OWL axioms)" + }, + { + "@id": "mapping/prov-mapping.rdf", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": [ + { + "@id": "http://www.w3.org/TR/owl2-mapping-to-rdf/" + }, + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#rdfxml-serialised-re-ified-owl-axioms" + } + ], + "encodingFormat": [ + "application/rdf+xml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/875" + } + ], + "isBasedOn": { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + "name": "PROV mapping to Workflow Run Crate (SSSOM OWL axioms)" + }, + { + "@id": "mapping/prov-mapping.json", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": [ + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#json" + } + ], + "encodingFormat": [ + "application/json", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/817" + } + ], + "isBasedOn": { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + "name": "PROV mapping to Workflow Run Crate (SSSOM JSON)" + }, + { + "@id": "http://www.w3.org/TR/owl2-mapping-to-rdf/", + "@type": "CreativeWork", + "name": "OWL 2 (in RDF)" + }, + { + "@id": "http://www.w3.org/TR/skos-reference", + "@type": "CreativeWork", + "alternateName": "SKOS Simple Knowledge Organization System Reference", + "name": "SKOS" + }, + { + "@id": "http://www.w3.org/ns/dx/prof/Profile", + "@type": "DefinedTerm", + "name": "Profile", + "url": "https://www.w3.org/TR/dx-prof/" + }, + { + "@id": "https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually", + "@type": "CreativeWork", + "name": "Conda environment file format" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#json", + "@type": "WebPageElement", + "name": "SSSOM JSON" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#rdfxml-serialised-re-ified-owl-axioms", + "@type": "WebPageElement", + "name": "SSSOM RDF/XML serialised re-ified OWL axioms" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#tsv", + "@type": "WebPageElement", + "name": "SSSOM TSV" + }, + { + "@id": "https://orcid.org/0000-0001-5845-8880", + "@type": "Person", + "name": "Sebastiaan Huber" + }, + { + "@id": "https://orcid.org/0000-0001-6740-9212", + "@type": "Person", + "name": "Samuel Lampa" + }, + { + "@id": "https://orcid.org/0000-0001-8172-8981", + "@type": "Person", + "name": "Jasper Koehorst" + }, + { + "@id": "https://orcid.org/0000-0001-9228-2882", + "@type": "Person", + "name": "Abigail Miller" + }, + { + "@id": "https://orcid.org/0000-0001-9818-9320", + "@type": "Person", + "name": "Johannes Kรถster" + }, + { + "@id": "https://orcid.org/0000-0002-4405-6802", + "@type": "Person", + "name": "Haris Zafeiropoulos" + }, + { + "@id": "https://orcid.org/0000-0002-5358-616X", + "@type": "Person", + "name": "Petr Holub" + }, + { + "@id": "https://orcid.org/0000-0002-5432-2748", + "@type": "Person", + "name": "Paul Brack" + }, + { + "@id": "https://orcid.org/0000-0002-5477-287X", + "@type": "Person", + "name": "Milan Markovic" + }, + { + "@id": "https://orcid.org/0000-0002-6190-122X", + "@type": "Person", + "name": "Ignacio Eguinoa" + }, + { + "@id": "https://orcid.org/0000-0002-8122-9522", + "@type": "Person", + "name": "Luiz Gadelha" + }, + { + "@id": "https://orcid.org/0000-0002-8330-4071", + "@type": "Person", + "name": "Mahnoor Zulfiqar" + }, + { + "@id": "https://orcid.org/0000-0002-9464-6640", + "@type": "Person", + "name": "Wolfgang Maier" + }, + { + "@id": "https://orcid.org/0000-0003-0617-9219", + "@type": "Person", + "name": "Jake Emerson" + }, + { + "@id": "https://orcid.org/0000-0003-1361-7301", + "@type": "Person", + "name": "Maciek Bฤ…k" + }, + { + "@id": "https://orcid.org/0000-0003-3156-2105", + "@type": "Person", + "name": "Alan R Williams" + }, + { + "@id": "https://orcid.org/0000-0003-3898-9451", + "@type": "Person", + "name": "Stelios Ninidakis" + }, + { + "@id": "https://orcid.org/0000-0003-3986-0510", + "@type": "Person", + "name": "LJ Garcia Castro" + }, + { + "@id": "https://orcid.org/0000-0003-4073-7456", + "@type": "Person", + "name": "Romain David" + }, + { + "@id": "https://orcid.org/0000-0003-4894-4660", + "@type": "Person", + "name": "Kevin Jablonka" + }, + { + "@id": "https://www.researchobject.org/ro-crate/community", + "@type": "Project", + "name": "RO-Crate Community" + }, + { + "@id": "https://w3id.org/sssom/", + "@type": [ + "WebPage", + "Standard" + ], + "alternateName": "Simple Standard for Sharing Ontological Mappings", + "name": "SSSOM", + "version": "0.15.0" + }, + { + "@id": "https://w3id.org/sssom/schema/MappingSet", + "@type": "DefinedTerm", + "url": "https://mapping-commons.github.io/sssom/MappingSet/" + }, + { + "@id": "https://www.apache.org/licenses/LICENSE-2.0", + "@type": "CreativeWork", + "name": "Apache License, Version 2.0", + "version": "2.0", + "identifier": { + "@id": "http://spdx.org/licenses/Apache-2.0" + } + }, + { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/", + "@type": "CreativeWork", + "identifier": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "name": "Creative Commons Zero v1.0 Universal", + "version": "1.0" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "identifier": { + "@id": "http://spdx.org/licenses/CC-BY-4.0" + }, + "name": "Creative Commons Attribution 4.0 International", + "version": "4.0" + }, + { + "@id": "http://spdx.org/licenses/Apache-2.0", + "@type": "PropertyValue", + "propertyID": "http://spdx.org/rdf/terms#licenseId", + "name": "spdx", + "value": "Apache-2.0" + }, + { + "@id": "http://spdx.org/licenses/CC0-1.0", + "@type": "PropertyValue", + "propertyID": "http://spdx.org/rdf/terms#licenseId", + "name": "spdx", + "value": "CC0-1.0" + }, + { + "@id": "mapping/", + "@type": "Dataset", + "name": "PROV mapping to Workflow Run Crate", + "description": "Mapping using SKOS and SSSOM", + "hasPart": [ + { + "@id": "mapping/environment.yml" + }, + { + "@id": "mapping/environment.lock.yml" + }, + { + "@id": "mapping/prov-mapping.tsv" + }, + { + "@id": "mapping/prov-mapping.yml" + }, + { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + { + "@id": "mapping/prov-mapping.ttl" + }, + { + "@id": "mapping/prov-mapping.rdf" + }, + { + "@id": "mapping/prov-mapping.json" + } + ] + } + ] +} diff --git a/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.jsonld b/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.jsonld new file mode 100644 index 00000000..82f7f245 --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.jsonld @@ -0,0 +1,810 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + { + "Standard": "http://purl.org/dc/terms/Standard", + "Profile": "http://www.w3.org/ns/dx/prof/Profile", + "MappingSet": "https://w3id.org/sssom/schema/MappingSet" + }, + { + "copyrightNotice": "http://schema.org/copyrightNotice", + "interpretedAsClaim": "http://schema.org/interpretedAsClaim", + "archivedAt": "http://schema.org/archivedAt", + "creditText": "http://schema.org/creditText" + } + ], + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.1" + }, + "about": { + "@id": "./" + }, + "author": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + } + }, + { + "@id": "./", + "identifier": { + "@id": "https://doi.org/10.5281/zenodo.10368990" + }, + "url": "https://w3id.org/ro/doi/10.5281/zenodo.10368989", + "@type": "Dataset", + "about": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "author": [ + { + "@id": "https://orcid.org/0000-0001-8271-5429" + }, + { + "@id": "https://orcid.org/0000-0002-2961-9670" + }, + { + "@id": "https://orcid.org/0000-0003-4929-1219" + }, + { + "@id": "https://orcid.org/0000-0003-0606-2512" + }, + { + "@id": "https://orcid.org/0000-0002-3468-0652" + }, + { + "@id": "https://orcid.org/0000-0002-8940-4946" + }, + { + "@id": "https://orcid.org/0000-0002-0003-2024" + }, + { + "@id": "https://orcid.org/0000-0002-4663-5613" + }, + { + "@id": "https://orcid.org/0000-0003-0454-7145" + }, + { + "@id": "https://orcid.org/0000-0002-4806-5140" + }, + { + "@id": "https://orcid.org/0000-0001-9290-2017" + }, + { + "@id": "https://orcid.org/0000-0002-1119-1792" + }, + { + "@id": "https://orcid.org/0000-0003-3777-5945" + }, + { + "@id": "https://orcid.org/0000-0003-2765-0049" + }, + { + "@id": "https://orcid.org/0000-0002-0309-604X" + }, + { + "@id": "https://orcid.org/0000-0003-0902-0086" + }, + { + "@id": "https://orcid.org/0000-0001-8250-4074" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718" + } + ], + "description": "RO-Crate for the manuscript that describes Workflow Run Crate, includes mapping to PROV using SKOS/SSSOM", + "hasPart": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/workflow/0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/provenance/0.3" + }, + { + "@id": "mapping/" + } + ], + "name": "Recording provenance of workflow runs with RO-Crate (RO-Crate and mapping)", + "datePublished": "2023-12-12T21:08", + "license": { + "@id": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "creator": [] + }, + { + "@id": "https://doi.org/10.5281/zenodo.10368990", + "@type": "PropertyValue", + "name": "doi", + "propertyID": "https://registry.identifiers.org/registry/doi", + "value": "doi:10.5281/zenodo.10368990", + "url": "https://doi.org/10.5281/zenodo.10368990" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718", + "@type": "Person", + "affiliation": [ + "Department of Computer Science, The University of Manchester, Manchester, United Kingdom", + "Informatics Institute, University of Amsterdam, Amsterdam, The Netherlands" + ], + "name": "Stian Soiland-Reyes" + }, + { + "@id": "https://researchobject.org/workflow-run-crate/", + "@type": "Project", + "member": [ + { + "@id": "https://orcid.org/0000-0001-8271-5429" + }, + { + "@id": "https://orcid.org/0000-0003-4929-1219" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + { + "@id": "https://orcid.org/0000-0002-5432-2748" + }, + { + "@id": "https://orcid.org/0000-0002-4806-5140" + }, + { + "@id": "https://orcid.org/0000-0003-3156-2105" + }, + { + "@id": "https://orcid.org/0000-0002-6190-122X" + }, + { + "@id": "https://orcid.org/0000-0003-0454-7145" + }, + { + "@id": "https://orcid.org/0000-0002-8940-4946" + }, + { + "@id": "https://orcid.org/0000-0003-0606-2512" + }, + { + "@id": "https://orcid.org/0000-0002-3468-0652" + }, + { + "@id": "https://orcid.org/0000-0002-2961-9670" + }, + { + "@id": "https://orcid.org/0000-0003-3986-0510" + }, + { + "@id": "https://orcid.org/0000-0002-0003-2024" + }, + { + "@id": "https://orcid.org/0000-0002-9464-6640" + }, + { + "@id": "https://orcid.org/0000-0001-5845-8880" + }, + { + "@id": "https://orcid.org/0000-0003-4894-4660" + }, + { + "@id": "https://orcid.org/0000-0002-4405-6802" + }, + { + "@id": "https://orcid.org/0000-0001-9290-2017" + }, + { + "@id": "https://orcid.org/0000-0003-0617-9219" + }, + { + "@id": "https://orcid.org/0000-0001-9228-2882" + }, + { + "@id": "https://orcid.org/0000-0003-3898-9451" + }, + { + "@id": "https://orcid.org/0000-0003-3777-5945" + }, + { + "@id": "https://orcid.org/0000-0003-2765-0049" + }, + { + "@id": "https://orcid.org/0000-0001-9818-9320" + }, + { + "@id": "https://orcid.org/0000-0002-8122-9522" + }, + { + "@id": "https://orcid.org/0000-0002-8330-4071" + }, + { + "@id": "https://orcid.org/0000-0003-4073-7456" + }, + { + "@id": "https://orcid.org/0000-0003-1361-7301" + }, + { + "@id": "https://orcid.org/0000-0002-5358-616X" + }, + { + "@id": "https://orcid.org/0000-0002-5477-287X" + }, + { + "@id": "https://orcid.org/0000-0001-8250-4074" + }, + { + "@id": "https://orcid.org/0000-0003-0902-0086" + }, + { + "@id": "https://orcid.org/0000-0001-8172-8981" + }, + { + "@id": "https://orcid.org/0000-0001-6740-9212" + } + ], + "name": "Workflow Run Crate task force", + "parentOrganization": { + "@id": "https://www.researchobject.org/ro-crate/community" + } + }, + { + "@id": "https://orcid.org/0000-0001-8271-5429", + "@type": "Person", + "affiliation": "Center for Advanced Studies, Research, and Development in Sardinia (CRS4), Pula, Sardinia, Italy", + "name": "Simone Leo" + }, + { + "@id": "https://orcid.org/0000-0002-2961-9670", + "@type": "Person", + "affiliation": [ + "Vrije Universiteit Amsterdam, Amsterdam, The Netherlands", + "DTL Projects, The Netherlands", + "Forschungszentrum Jรผlich, Germany" + ], + "name": "Michael R Crusoe" + }, + { + "@id": "https://orcid.org/0000-0003-4929-1219", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": "Laura Rodrรญguez-Navas" + }, + { + "@id": "https://orcid.org/0000-0003-0606-2512", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": "Raรผl Sirvent" + }, + { + "@id": "https://orcid.org/0000-0002-3468-0652", + "@type": "Person", + "affiliation": [ + "Biozentrum, University of Basel, Basel, Switzerland", + "Swiss Institute of Bioinformatics, Lausanne, Switzerland" + ], + "name": "Alexander Kanitz" + }, + { + "@id": "https://orcid.org/0000-0002-8940-4946", + "@type": "Person", + "affiliation": "VIB-UGent Center for Plant Systems Biology, Gent, Belgium", + "name": "Paul De Geest" + }, + { + "@id": "https://orcid.org/0000-0002-0003-2024", + "@type": "Person", + "affiliation": [ + "Faculty of Informatics, Masaryk Universit, Brno, Czech Republic", + "Institute of Computer Science, Masaryk University, Brno, Czech Republic", + "BBMRI-ERIC, Graz, Austria" + ], + "name": "Rudolf Wittner" + }, + { + "@id": "https://orcid.org/0000-0002-4663-5613", + "@type": "Person", + "affiliation": "Center for Advanced Studies, Research, and Development in Sardinia (CRS4), Pula, Sardinia, Italy", + "name": "Luca Pireddu" + }, + { + "@id": "https://orcid.org/0000-0003-0454-7145", + "@type": "Person", + "affiliation": "Ontology Engineering Group, Universidad Politรฉcnica de Madrid, Madrid, Spain", + "name": "Daniel Garijo" + }, + { + "@id": "https://orcid.org/0000-0002-4806-5140", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": [ + "Josรฉ Marรญa Fernรกndez", + "Josรฉ M. Fernรกndez" + ] + }, + { + "@id": "https://orcid.org/0000-0001-9290-2017", + "@type": "Person", + "affiliation": "Computer Science Dept., Universitร  degli Studi di Torino, Torino, Italy", + "name": "Iacopo Colonnelli" + }, + { + "@id": "https://orcid.org/0000-0002-1119-1792", + "@type": "Person", + "affiliation": "Faculty of Informatics, Masaryk University, Brno, Czech Republic", + "name": "Matej Gallo" + }, + { + "@id": "https://orcid.org/0000-0003-3777-5945", + "@type": "Person", + "affiliation": [ + "Database Center for Life Science, Joint Support-Center for Data Science Research, Research Organization of Information and Systems, Shizuoka, Japan", + "Institute for Advanced Academic Research, Chiba University, Chiba, Japan" + ], + "name": "Tazro Ohta" + }, + { + "@id": "https://orcid.org/0000-0003-2765-0049", + "@type": "Person", + "affiliation": "Sator Inc., Tokyo, Japan", + "name": "Hirotaka Suetake" + }, + { + "@id": "https://orcid.org/0000-0002-0309-604X", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": "Salvador Capella-Gutierrez" + }, + { + "@id": "https://orcid.org/0000-0003-0902-0086", + "@type": "Person", + "affiliation": "Vrije Universiteit Amsterdam, Amsterdam, The Netherlands", + "name": "Renske de Wit" + }, + { + "@id": "https://orcid.org/0000-0001-8250-4074", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": [ + "Bruno P. Kinoshita", + "Bruno de Paula Kinoshita" + ] + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.3", + "@type": "Profile", + "author": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "name": "Process Run Crate", + "version": "0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/workflow/0.3", + "@type": "Profile", + "author": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "name": "Workflow Run Crate", + "version": "0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/provenance/0.3", + "@type": "Profile", + "author": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "name": "Provenance Run Crate", + "version": "0.3" + }, + { + "@id": "mapping/environment.yml", + "@type": "File", + "conformsTo": { + "@id": "https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually" + }, + "encodingFormat": [ + "application/yaml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818" + } + ], + "name": "Conda environment for sssom" + }, + { + "@id": "mapping/environment.lock.yml", + "@type": "File", + "conformsTo": { + "@id": "https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually" + }, + "encodingFormat": [ + "application/yaml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818" + } + ], + "name": "Conda environment for sssom, version-pinned" + }, + { + "@id": "mapping/prov-mapping.tsv", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": [ + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#tsv" + } + ], + "author": [ + { + "@id": "https://orcid.org/0000-0003-0454-7145" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718" + } + ], + "creator": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "encodingFormat": [ + "text/tab-separated-values", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/x-fmt/13" + } + ], + "name": "PROV mapping to Workflow Run Crate (SSSOM TSV)" + }, + { + "@id": "mapping/prov-mapping.yml", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": { + "@id": "https://w3id.org/sssom/" + }, + "encodingFormat": [ + "text/yaml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818" + } + ], + "name": "PROV mapping to Workflow Run Crate (SSSOM metadata)" + }, + { + "@id": "mapping/prov-mapping-w-metadata.tsv", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": [ + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#tsv" + } + ], + "encodingFormat": "text/tab-separated-values", + "isBasedOn": [ + { + "@id": "mapping/prov-mapping.tsv" + }, + { + "@id": "mapping/prov-mapping.yml" + } + ], + "name": "PROV mapping to Workflow Run Crate (SSSOM TSV with metadata)" + }, + { + "@id": "mapping/prov-mapping.ttl", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": [ + { + "@id": "http://www.w3.org/TR/owl2-mapping-to-rdf/" + }, + { + "@id": "http://www.w3.org/TR/skos-reference" + } + ], + "encodingFormat": [ + "text/turtle", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/874" + } + ], + "isBasedOn": { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + "name": "PROV to Workflow Run Crate (SKOS and SSSOM OWL axioms)" + }, + { + "@id": "mapping/prov-mapping.rdf", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": [ + { + "@id": "http://www.w3.org/TR/owl2-mapping-to-rdf/" + }, + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#rdfxml-serialised-re-ified-owl-axioms" + } + ], + "encodingFormat": [ + "application/rdf+xml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/875" + } + ], + "isBasedOn": { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + "name": "PROV mapping to Workflow Run Crate (SSSOM OWL axioms)" + }, + { + "@id": "mapping/prov-mapping.json", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": [ + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#json" + } + ], + "encodingFormat": [ + "application/json", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/817" + } + ], + "isBasedOn": { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + "name": "PROV mapping to Workflow Run Crate (SSSOM JSON)" + }, + { + "@id": "http://www.w3.org/TR/owl2-mapping-to-rdf/", + "@type": "CreativeWork", + "name": "OWL 2 (in RDF)" + }, + { + "@id": "http://www.w3.org/TR/skos-reference", + "@type": "CreativeWork", + "alternateName": "SKOS Simple Knowledge Organization System Reference", + "name": "SKOS" + }, + { + "@id": "http://www.w3.org/ns/dx/prof/Profile", + "@type": "DefinedTerm", + "name": "Profile", + "url": "https://www.w3.org/TR/dx-prof/" + }, + { + "@id": "https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually", + "@type": "CreativeWork", + "name": "Conda environment file format" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#json", + "@type": "WebPageElement", + "name": "SSSOM JSON" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#rdfxml-serialised-re-ified-owl-axioms", + "@type": "WebPageElement", + "name": "SSSOM RDF/XML serialised re-ified OWL axioms" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#tsv", + "@type": "WebPageElement", + "name": "SSSOM TSV" + }, + { + "@id": "https://orcid.org/0000-0001-5845-8880", + "@type": "Person", + "name": "Sebastiaan Huber" + }, + { + "@id": "https://orcid.org/0000-0001-6740-9212", + "@type": "Person", + "name": "Samuel Lampa" + }, + { + "@id": "https://orcid.org/0000-0001-8172-8981", + "@type": "Person", + "name": "Jasper Koehorst" + }, + { + "@id": "https://orcid.org/0000-0001-9228-2882", + "@type": "Person", + "name": "Abigail Miller" + }, + { + "@id": "https://orcid.org/0000-0001-9818-9320", + "@type": "Person", + "name": "Johannes Kรถster" + }, + { + "@id": "https://orcid.org/0000-0002-4405-6802", + "@type": "Person", + "name": "Haris Zafeiropoulos" + }, + { + "@id": "https://orcid.org/0000-0002-5358-616X", + "@type": "Person", + "name": "Petr Holub" + }, + { + "@id": "https://orcid.org/0000-0002-5432-2748", + "@type": "Person", + "name": "Paul Brack" + }, + { + "@id": "https://orcid.org/0000-0002-5477-287X", + "@type": "Person", + "name": "Milan Markovic" + }, + { + "@id": "https://orcid.org/0000-0002-6190-122X", + "@type": "Person", + "name": "Ignacio Eguinoa" + }, + { + "@id": "https://orcid.org/0000-0002-8122-9522", + "@type": "Person", + "name": "Luiz Gadelha" + }, + { + "@id": "https://orcid.org/0000-0002-8330-4071", + "@type": "Person", + "name": "Mahnoor Zulfiqar" + }, + { + "@id": "https://orcid.org/0000-0002-9464-6640", + "@type": "Person", + "name": "Wolfgang Maier" + }, + { + "@id": "https://orcid.org/0000-0003-0617-9219", + "@type": "Person", + "name": "Jake Emerson" + }, + { + "@id": "https://orcid.org/0000-0003-1361-7301", + "@type": "Person", + "name": "Maciek Bฤ…k" + }, + { + "@id": "https://orcid.org/0000-0003-3156-2105", + "@type": "Person", + "name": "Alan R Williams" + }, + { + "@id": "https://orcid.org/0000-0003-3898-9451", + "@type": "Person", + "name": "Stelios Ninidakis" + }, + { + "@id": "https://orcid.org/0000-0003-3986-0510", + "@type": "Person", + "name": "LJ Garcia Castro" + }, + { + "@id": "https://orcid.org/0000-0003-4073-7456", + "@type": "Person", + "name": "Romain David" + }, + { + "@id": "https://orcid.org/0000-0003-4894-4660", + "@type": "Person", + "name": "Kevin Jablonka" + }, + { + "@id": "https://www.researchobject.org/ro-crate/community", + "@type": "Project", + "name": "RO-Crate Community" + }, + { + "@id": "https://w3id.org/sssom/", + "@type": [ + "WebPage", + "Standard" + ], + "alternateName": "Simple Standard for Sharing Ontological Mappings", + "name": "SSSOM", + "version": "0.15.0" + }, + { + "@id": "https://w3id.org/sssom/schema/MappingSet", + "@type": "DefinedTerm", + "url": "https://mapping-commons.github.io/sssom/MappingSet/" + }, + { + "@id": "https://www.apache.org/licenses/LICENSE-2.0", + "@type": "CreativeWork", + "name": "Apache License, Version 2.0", + "version": "2.0", + "identifier": { + "@id": "http://spdx.org/licenses/Apache-2.0" + } + }, + { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/", + "@type": "CreativeWork", + "identifier": { "@id": "http://spdx.org/licenses/CC0-1.0"}, + "name": "Creative Commons Zero v1.0 Universal", + "version": "1.0" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "identifier": { "@id": "http://spdx.org/licenses/CC-BY-4.0"}, + "name": "Creative Commons Attribution 4.0 International", + "version": "4.0" + }, + { + "@id": "http://spdx.org/licenses/Apache-2.0", + "@type": "PropertyValue", + "propertyID": "http://spdx.org/rdf/terms#licenseId", + "name": "spdx", + "value": "Apache-2.0" + }, + { + "@id": "http://spdx.org/licenses/CC0-1.0", + "@type": "PropertyValue", + "propertyID": "http://spdx.org/rdf/terms#licenseId", + "name": "spdx", + "value": "CC0-1.0" + }, + { + "@id": "mapping/", + "@type": "Dataset", + "name": "PROV mapping to Workflow Run Crate", + "description": "Mapping using SKOS and SSSOM", + "hasPart": [ + { + "@id": "mapping/environment.yml" + }, + { + "@id": "mapping/environment.lock.yml" + }, + { + "@id": "mapping/prov-mapping.tsv" + }, + { + "@id": "mapping/prov-mapping.yml" + }, + { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + { + "@id": "mapping/prov-mapping.ttl" + }, + { + "@id": "mapping/prov-mapping.rdf" + }, + { + "@id": "mapping/prov-mapping.json" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/crates/valid/wrroc-paper/ro-crate-preview.html b/tests/data/crates/valid/wrroc-paper/ro-crate-preview.html new file mode 100644 index 00000000..7a2fc2a8 --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper/ro-crate-preview.html @@ -0,0 +1,2530 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+

Recording provenance of workflow runs with RO-Crate (RO-Crate and mapping)

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Simone Leo

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Michael R Crusoe

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Laura Rodrรญguez-Navas

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Raรผl Sirvent

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Alexander Kanitz

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Paul De Geest

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Rudolf Wittner

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Luca Pireddu

+ + + + +
+ + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Daniel Garijo

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Josรฉ Marรญa Fernรกndez,Josรฉ M. Fernรกndez

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Iacopo Colonnelli

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Matej Gallo

+ + + + +
+ + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Tazro Ohta

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Hirotaka Suetake

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Salvador Capella-Gutierrez

+ + + + +
+ + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Renske de Wit

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Bruno P. Kinoshita,Bruno de Paula Kinoshita

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Stian Soiland-Reyes

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: doi

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Workflow Run Crate task force

+ + + + + + +
+


+
+

Go to: Process Run Crate

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Workflow Run Crate

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+

Go to: Provenance Run Crate

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+ +


+
+

Go to: Apache License, Version 2.0

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+


+
+ + + + + + + diff --git a/tests/integration/profiles/ro-crate/test_valid_ro-crate.py b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py new file mode 100644 index 00000000..5cdb01bd --- /dev/null +++ b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py @@ -0,0 +1,30 @@ +import logging + +import pytest + +from rocrate_validator.models import Severity +from tests.ro_crates import ValidROC +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail(reason="Known problem with data validation in RO-Crate profile") +def test_valid_roc_required(): + """Test a valid RO-Crate.""" + do_entity_test( + ValidROC().wrroc_paper, + Severity.REQUIRED, + True + ) + + +@pytest.mark.xfail(reason="Known problem with data validation in RO-Crate profile") +def test_valid_roc_recommended(): + """Test a valid RO-Crate.""" + do_entity_test( + ValidROC().wrroc_paper, + Severity.RECOMMENDED, + True + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 63f53e6d..9b70d0dc 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -1,10 +1,9 @@ +import logging import os from pathlib import Path - -from pytest import fixture from tempfile import TemporaryDirectory -import logging +from pytest import fixture logging.basicConfig(level=logging.DEBUG) @@ -20,6 +19,12 @@ def ro_crates_path(): return CRATES_DATA_PATH +class ValidROC: + @property + def wrroc_paper(self) -> Path: + return Path(f"{VALID_CRATES_DATA_PATH}/wrroc-paper") + + class InvalidFileDescriptor: base_path = f"{INVALID_CRATES_DATA_PATH}/0_file_descriptor_format" From c71fa7043c733cf99a651952be7f6524b2cb3a4e Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 3 Apr 2024 19:08:03 +0200 Subject: [PATCH 248/902] Whitespace --- profiles/ro-crate/must/2_root_data_entity_metadata.ttl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/profiles/ro-crate/must/2_root_data_entity_metadata.ttl b/profiles/ro-crate/must/2_root_data_entity_metadata.ttl index 704c01cb..54673521 100644 --- a/profiles/ro-crate/must/2_root_data_entity_metadata.ttl +++ b/profiles/ro-crate/must/2_root_data_entity_metadata.ttl @@ -24,10 +24,10 @@ :RootDataEntityRequiredDirectProperties a sh:NodeShape ; sh:name "RO-Crate Data Entity definition" ; - sh:description """The Root Data Entity MUST have the properties defined in + sh:description """The Root Data Entity MUST have the properties defined in """; sh:targetNode : ; - sh:property [ + sh:property [ a sh:PropertyShape ; sh:name "Date Published of the Root Data Entity" ; sh:description "The datePublished of the Root Data Entity MUST be a valid ISO 8601 date" ; From 14d3239b64cfe77dc555562203ab3d997b2571ce Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 4 Apr 2024 09:50:13 +0200 Subject: [PATCH 249/902] Add test with valid RO-Crate with a date format that passes validation now --- .../valid/wrroc-paper-long-date/index.html | 1 + .../valid/wrroc-paper-long-date/mapping | 1 + .../ro-crate-metadata.json | 814 ++++++++++++++++++ .../ro-crate-metadata.jsonld | 1 + .../ro-crate-preview.html | 1 + .../profiles/ro-crate/test_valid_ro-crate.py | 9 + tests/ro_crates.py | 4 + 7 files changed, 831 insertions(+) create mode 120000 tests/data/crates/valid/wrroc-paper-long-date/index.html create mode 120000 tests/data/crates/valid/wrroc-paper-long-date/mapping create mode 100644 tests/data/crates/valid/wrroc-paper-long-date/ro-crate-metadata.json create mode 120000 tests/data/crates/valid/wrroc-paper-long-date/ro-crate-metadata.jsonld create mode 120000 tests/data/crates/valid/wrroc-paper-long-date/ro-crate-preview.html diff --git a/tests/data/crates/valid/wrroc-paper-long-date/index.html b/tests/data/crates/valid/wrroc-paper-long-date/index.html new file mode 120000 index 00000000..a79e58d6 --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper-long-date/index.html @@ -0,0 +1 @@ +../wrroc-paper/index.html \ No newline at end of file diff --git a/tests/data/crates/valid/wrroc-paper-long-date/mapping b/tests/data/crates/valid/wrroc-paper-long-date/mapping new file mode 120000 index 00000000..3853ffbd --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper-long-date/mapping @@ -0,0 +1 @@ +../wrroc-paper/mapping/ \ No newline at end of file diff --git a/tests/data/crates/valid/wrroc-paper-long-date/ro-crate-metadata.json b/tests/data/crates/valid/wrroc-paper-long-date/ro-crate-metadata.json new file mode 100644 index 00000000..0ca2fa9f --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper-long-date/ro-crate-metadata.json @@ -0,0 +1,814 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + { + "Standard": "http://purl.org/dc/terms/Standard", + "Profile": "http://www.w3.org/ns/dx/prof/Profile", + "MappingSet": "https://w3id.org/sssom/schema/MappingSet" + }, + { + "copyrightNotice": "http://schema.org/copyrightNotice", + "interpretedAsClaim": "http://schema.org/interpretedAsClaim", + "archivedAt": "http://schema.org/archivedAt", + "creditText": "http://schema.org/creditText" + } + ], + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.1" + }, + "about": { + "@id": "./" + }, + "author": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + } + }, + { + "@id": "./", + "identifier": { + "@id": "https://doi.org/10.5281/zenodo.10368990" + }, + "url": "https://w3id.org/ro/doi/10.5281/zenodo.10368989", + "@type": "Dataset", + "about": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "author": [ + { + "@id": "https://orcid.org/0000-0001-8271-5429" + }, + { + "@id": "https://orcid.org/0000-0002-2961-9670" + }, + { + "@id": "https://orcid.org/0000-0003-4929-1219" + }, + { + "@id": "https://orcid.org/0000-0003-0606-2512" + }, + { + "@id": "https://orcid.org/0000-0002-3468-0652" + }, + { + "@id": "https://orcid.org/0000-0002-8940-4946" + }, + { + "@id": "https://orcid.org/0000-0002-0003-2024" + }, + { + "@id": "https://orcid.org/0000-0002-4663-5613" + }, + { + "@id": "https://orcid.org/0000-0003-0454-7145" + }, + { + "@id": "https://orcid.org/0000-0002-4806-5140" + }, + { + "@id": "https://orcid.org/0000-0001-9290-2017" + }, + { + "@id": "https://orcid.org/0000-0002-1119-1792" + }, + { + "@id": "https://orcid.org/0000-0003-3777-5945" + }, + { + "@id": "https://orcid.org/0000-0003-2765-0049" + }, + { + "@id": "https://orcid.org/0000-0002-0309-604X" + }, + { + "@id": "https://orcid.org/0000-0003-0902-0086" + }, + { + "@id": "https://orcid.org/0000-0001-8250-4074" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718" + } + ], + "description": "RO-Crate for the manuscript that describes Workflow Run Crate, includes mapping to PROV using SKOS/SSSOM", + "hasPart": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/workflow/0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/provenance/0.3" + }, + { + "@id": "mapping/" + } + ], + "name": "Recording provenance of workflow runs with RO-Crate (RO-Crate and mapping)", + "datePublished": "2023-12-12T12:00:00+06:00", + "license": { + "@id": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "creator": [] + }, + { + "@id": "https://doi.org/10.5281/zenodo.10368990", + "@type": "PropertyValue", + "name": "doi", + "propertyID": "https://registry.identifiers.org/registry/doi", + "value": "doi:10.5281/zenodo.10368990", + "url": "https://doi.org/10.5281/zenodo.10368990" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718", + "@type": "Person", + "affiliation": [ + "Department of Computer Science, The University of Manchester, Manchester, United Kingdom", + "Informatics Institute, University of Amsterdam, Amsterdam, The Netherlands" + ], + "name": "Stian Soiland-Reyes" + }, + { + "@id": "https://researchobject.org/workflow-run-crate/", + "@type": "Project", + "member": [ + { + "@id": "https://orcid.org/0000-0001-8271-5429" + }, + { + "@id": "https://orcid.org/0000-0003-4929-1219" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + { + "@id": "https://orcid.org/0000-0002-5432-2748" + }, + { + "@id": "https://orcid.org/0000-0002-4806-5140" + }, + { + "@id": "https://orcid.org/0000-0003-3156-2105" + }, + { + "@id": "https://orcid.org/0000-0002-6190-122X" + }, + { + "@id": "https://orcid.org/0000-0003-0454-7145" + }, + { + "@id": "https://orcid.org/0000-0002-8940-4946" + }, + { + "@id": "https://orcid.org/0000-0003-0606-2512" + }, + { + "@id": "https://orcid.org/0000-0002-3468-0652" + }, + { + "@id": "https://orcid.org/0000-0002-2961-9670" + }, + { + "@id": "https://orcid.org/0000-0003-3986-0510" + }, + { + "@id": "https://orcid.org/0000-0002-0003-2024" + }, + { + "@id": "https://orcid.org/0000-0002-9464-6640" + }, + { + "@id": "https://orcid.org/0000-0001-5845-8880" + }, + { + "@id": "https://orcid.org/0000-0003-4894-4660" + }, + { + "@id": "https://orcid.org/0000-0002-4405-6802" + }, + { + "@id": "https://orcid.org/0000-0001-9290-2017" + }, + { + "@id": "https://orcid.org/0000-0003-0617-9219" + }, + { + "@id": "https://orcid.org/0000-0001-9228-2882" + }, + { + "@id": "https://orcid.org/0000-0003-3898-9451" + }, + { + "@id": "https://orcid.org/0000-0003-3777-5945" + }, + { + "@id": "https://orcid.org/0000-0003-2765-0049" + }, + { + "@id": "https://orcid.org/0000-0001-9818-9320" + }, + { + "@id": "https://orcid.org/0000-0002-8122-9522" + }, + { + "@id": "https://orcid.org/0000-0002-8330-4071" + }, + { + "@id": "https://orcid.org/0000-0003-4073-7456" + }, + { + "@id": "https://orcid.org/0000-0003-1361-7301" + }, + { + "@id": "https://orcid.org/0000-0002-5358-616X" + }, + { + "@id": "https://orcid.org/0000-0002-5477-287X" + }, + { + "@id": "https://orcid.org/0000-0001-8250-4074" + }, + { + "@id": "https://orcid.org/0000-0003-0902-0086" + }, + { + "@id": "https://orcid.org/0000-0001-8172-8981" + }, + { + "@id": "https://orcid.org/0000-0001-6740-9212" + } + ], + "name": "Workflow Run Crate task force", + "parentOrganization": { + "@id": "https://www.researchobject.org/ro-crate/community" + } + }, + { + "@id": "https://orcid.org/0000-0001-8271-5429", + "@type": "Person", + "affiliation": "Center for Advanced Studies, Research, and Development in Sardinia (CRS4), Pula, Sardinia, Italy", + "name": "Simone Leo" + }, + { + "@id": "https://orcid.org/0000-0002-2961-9670", + "@type": "Person", + "affiliation": [ + "Vrije Universiteit Amsterdam, Amsterdam, The Netherlands", + "DTL Projects, The Netherlands", + "Forschungszentrum Jรผlich, Germany" + ], + "name": "Michael R Crusoe" + }, + { + "@id": "https://orcid.org/0000-0003-4929-1219", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": "Laura Rodrรญguez-Navas" + }, + { + "@id": "https://orcid.org/0000-0003-0606-2512", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": "Raรผl Sirvent" + }, + { + "@id": "https://orcid.org/0000-0002-3468-0652", + "@type": "Person", + "affiliation": [ + "Biozentrum, University of Basel, Basel, Switzerland", + "Swiss Institute of Bioinformatics, Lausanne, Switzerland" + ], + "name": "Alexander Kanitz" + }, + { + "@id": "https://orcid.org/0000-0002-8940-4946", + "@type": "Person", + "affiliation": "VIB-UGent Center for Plant Systems Biology, Gent, Belgium", + "name": "Paul De Geest" + }, + { + "@id": "https://orcid.org/0000-0002-0003-2024", + "@type": "Person", + "affiliation": [ + "Faculty of Informatics, Masaryk Universit, Brno, Czech Republic", + "Institute of Computer Science, Masaryk University, Brno, Czech Republic", + "BBMRI-ERIC, Graz, Austria" + ], + "name": "Rudolf Wittner" + }, + { + "@id": "https://orcid.org/0000-0002-4663-5613", + "@type": "Person", + "affiliation": "Center for Advanced Studies, Research, and Development in Sardinia (CRS4), Pula, Sardinia, Italy", + "name": "Luca Pireddu" + }, + { + "@id": "https://orcid.org/0000-0003-0454-7145", + "@type": "Person", + "affiliation": "Ontology Engineering Group, Universidad Politรฉcnica de Madrid, Madrid, Spain", + "name": "Daniel Garijo" + }, + { + "@id": "https://orcid.org/0000-0002-4806-5140", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": [ + "Josรฉ Marรญa Fernรกndez", + "Josรฉ M. Fernรกndez" + ] + }, + { + "@id": "https://orcid.org/0000-0001-9290-2017", + "@type": "Person", + "affiliation": "Computer Science Dept., Universitร  degli Studi di Torino, Torino, Italy", + "name": "Iacopo Colonnelli" + }, + { + "@id": "https://orcid.org/0000-0002-1119-1792", + "@type": "Person", + "affiliation": "Faculty of Informatics, Masaryk University, Brno, Czech Republic", + "name": "Matej Gallo" + }, + { + "@id": "https://orcid.org/0000-0003-3777-5945", + "@type": "Person", + "affiliation": [ + "Database Center for Life Science, Joint Support-Center for Data Science Research, Research Organization of Information and Systems, Shizuoka, Japan", + "Institute for Advanced Academic Research, Chiba University, Chiba, Japan" + ], + "name": "Tazro Ohta" + }, + { + "@id": "https://orcid.org/0000-0003-2765-0049", + "@type": "Person", + "affiliation": "Sator Inc., Tokyo, Japan", + "name": "Hirotaka Suetake" + }, + { + "@id": "https://orcid.org/0000-0002-0309-604X", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": "Salvador Capella-Gutierrez" + }, + { + "@id": "https://orcid.org/0000-0003-0902-0086", + "@type": "Person", + "affiliation": "Vrije Universiteit Amsterdam, Amsterdam, The Netherlands", + "name": "Renske de Wit" + }, + { + "@id": "https://orcid.org/0000-0001-8250-4074", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": [ + "Bruno P. Kinoshita", + "Bruno de Paula Kinoshita" + ] + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.3", + "@type": "Profile", + "author": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "name": "Process Run Crate", + "version": "0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/workflow/0.3", + "@type": "Profile", + "author": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "name": "Workflow Run Crate", + "version": "0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/provenance/0.3", + "@type": "Profile", + "author": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "name": "Provenance Run Crate", + "version": "0.3" + }, + { + "@id": "mapping/environment.yml", + "@type": "File", + "conformsTo": { + "@id": "https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually" + }, + "encodingFormat": [ + "application/yaml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818" + } + ], + "name": "Conda environment for sssom" + }, + { + "@id": "mapping/environment.lock.yml", + "@type": "File", + "conformsTo": { + "@id": "https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually" + }, + "encodingFormat": [ + "application/yaml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818" + } + ], + "name": "Conda environment for sssom, version-pinned" + }, + { + "@id": "mapping/prov-mapping.tsv", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": [ + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#tsv" + } + ], + "author": [ + { + "@id": "https://orcid.org/0000-0003-0454-7145" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718" + } + ], + "creator": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "encodingFormat": [ + "text/tab-separated-values", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/x-fmt/13" + } + ], + "name": "PROV mapping to Workflow Run Crate (SSSOM TSV)" + }, + { + "@id": "mapping/prov-mapping.yml", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": { + "@id": "https://w3id.org/sssom/" + }, + "encodingFormat": [ + "text/yaml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818" + } + ], + "name": "PROV mapping to Workflow Run Crate (SSSOM metadata)" + }, + { + "@id": "mapping/prov-mapping-w-metadata.tsv", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": [ + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#tsv" + } + ], + "encodingFormat": "text/tab-separated-values", + "isBasedOn": [ + { + "@id": "mapping/prov-mapping.tsv" + }, + { + "@id": "mapping/prov-mapping.yml" + } + ], + "name": "PROV mapping to Workflow Run Crate (SSSOM TSV with metadata)" + }, + { + "@id": "mapping/prov-mapping.ttl", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": [ + { + "@id": "http://www.w3.org/TR/owl2-mapping-to-rdf/" + }, + { + "@id": "http://www.w3.org/TR/skos-reference" + } + ], + "encodingFormat": [ + "text/turtle", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/874" + } + ], + "isBasedOn": { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + "name": "PROV to Workflow Run Crate (SKOS and SSSOM OWL axioms)" + }, + { + "@id": "mapping/prov-mapping.rdf", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": [ + { + "@id": "http://www.w3.org/TR/owl2-mapping-to-rdf/" + }, + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#rdfxml-serialised-re-ified-owl-axioms" + } + ], + "encodingFormat": [ + "application/rdf+xml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/875" + } + ], + "isBasedOn": { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + "name": "PROV mapping to Workflow Run Crate (SSSOM OWL axioms)" + }, + { + "@id": "mapping/prov-mapping.json", + "@type": [ + "File", + "MappingSet" + ], + "conformsTo": [ + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#json" + } + ], + "encodingFormat": [ + "application/json", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/817" + } + ], + "isBasedOn": { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + "name": "PROV mapping to Workflow Run Crate (SSSOM JSON)" + }, + { + "@id": "http://www.w3.org/TR/owl2-mapping-to-rdf/", + "@type": "CreativeWork", + "name": "OWL 2 (in RDF)" + }, + { + "@id": "http://www.w3.org/TR/skos-reference", + "@type": "CreativeWork", + "alternateName": "SKOS Simple Knowledge Organization System Reference", + "name": "SKOS" + }, + { + "@id": "http://www.w3.org/ns/dx/prof/Profile", + "@type": "DefinedTerm", + "name": "Profile", + "url": "https://www.w3.org/TR/dx-prof/" + }, + { + "@id": "https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually", + "@type": "CreativeWork", + "name": "Conda environment file format" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#json", + "@type": "WebPageElement", + "name": "SSSOM JSON" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#rdfxml-serialised-re-ified-owl-axioms", + "@type": "WebPageElement", + "name": "SSSOM RDF/XML serialised re-ified OWL axioms" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#tsv", + "@type": "WebPageElement", + "name": "SSSOM TSV" + }, + { + "@id": "https://orcid.org/0000-0001-5845-8880", + "@type": "Person", + "name": "Sebastiaan Huber" + }, + { + "@id": "https://orcid.org/0000-0001-6740-9212", + "@type": "Person", + "name": "Samuel Lampa" + }, + { + "@id": "https://orcid.org/0000-0001-8172-8981", + "@type": "Person", + "name": "Jasper Koehorst" + }, + { + "@id": "https://orcid.org/0000-0001-9228-2882", + "@type": "Person", + "name": "Abigail Miller" + }, + { + "@id": "https://orcid.org/0000-0001-9818-9320", + "@type": "Person", + "name": "Johannes Kรถster" + }, + { + "@id": "https://orcid.org/0000-0002-4405-6802", + "@type": "Person", + "name": "Haris Zafeiropoulos" + }, + { + "@id": "https://orcid.org/0000-0002-5358-616X", + "@type": "Person", + "name": "Petr Holub" + }, + { + "@id": "https://orcid.org/0000-0002-5432-2748", + "@type": "Person", + "name": "Paul Brack" + }, + { + "@id": "https://orcid.org/0000-0002-5477-287X", + "@type": "Person", + "name": "Milan Markovic" + }, + { + "@id": "https://orcid.org/0000-0002-6190-122X", + "@type": "Person", + "name": "Ignacio Eguinoa" + }, + { + "@id": "https://orcid.org/0000-0002-8122-9522", + "@type": "Person", + "name": "Luiz Gadelha" + }, + { + "@id": "https://orcid.org/0000-0002-8330-4071", + "@type": "Person", + "name": "Mahnoor Zulfiqar" + }, + { + "@id": "https://orcid.org/0000-0002-9464-6640", + "@type": "Person", + "name": "Wolfgang Maier" + }, + { + "@id": "https://orcid.org/0000-0003-0617-9219", + "@type": "Person", + "name": "Jake Emerson" + }, + { + "@id": "https://orcid.org/0000-0003-1361-7301", + "@type": "Person", + "name": "Maciek Bฤ…k" + }, + { + "@id": "https://orcid.org/0000-0003-3156-2105", + "@type": "Person", + "name": "Alan R Williams" + }, + { + "@id": "https://orcid.org/0000-0003-3898-9451", + "@type": "Person", + "name": "Stelios Ninidakis" + }, + { + "@id": "https://orcid.org/0000-0003-3986-0510", + "@type": "Person", + "name": "LJ Garcia Castro" + }, + { + "@id": "https://orcid.org/0000-0003-4073-7456", + "@type": "Person", + "name": "Romain David" + }, + { + "@id": "https://orcid.org/0000-0003-4894-4660", + "@type": "Person", + "name": "Kevin Jablonka" + }, + { + "@id": "https://www.researchobject.org/ro-crate/community", + "@type": "Project", + "name": "RO-Crate Community" + }, + { + "@id": "https://w3id.org/sssom/", + "@type": [ + "WebPage", + "Standard" + ], + "alternateName": "Simple Standard for Sharing Ontological Mappings", + "name": "SSSOM", + "version": "0.15.0" + }, + { + "@id": "https://w3id.org/sssom/schema/MappingSet", + "@type": "DefinedTerm", + "url": "https://mapping-commons.github.io/sssom/MappingSet/" + }, + { + "@id": "https://www.apache.org/licenses/LICENSE-2.0", + "@type": "CreativeWork", + "name": "Apache License, Version 2.0", + "version": "2.0", + "identifier": { + "@id": "http://spdx.org/licenses/Apache-2.0" + } + }, + { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/", + "@type": "CreativeWork", + "identifier": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "name": "Creative Commons Zero v1.0 Universal", + "version": "1.0" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "identifier": { + "@id": "http://spdx.org/licenses/CC-BY-4.0" + }, + "name": "Creative Commons Attribution 4.0 International", + "version": "4.0" + }, + { + "@id": "http://spdx.org/licenses/Apache-2.0", + "@type": "PropertyValue", + "propertyID": "http://spdx.org/rdf/terms#licenseId", + "name": "spdx", + "value": "Apache-2.0" + }, + { + "@id": "http://spdx.org/licenses/CC0-1.0", + "@type": "PropertyValue", + "propertyID": "http://spdx.org/rdf/terms#licenseId", + "name": "spdx", + "value": "CC0-1.0" + }, + { + "@id": "mapping/", + "@type": "Dataset", + "name": "PROV mapping to Workflow Run Crate", + "description": "Mapping using SKOS and SSSOM", + "hasPart": [ + { + "@id": "mapping/environment.yml" + }, + { + "@id": "mapping/environment.lock.yml" + }, + { + "@id": "mapping/prov-mapping.tsv" + }, + { + "@id": "mapping/prov-mapping.yml" + }, + { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + { + "@id": "mapping/prov-mapping.ttl" + }, + { + "@id": "mapping/prov-mapping.rdf" + }, + { + "@id": "mapping/prov-mapping.json" + } + ] + } + ] +} diff --git a/tests/data/crates/valid/wrroc-paper-long-date/ro-crate-metadata.jsonld b/tests/data/crates/valid/wrroc-paper-long-date/ro-crate-metadata.jsonld new file mode 120000 index 00000000..3c4380a3 --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper-long-date/ro-crate-metadata.jsonld @@ -0,0 +1 @@ +../wrroc-paper/ro-crate-metadata.jsonld \ No newline at end of file diff --git a/tests/data/crates/valid/wrroc-paper-long-date/ro-crate-preview.html b/tests/data/crates/valid/wrroc-paper-long-date/ro-crate-preview.html new file mode 120000 index 00000000..f7e328f9 --- /dev/null +++ b/tests/data/crates/valid/wrroc-paper-long-date/ro-crate-preview.html @@ -0,0 +1 @@ +../wrroc-paper/ro-crate-preview.html \ No newline at end of file diff --git a/tests/integration/profiles/ro-crate/test_valid_ro-crate.py b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py index 5cdb01bd..ac8a2ea8 100644 --- a/tests/integration/profiles/ro-crate/test_valid_ro-crate.py +++ b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py @@ -28,3 +28,12 @@ def test_valid_roc_recommended(): Severity.RECOMMENDED, True ) + + +def test_valid_roc_required_with_long_datetime(): + """Test a valid RO-Crate.""" + do_entity_test( + ValidROC().wrroc_paper_long_date, + Severity.REQUIRED, + True + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 9b70d0dc..e4f003c9 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -24,6 +24,10 @@ class ValidROC: def wrroc_paper(self) -> Path: return Path(f"{VALID_CRATES_DATA_PATH}/wrroc-paper") + @property + def wrroc_paper_long_date(self) -> Path: + return Path(f"{VALID_CRATES_DATA_PATH}/wrroc-paper-long-date") + class InvalidFileDescriptor: From 0738db23a8f7cb4e00bf68a362d85ea944f6125c Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 4 Apr 2024 09:36:52 +0200 Subject: [PATCH 250/902] Replace deprecated types List and Dict with list and dict --- rocrate_validator/models.py | 62 +++++++++---------- .../requirements/python/__init__.py | 3 +- .../requirements/shacl/models.py | 9 ++- .../requirements/shacl/requirements.py | 7 +-- .../requirements/shacl/validator.py | 4 +- rocrate_validator/services.py | 4 +- rocrate_validator/utils.py | 6 +- tests/shared.py | 6 +- 8 files changed, 49 insertions(+), 52 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 1a496797..b943607c 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from functools import total_ordering from pathlib import Path -from typing import Callable, Dict, List, Optional, Set, Type, Union +from typing import Callable, Optional, Set, Type, Union from rdflib import Graph @@ -100,7 +100,7 @@ def __init__(self): raise NotImplementedError(f"{type(self)} can't be instantianted") @staticmethod - def all() -> List[RequirementLevel]: + def all() -> list[RequirementLevel]: return [level for name, level in inspect.getmembers(LevelCollection) if not inspect.isroutine(level) and not inspect.isdatadescriptor(level) and not name.startswith('__')] @@ -112,7 +112,7 @@ def get(name: str) -> RequirementLevel: class Profile: def __init__(self, name: str, path: Path = None, - requirements: Optional[List[Requirement]] = None, + requirements: Optional[list[Requirement]] = None, publicID: str = None): self._path = path self._name = name @@ -152,7 +152,7 @@ def description(self) -> str: # return requirement # return None - def load_requirements(self) -> List[Requirement]: + def load_requirements(self) -> list[Requirement]: """ Load the requirements from the profile directory """ @@ -180,25 +180,25 @@ def load_requirements(self) -> List[Requirement]: return self._requirements @property - def requirements(self) -> List[Requirement]: + def requirements(self) -> list[Requirement]: if not self._requirements: self.load_requirements() return self._requirements def get_requirements( self, severity: Severity = Severity.REQUIRED, - exact_match: bool = False) -> List[Requirement]: + exact_match: bool = False) -> list[Requirement]: return [requirement for requirement in self.requirements if (not exact_match and requirement.severity >= severity) or (exact_match and requirement.severity == severity)] # @property - # def requirements_by_severity_map(self) -> Dict[Severity, List[Requirement]]: + # def requirements_by_severity_map(self) -> dict[Severity, list[Requirement]]: # return {level.severity: self.get_requirements_by_type(level.severity) # for level in LevelCollection.all()} @property - def inherited_profiles(self) -> List[Profile]: + def inherited_profiles(self) -> list[Profile]: profiles = [ _ for _ in sorted( Profile.load_profiles(self.path.parent).values(), key=lambda x: x, reverse=True) @@ -209,7 +209,7 @@ def inherited_profiles(self) -> List[Profile]: def has_requirement(self, name: str) -> bool: return self.get_requirement(name) is not None - # def get_requirements_by_type(self, type: RequirementLevel) -> List[Requirement]: + # def get_requirements_by_type(self, type: RequirementLevel) -> list[Requirement]: # return [requirement for requirement in self.requirements if requirement.severity == type] def add_requirement(self, requirement: Requirement): @@ -265,7 +265,7 @@ def load(path: Union[str, Path], publicID: str = None) -> Profile: return profile @staticmethod - def load_profiles(profiles_path: Union[str, Path], publicID: str = None) -> Dict[str, Profile]: + def load_profiles(profiles_path: Union[str, Path], publicID: str = None) -> dict[str, Profile]: # if the path is a string, convert it to a Path if isinstance(profiles_path, str): profiles_path = Path(profiles_path) @@ -306,7 +306,7 @@ def __init__(self, self._profile = profile self._description = description self._path = path # path of code implementing the requirement - self._checks: List[RequirementCheck] = [] + self._checks: list[RequirementCheck] = [] # reference to the current validation context self._validation_context: ValidationContext = None @@ -367,10 +367,10 @@ def path(self) -> Path: return self._path @abstractmethod - def __init_checks__(self) -> List[RequirementCheck]: + def __init_checks__(self) -> list[RequirementCheck]: pass - def get_checks(self) -> List[RequirementCheck]: + def get_checks(self) -> list[RequirementCheck]: return self._checks.copy() def get_check(self, name: str) -> RequirementCheck: @@ -475,7 +475,7 @@ def __str__(self): def load(profile: Profile, requirement_level: RequirementLevel, file_path: Path, - publicID: str = None) -> List[Requirement]: + publicID: str = None) -> list[Requirement]: # initialize the set of requirements requirements = [] @@ -587,15 +587,15 @@ def ro_crate_path(self) -> Path: return self.validator.rocrate_path @property - def issues(self) -> List[CheckIssue]: + def issues(self) -> list[CheckIssue]: """Return the issues found during the check""" assert self._result, "Issues not set before the check" return self._result.get_issues_by_check(self, Severity.OPTIONAL) - def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> List[CheckIssue]: + def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: return self._result.get_issues_by_check(self, severity) - def get_issues_by_severity(self, severity: Severity = Severity.RECOMMENDED) -> List[CheckIssue]: + def get_issues_by_severity(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: return self._result.get_issues_by_check_and_severity(self, severity) def check(self) -> bool: @@ -628,7 +628,7 @@ def __hash__(self): return hash((self.requirement, self.name or "")) -def issue_types(issues: List[Type[CheckIssue]]) -> Type[RequirementCheck]: +def issue_types(issues: list[Type[CheckIssue]]) -> Type[RequirementCheck]: def class_decorator(cls): cls.issue_types = issues return cls @@ -710,7 +710,7 @@ def code(self) -> int: class ValidationResult: - def __init__(self, rocrate_path: Path, validation_settings: Dict = None): + def __init__(self, rocrate_path: Path, validation_settings: dict = None): # reference to the ro-crate path self._rocrate_path = rocrate_path # reference to the validation settings @@ -720,7 +720,7 @@ def __init__(self, rocrate_path: Path, validation_settings: Dict = None): # keep track of the checks that have been performed self._checks: Set[RequirementCheck] = set() # keep track of the issues found during the validation - self._issues: List[CheckIssue] = [] + self._issues: list[CheckIssue] = [] def get_rocrate_path(self): return self._rocrate_path @@ -777,7 +777,7 @@ def add_issue(self, issue: CheckIssue): # TODO: check if the issue belongs to the current validation context self._issues.append(issue) - def add_issues(self, issues: List[CheckIssue]): + def add_issues(self, issues: list[CheckIssue]): # TODO: check if the issues belong to the current validation context self._issues.extend(issues) @@ -816,19 +816,19 @@ def add_must_not(self, message: str, check: RequirementCheck, code: int = None) self.add_check_issue(message, check, code) @property - def issues(self) -> List[CheckIssue]: + def issues(self) -> list[CheckIssue]: return self._issues - def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> List[CheckIssue]: + def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: return [issue for issue in self._issues if issue.severity >= severity] - def get_issues_by_severity(self, severity: Severity) -> List[CheckIssue]: + def get_issues_by_severity(self, severity: Severity) -> list[CheckIssue]: return [issue for issue in self._issues if issue.severity == severity] - def get_issues_by_check(self, check: RequirementCheck, severity: Severity.RECOMMENDED) -> List[CheckIssue]: + def get_issues_by_check(self, check: RequirementCheck, severity: Severity.RECOMMENDED) -> list[CheckIssue]: return [issue for issue in self.issues if issue.check == check and issue.severity.value >= severity.value] - def get_issues_by_check_and_severity(self, check: RequirementCheck, severity: Severity) -> List[CheckIssue]: + def get_issues_by_check_and_severity(self, check: RequirementCheck, severity: Severity) -> list[CheckIssue]: return [issue for issue in self.issues if issue.check == check and issue.severity == severity] def has_issues(self, severity: Severity = Severity.RECOMMENDED) -> bool: @@ -894,7 +894,7 @@ def __init__(self, self._ontologies_graph = None @property - def validation_settings(self) -> Dict[str, Union[str, Path, bool, int]]: + def validation_settings(self) -> dict[str, Union[str, Path, bool, int]]: return self._validation_settings @property @@ -970,7 +970,7 @@ def get_graphs_of_shapes(self, refresh: bool = False): return self._shapes_graphs @property - def shapes_graphs(self) -> Dict[str, Graph]: + def shapes_graphs(self) -> dict[str, Graph]: return self.get_graphs_of_shapes() def get_graph_of_shapes(self, requirement_name: str, refresh: bool = False): @@ -997,7 +997,7 @@ def get_ontologies_graph(self, refresh: bool = False): def ontologies_graph(self) -> Graph: return self.get_ontologies_graph() - def validate_requirements(self, requirements: List[Requirement]) -> ValidationResult: + def validate_requirements(self, requirements: list[Requirement]) -> ValidationResult: # check if requirement is an instance of Requirement assert all(isinstance(requirement, Requirement) for requirement in requirements), \ "Invalid requirement type" @@ -1007,7 +1007,7 @@ def validate_requirements(self, requirements: List[Requirement]) -> ValidationRe def validate(self) -> ValidationResult: return self.__do_validate__() - def __do_validate__(self, requirements: List[Requirement] = None) -> ValidationResult: + def __do_validate__(self, requirements: list[Requirement] = None) -> ValidationResult: # initialize the validation result validation_result = ValidationResult( @@ -1061,7 +1061,7 @@ def result(self) -> ValidationResult: return self._result @property - def settings(self) -> Dict: + def settings(self) -> dict: return self.validator.validation_settings @property diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index 48aded29..00bbf219 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -1,7 +1,6 @@ import inspect import logging from pathlib import Path -from typing import List from ...models import Profile, Requirement, RequirementCheck, RequirementLevel from ...utils import get_classes_from_file @@ -42,7 +41,7 @@ def __init_checks__(self): @classmethod def load(cls, profile: Profile, requirement_level: RequirementLevel, file_path: Path): # instantiate a list to store the requirements - requirements: List[Requirement] = [] + requirements: list[Requirement] = [] # get the classes from the file classes = get_classes_from_file(file_path, filter_class=RequirementCheck) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 7f08d75e..19106e0b 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -3,7 +3,7 @@ import json import logging from pathlib import Path -from typing import Dict, List, Union +from typing import Union from rdflib import RDF, Graph, Namespace, URIRef from rdflib.term import Node @@ -149,7 +149,7 @@ def properties(self): """Return the properties of the shape""" return self._properties.copy() - def get_properties(self) -> List[ShapeProperty]: + def get_properties(self) -> list[ShapeProperty]: """Return the properties of the shape""" return self._properties.copy() @@ -175,7 +175,7 @@ def __hash__(self): return hash(self._node) @classmethod - def load(cls, shapes_path: Union[str, Path], publicID: str = None) -> Dict[str, Shape]: + def load(cls, shapes_path: Union[str, Path], publicID: str = None) -> dict[str, Shape]: """ Load the shapes from the graph """ @@ -188,7 +188,7 @@ def load(cls, shapes_path: Union[str, Path], publicID: str = None) -> Dict[str, shapes_nodes = shapes_graph.triples((None, RDF.type, shapeNode)) logger.debug("Shapes nodes: %s" % shapes_nodes) # create a shape object for each shape node - shapes: Dict[str, Shape] = {} + shapes: dict[str, Shape] = {} for shape_node, _, _ in shapes_nodes: logger.debug(f"Processing Shape Node: {shape_node}") shape = Shape(shape_node, shapes_graph) @@ -225,7 +225,6 @@ def __init__(self, shape_node: Node, graph: Graph) -> None: # if logger.isEnabledFor(logging.DEBUG): # logger.exception(e) # raise e - pass @property def node(self) -> Node: diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index 2c6f77a7..57bb572f 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -1,6 +1,5 @@ import logging from pathlib import Path -from typing import Dict, List from ...models import Profile, Requirement, RequirementCheck, RequirementLevel from .checks import SHACLCheck @@ -27,7 +26,7 @@ def __init__(self, # assign check IDs self.__reorder_checks__() - def __init_checks__(self) -> List[RequirementCheck]: + def __init_checks__(self) -> list[RequirementCheck]: # assign a check to each property of the shape checks = [] for prop in self._shape.get_properties(): @@ -48,8 +47,8 @@ def shape(self) -> Shape: @staticmethod def load(profile: Profile, requirement_level: RequirementLevel, - file_path: Path, publicID: str = None) -> List[Requirement]: - shapes: Dict[str, Shape] = Shape.load(file_path, publicID=publicID) + file_path: Path, publicID: str = None) -> list[Requirement]: + shapes: dict[str, Shape] = Shape.load(file_path, publicID=publicID) logger.debug("Loaded %s shapes: %s", len(shapes), shapes) requirements = [] for shape in shapes.values(): diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index d8480065..17ed5315 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -3,7 +3,7 @@ import json import logging import os -from typing import List, Optional, Union +from typing import Optional, Union import pyshacl from pyshacl.pytypes import GraphLike @@ -196,7 +196,7 @@ def conforms(self) -> bool: return self._conforms @property - def violations(self) -> List: + def violations(self) -> list: return self._violations @property diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 9670d4fb..523e83b5 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Dict, Literal, Optional, Union +from typing import Literal, Optional, Union from pyshacl.pytypes import GraphLike @@ -60,7 +60,7 @@ def validate( return result -def get_profiles(profiles_path: str = "./profiles", publicID: str = None) -> Dict[str, Profile]: +def get_profiles(profiles_path: str = "./profiles", publicID: str = None) -> dict[str, Profile]: """ Load the profiles from the given path """ diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 857b9919..86ffc59c 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -5,7 +5,7 @@ import sys from importlib import import_module from pathlib import Path -from typing import List, Optional, Type +from typing import Optional, Type import toml from rdflib import Graph @@ -72,7 +72,7 @@ def get_format_extension(serialization_format: constants.RDF_SERIALIZATION_FORMA def get_all_files( directory: str = '.', - serialization_format: constants.RDF_SERIALIZATION_FORMATS_TYPES = "turtle") -> List[str]: + serialization_format: constants.RDF_SERIALIZATION_FORMATS_TYPES = "turtle") -> list[str]: """ Get all the files in the directory matching the format. @@ -99,7 +99,7 @@ def get_all_files( def get_graphs_paths( - graphs_dir: str = CURRENT_DIR, serialization_format="turtle") -> List[str]: + graphs_dir: str = CURRENT_DIR, serialization_format="turtle") -> list[str]: """ Get the paths to all the graphs in the directory diff --git a/tests/shared.py b/tests/shared.py index b350e845..d9de33e7 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -4,7 +4,7 @@ import logging from pathlib import Path -from typing import List, Optional, Union +from typing import Optional, Union from rocrate_validator import models, services @@ -15,8 +15,8 @@ def do_entity_test( rocrate_path: Union[Path, str], requirement_severity: models.Severity, expected_validation_result: bool, - expected_triggered_requirements: Optional[List[str]] = None, - expected_triggered_issues: Optional[List[str]] = None, + expected_triggered_requirements: Optional[list[str]] = None, + expected_triggered_issues: Optional[list[str]] = None, abort_on_first: bool = True ): """ From ce9f40ff09b38d78c4f572cbfc712997e5b050d6 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 4 Apr 2024 17:31:20 +0200 Subject: [PATCH 251/902] Use Path for fixture paths --- tests/ro_crates.py | 58 +++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index e4f003c9..7b940ac1 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -1,37 +1,33 @@ -import logging -import os from pathlib import Path from tempfile import TemporaryDirectory from pytest import fixture -logging.basicConfig(level=logging.DEBUG) - -CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) -TEST_DATA_PATH = os.path.abspath(os.path.join(CURRENT_PATH, "data")) -CRATES_DATA_PATH = f"{TEST_DATA_PATH}/crates" -VALID_CRATES_DATA_PATH = f"{CRATES_DATA_PATH}/valid" -INVALID_CRATES_DATA_PATH = f"{CRATES_DATA_PATH}/invalid" +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(): +def ro_crates_path() -> Path: return CRATES_DATA_PATH class ValidROC: @property def wrroc_paper(self) -> Path: - return Path(f"{VALID_CRATES_DATA_PATH}/wrroc-paper") + return VALID_CRATES_DATA_PATH / "wrroc-paper" @property def wrroc_paper_long_date(self) -> Path: - return Path(f"{VALID_CRATES_DATA_PATH}/wrroc-paper-long-date") + return VALID_CRATES_DATA_PATH / "wrroc-paper-long-date" class InvalidFileDescriptor: - base_path = f"{INVALID_CRATES_DATA_PATH}/0_file_descriptor_format" + base_path = INVALID_CRATES_DATA_PATH / "0_file_descriptor_format" @property def missing_file_descriptor(self) -> Path: @@ -39,74 +35,74 @@ def missing_file_descriptor(self) -> Path: @property def invalid_json_format(self) -> Path: - return Path(f"{self.base_path}/invalid_json_format") + return self.base_path / "invalid_json_format" @property def invalid_jsonld_format(self) -> Path: - return Path(f"{self.base_path}/invalid_jsonld_format") + return self.base_path / "invalid_jsonld_format" class InvalidRootDataEntity: - base_path = f"{INVALID_CRATES_DATA_PATH}/2_root_data_entity_metadata" + base_path = INVALID_CRATES_DATA_PATH / "2_root_data_entity_metadata" @property def missing_root(self) -> Path: - return Path(f"{self.base_path}/missing_root_entity") + return self.base_path / "missing_root_entity" @property def invalid_root_type(self) -> Path: - return Path(f"{self.base_path}/invalid_root_type") + return self.base_path / "invalid_root_type" @property def invalid_root_date(self) -> Path: - return Path(f"{self.base_path}/invalid_root_date") + return self.base_path / "invalid_root_date" @property def missing_root_name(self) -> Path: - return Path(f"{self.base_path}/missing_root_name") + return self.base_path / "missing_root_name" @property def missing_root_description(self) -> Path: - return Path(f"{self.base_path}/missing_root_description") + return self.base_path / "missing_root_description" @property def missing_root_license(self) -> Path: - return Path(f"{self.base_path}/missing_root_license") + return self.base_path / "missing_root_license" @property def missing_root_license_name(self) -> Path: - return Path(f"{self.base_path}/missing_root_license_name") + return self.base_path / "missing_root_license_name" @property def missing_root_license_description(self) -> Path: - return Path(f"{self.base_path}/missing_root_license_description") + return self.base_path / "missing_root_license_description" class InvalidFileDescriptorEntity: - base_path = f"{INVALID_CRATES_DATA_PATH}/1_file_descriptor_metadata" + base_path = INVALID_CRATES_DATA_PATH / "1_file_descriptor_metadata" @property def missing_entity(self) -> Path: - return Path(f"{self.base_path}/missing_entity") + return self.base_path / "missing_entity" @property def invalid_entity_type(self) -> Path: - return Path(f"{self.base_path}/invalid_entity_type") + return self.base_path / "invalid_entity_type" @property def missing_entity_about(self) -> Path: - return Path(f"{self.base_path}/missing_entity_about") + return self.base_path / "missing_entity_about" @property def invalid_entity_about_type(self) -> Path: - return Path(f"{self.base_path}/invalid_entity_about_type") + return self.base_path / "invalid_entity_about_type" @property def missing_conforms_to(self) -> Path: - return Path(f"{self.base_path}/missing_conforms_to") + return self.base_path / "missing_conforms_to" @property def invalid_conforms_to(self) -> Path: - return Path(f"{self.base_path}/invalid_conforms_to") + return self.base_path / "invalid_conforms_to" From 9ca9d205c3edb0035b0f6a15264977bd750ff067 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 4 Apr 2024 17:43:36 +0200 Subject: [PATCH 252/902] Mainly typing fixes --- rocrate_validator/models.py | 78 ++++++++++--------- .../requirements/python/__init__.py | 4 +- .../requirements/shacl/models.py | 4 +- .../requirements/shacl/requirements.py | 3 +- rocrate_validator/utils.py | 10 +-- 5 files changed, 51 insertions(+), 48 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index b943607c..3628c618 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from functools import total_ordering from pathlib import Path -from typing import Callable, Optional, Set, Type, Union +from typing import Callable, Optional, Set, Union from rdflib import Graph @@ -24,6 +24,8 @@ logger = logging.getLogger(__name__) +BaseTypes = Union[str, Path, bool, int, None] + @enum.unique @total_ordering @@ -33,7 +35,7 @@ class Severity(enum.Enum): RECOMMENDED = 2 REQUIRED = 4 - def __lt__(self, other) -> bool: + def __lt__(self, other: object) -> bool: if isinstance(other, Severity): return self.value < other.value else: @@ -46,12 +48,12 @@ class RequirementLevel: name: str severity: Severity - def __eq__(self, other) -> bool: + def __eq__(self, other: object) -> bool: if not isinstance(other, RequirementLevel): return False return self.name == other.name and self.severity == other.severity - def __lt__(self, other) -> bool: + def __lt__(self, other: object) -> bool: # TODO: this ordering is not totally coherent, since for two objects a and b # with equal Severity but different names you would have # not a < b, which implies a >= b @@ -113,11 +115,11 @@ def get(name: str) -> RequirementLevel: class Profile: def __init__(self, name: str, path: Path = None, requirements: Optional[list[Requirement]] = None, - publicID: str = None): + publicID: Optional[str] = None): self._path = path self._name = name - self._description = None - self._requirements = requirements if requirements is not None else [] + self._description: Optional[str] = None + self._requirements: list[Requirement] = requirements if requirements is not None else [] self._publicID = publicID @property @@ -133,7 +135,7 @@ def readme_file_path(self) -> Path: return self.path / DEFAULT_PROFILE_README_FILE @property - def publicID(self) -> str: + def publicID(self) -> Optional[str]: return self._publicID @property @@ -152,7 +154,7 @@ def description(self) -> str: # return requirement # return None - def load_requirements(self) -> list[Requirement]: + def _load_requirements(self) -> None: """ Load the requirements from the profile directory """ @@ -283,7 +285,7 @@ def load_profiles(profiles_path: Union[str, Path], publicID: str = None) -> dict return profiles -def check(name=None): +def check(name: Optional[str] = None): def decorator(func): func.check = True func.name = name if name else func.__name__ @@ -296,12 +298,11 @@ class Requirement(ABC): def __init__(self, level: RequirementLevel, profile: Profile, - name: str = None, - description: str = None, - path: Path = None, + name: str = "", + description: Optional[str] = None, + path: Optional[Path] = None, initialize_checks: bool = True): self._order_number: int = None - self._name = name self._level = level self._profile = profile self._description = description @@ -309,7 +310,7 @@ def __init__(self, self._checks: list[RequirementCheck] = [] # reference to the current validation context - self._validation_context: ValidationContext = None + self._validation_context: Optional[ValidationContext] = None if not self._name and self._path: self._name = get_requirement_name_from_file(self._path) @@ -318,7 +319,7 @@ def __init__(self, self._checks_initialized = False # initialize the checks if the flag is set if initialize_checks: - self.__init_checks__() + _ = self.__init_checks__() # assign order numbers to checks self.__reorder_checks__() # update the checks initialized flag @@ -363,7 +364,7 @@ def description(self) -> str: return self._description @property - def path(self) -> Path: + def path(self) -> Optional[Path]: return self._path @abstractmethod @@ -373,7 +374,7 @@ def __init_checks__(self) -> list[RequirementCheck]: def get_checks(self) -> list[RequirementCheck]: return self._checks.copy() - def get_check(self, name: str) -> RequirementCheck: + def get_check(self, name: str) -> Optional[RequirementCheck]: for check in self._checks: if check.name == name: return check @@ -468,16 +469,16 @@ def __repr__(self): ')' ) - def __str__(self): + def __str__(self) -> str: return self.name @staticmethod def load(profile: Profile, requirement_level: RequirementLevel, file_path: Path, - publicID: str = None) -> list[Requirement]: + publicID: Optional[str] = None) -> list[Requirement]: # initialize the set of requirements - requirements = [] + requirements: list[Requirement] = [] # if the path is a string, convert it to a Path if isinstance(file_path, str): @@ -614,7 +615,7 @@ def __do_check__(self, context: ValidationContext) -> bool: # Perform the check return self.check() - def __eq__(self, other: RequirementCheck): + def __eq__(self, other: other) -> bool: if not isinstance(other, RequirementCheck): raise ValueError(f"Cannot compare RequirementCheck with {type(other)}") return self.requirement == other.requirement and self.name == other.name @@ -624,7 +625,7 @@ def __ne__(self, other: RequirementCheck): raise ValueError(f"Cannot compare RequirementCheck with {type(other)}") return self.requirement != other.requirement or self.name != other.name - def __hash__(self): + def __hash__(self) -> int: return hash((self.requirement, self.name or "")) @@ -647,14 +648,12 @@ class CheckIssue: """ # TODO: - # 1. CheckIssue should keep track of the RequirementCheck that was broken, - # instead of the RequirementLevel; # 2. CheckIssue has the check, to it is able to determine the level and the Severity # without having it provided through an additional argument. def __init__(self, severity: Severity, + check: RequirementCheck, message: Optional[str] = None, - code: int = None, - check: RequirementCheck = None): + code: int = None): if not isinstance(severity, Severity): raise TypeError(f"CheckIssue constructed with a severity '{severity}' of type {type(severity)}") self._severity = severity @@ -663,7 +662,7 @@ def __init__(self, severity: Severity, self._check: RequirementCheck = check @property - def message(self) -> str: + def message(self) -> Optional[str]: """The message associated with the issue""" return self._message @@ -710,11 +709,12 @@ def code(self) -> int: class ValidationResult: - def __init__(self, rocrate_path: Path, validation_settings: dict = None): + def __init__(self, rocrate_path: Path, validation_settings: Optional[dict[str, BaseTypes]] = None): # reference to the ro-crate path self._rocrate_path = rocrate_path # reference to the validation settings - self._validation_settings = validation_settings + self._validation_settings: dict[str, BaseTypes] = \ + validation_settings if validation_settings is not None else {} # keep track of the requirements that have been checked self._validated_requirements: Set[Requirement] = set() # keep track of the checks that have been performed @@ -728,6 +728,8 @@ def get_rocrate_path(self): def get_validation_settings(self): return self._validation_settings + # TODO: see which of these accessors are really needed + @property def validated_requirements(self) -> Set[Requirement]: return self._validated_requirements.copy() @@ -837,7 +839,7 @@ def has_issues(self, severity: Severity = Severity.RECOMMENDED) -> bool: def passed(self, severity: Severity = Severity.RECOMMENDED) -> bool: return not any(issue.severity >= severity for issue in self._issues) - def __str__(self): + def __str__(self) -> str: return f"Validation result: {len(self._issues)} issues" def __repr__(self): @@ -871,7 +873,7 @@ def __init__(self, self.disable_profile_inheritance = disable_profile_inheritance self.ontologies_path = ontologies_path - self._validation_settings = { + self._validation_settings: dict[str, BaseTypes] = { 'advanced': advanced, 'inference': inference, 'inplace': inplace, @@ -894,7 +896,7 @@ def __init__(self, self._ontologies_graph = None @property - def validation_settings(self) -> dict[str, Union[str, Path, bool, int]]: + def validation_settings(self) -> dict[str, BaseTypes]: return self._validation_settings @property @@ -908,8 +910,8 @@ def profile_path(self): def load_data_graph(self): data_graph = Graph() logger.debug("Loading RO-Crate metadata: %s", self.rocrate_metadata_path) - data_graph.parse(self.rocrate_metadata_path, - format="json-ld", publicID=self.publicID) + _ = data_graph.parse(self.rocrate_metadata_path, + format="json-ld", publicID=self.publicID) logger.debug("RO-Crate metadata loaded: %s", data_graph) return data_graph @@ -947,9 +949,9 @@ def publicID(self) -> str: return path @classmethod - def load_graph_of_shapes(cls, requirement: Requirement, publicID: str = None) -> Graph: + def load_graph_of_shapes(cls, requirement: Requirement, publicID: Optional[str] = None) -> Graph: shapes_graph = Graph() - shapes_graph.parse(str(requirement.path), format="ttl", publicID=publicID) + _ = shapes_graph.parse(requirement.path, format="ttl", publicID=publicID) return shapes_graph def load_graphs_of_shapes(self): @@ -958,7 +960,7 @@ def load_graphs_of_shapes(self): for requirement in self._profile.requirements: if requirement.path.suffix == ".ttl": shapes_graph = Graph() - shapes_graph.parse(str(requirement.path), format="ttl", + shapes_graph.parse(requirement.path, format="ttl", publicID=self.publicID) shapes_graphs[requirement.name] = shapes_graph return shapes_graphs diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index 00bbf219..bbeb5f99 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -38,8 +38,8 @@ def __init_checks__(self): # return the checks return checks - @classmethod - def load(cls, profile: Profile, requirement_level: RequirementLevel, file_path: Path): + @staticmethod + def load(profile: Profile, requirement_level: RequirementLevel, file_path: Path) -> list[Requirement]: # instantiate a list to store the requirements requirements: list[Requirement] = [] diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 19106e0b..73e91164 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -3,7 +3,7 @@ import json import logging from pathlib import Path -from typing import Union +from typing import Optional, Union from rdflib import RDF, Graph, Namespace, URIRef from rdflib.term import Node @@ -175,7 +175,7 @@ def __hash__(self): return hash(self._node) @classmethod - def load(cls, shapes_path: Union[str, Path], publicID: str = None) -> dict[str, Shape]: + def load(cls, shapes_path: Union[str, Path], publicID: Optional[str] = None) -> dict[str, Shape]: """ Load the shapes from the graph """ diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index 57bb572f..7ed44947 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -1,5 +1,6 @@ import logging from pathlib import Path +from typing import Optional from ...models import Profile, Requirement, RequirementCheck, RequirementLevel from .checks import SHACLCheck @@ -47,7 +48,7 @@ def shape(self) -> Shape: @staticmethod def load(profile: Profile, requirement_level: RequirementLevel, - file_path: Path, publicID: str = None) -> list[Requirement]: + file_path: Path, publicID: Optional[str] = None) -> list[Requirement]: shapes: dict[str, Shape] = Shape.load(file_path, publicID=publicID) logger.debug("Loaded %s shapes: %s", len(shapes), shapes) requirements = [] diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 86ffc59c..de0c6da7 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -5,7 +5,7 @@ import sys from importlib import import_module from pathlib import Path -from typing import Optional, Type +from typing import Optional import toml from rdflib import Graph @@ -131,8 +131,8 @@ def get_full_graph( def get_classes_from_file(file_path: Path, - filter_class: Optional[Type] = None, - class_name_suffix: str = None) -> dict: + filter_class: Optional[type] = None, + class_name_suffix: Optional[str] = None) -> dict: """Get all classes in a Python file """ # ensure the file path is a Path object assert file_path, "The file path is required" @@ -148,7 +148,7 @@ def get_classes_from_file(file_path: Path, raise ValueError("The file is not a Python file") # Get the module name from the file path - module_name = os.path.basename(file_path)[:-3] + module_name = file_path.stem logger.debug("Module: %r", module_name) # Add the directory containing the file to the system path @@ -183,7 +183,7 @@ def get_requirement_name_from_file(file: Path, check_name: Optional[str] = None) return base_name -def get_requirement_class_by_name(requirement_name: str) -> Type: +def get_requirement_class_by_name(requirement_name: str) -> type: """ Dynamically load the module of the class and return the class""" From 7d6e2bcda3f341ec37f1c8171b4f104db57adc8e Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 4 Apr 2024 17:45:02 +0200 Subject: [PATCH 253/902] Simplify comparison implementation with @total_ordering --- rocrate_validator/models.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 3628c618..7b4de128 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -112,6 +112,7 @@ def get(name: str) -> RequirementLevel: return getattr(LevelCollection, name.upper()) +@total_ordering class Profile: def __init__(self, name: str, path: Path = None, requirements: Optional[list[Requirement]] = None, @@ -179,12 +180,11 @@ def _load_requirements(self) -> None: self.add_requirement(requirement) logger.debug("Profile %s loaded %s requiremens: %s", self.name, len(self._requirements), self._requirements) - return self._requirements @property def requirements(self) -> list[Requirement]: if not self._requirements: - self.load_requirements() + self._load_requirements() return self._requirements def get_requirements( @@ -208,8 +208,8 @@ def inherited_profiles(self) -> list[Profile]: logger.debug("Inherited profiles: %s", profiles) return profiles - def has_requirement(self, name: str) -> bool: - return self.get_requirement(name) is not None + # def has_requirement(self, name: str) -> bool: + # return self.get_requirement(name) is not None # def get_requirements_by_type(self, type: RequirementLevel) -> list[Requirement]: # return [requirement for requirement in self.requirements if requirement.severity == type] @@ -223,24 +223,15 @@ def remove_requirement(self, requirement: Requirement): def validate(self, rocrate_path: Path) -> ValidationResult: raise NotImplementedError() - def __eq__(self, other) -> bool: + def __eq__(self, other: object) -> bool: return isinstance(other, Profile) \ and self.name == other.name \ and self.path == other.path \ and self.requirements == other.requirements - def __ne__(self, other) -> bool: - return not self.__eq__(other) - - def __lt__(self, other) -> bool: + def __lt__(self, other: object) -> bool: return self.name < other.name - def __le__(self, other) -> bool: - return self.name <= other.name - - def __gt__(self, other) -> bool: - return self.name > other.name - def __hash__(self) -> int: return hash((self.name, self.path, self.requirements)) @@ -440,20 +431,20 @@ def validation_settings(self): raise OutOfValidationContext("Validation context has not been initialized") return self._validation_context.settings - def __eq__(self, other) -> bool: + def __eq__(self, other: object) -> bool: if not isinstance(other, Requirement): raise TypeError(f"Cannot compare {type(self)} with {type(other)}") return self.name == other.name \ and self.severity == other.severity and self.description == other.description \ and self.path == other.path - def __ne__(self, other) -> bool: + def __ne__(self, other: object) -> bool: return not self.__eq__(other) def __hash__(self): return hash((self.name, self.severity, self.description, self.path)) - def __lt__(self, other) -> bool: + def __lt__(self, other: object) -> bool: if not isinstance(other, Requirement): raise ValueError(f"Cannot compare Requirement with {type(other)}") return self.severity >= other.severity or self.name >= other.name From 9f612b9cdebc8f024f8ea6194359593ea7ef5c4d Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 4 Apr 2024 17:46:33 +0200 Subject: [PATCH 254/902] Try to set Requirement name in constructor --- rocrate_validator/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 7b4de128..ab9d56f0 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -303,8 +303,10 @@ def __init__(self, # reference to the current validation context self._validation_context: Optional[ValidationContext] = None - if not self._name and self._path: - self._name = get_requirement_name_from_file(self._path) + if not name and path: + self._name = get_requirement_name_from_file(path) + else: + self._name = name # set flag to indicate if the checks have been initialized self._checks_initialized = False @@ -326,8 +328,6 @@ def identifier(self) -> str: @property def name(self) -> str: - if not self._name and self._path: - return get_requirement_name_from_file(self._path) return self._name @property From bceddcb2a5e3ca96bd86d37c4728d535cb41bba6 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 4 Apr 2024 17:47:30 +0200 Subject: [PATCH 255/902] set order_number through setter --- rocrate_validator/models.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index ab9d56f0..cfd57dde 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -373,7 +373,7 @@ def get_check(self, name: str) -> Optional[RequirementCheck]: def __reorder_checks__(self) -> None: for i, check in enumerate(self._checks): - check._order_number = i + 1 + check.order_number = i + 1 def __do_validate__(self, context: ValidationContext) -> bool: """ @@ -506,7 +506,7 @@ def __init__(self, check_function: Callable, description: str = None): self._requirement: Requirement = requirement - self._order_number: int = None + self._order_number = 0 self._name = name self._description = description self._check_function = check_function @@ -521,6 +521,12 @@ def __init__(self, def order_number(self) -> int: return self._order_number + @order_number.setter + def order_number(self, value: int) -> None: + if value < 0: + raise ValueError("order_number can't be < 0") + self._order_number = value + @property def identifier(self) -> str: return f"{self.requirement.identifier}.{self.order_number}" From 96963aca269f525527febb24c649e927af312d5f Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 4 Apr 2024 17:48:13 +0200 Subject: [PATCH 256/902] Refactor shacl validator contruction --- .../requirements/shacl/validator.py | 112 +++++++----------- 1 file changed, 46 insertions(+), 66 deletions(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 17ed5315..e5e6504b 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -2,7 +2,7 @@ import json import logging -import os +from pathlib import Path from typing import Optional, Union import pyshacl @@ -42,41 +42,26 @@ def __init__(self, result: ValidationResult, violation_node: Node, graph: Graph) self.violation_graph = violation_graph # serialize the graph in json-ld - violation_json = violation_graph.serialize(format="json-ld") - violation_obj = json.loads(violation_json) - self.violation_json = violation_obj[0] + violation_obj = json.loads(violation_graph.serialize(format="json-ld")) + self._violation_json = violation_obj[0] + + # initialize the parent class + super().__init__(severity=self._get_result_severity(), + check=result.validator.check, + message=self._get_result_message(result.validator.check.ro_crate_path)) # get the source shape shapes = list(graph.triples( (violation_node, URIRef(f"{SHACL_NS}sourceShape"), None))) self.source_shape_node = shapes[0][2] - # initialize the parent class - super().__init__(severity=self.resultSeverity, - message=self.resultMessage) - - @property - def result(self): - return self._result - - @property - def validator(self): - return self.result.validator - - @property - def check(self): - return self.result.validator.check - - @property - def node(self) -> Node: - return self._violation_node - @property - def graph(self) -> Graph: - return self._graph + def _get_result_message(self, ro_crate_path: Union[Path, str]) -> str: + return self._make_uris_relative( + self._violation_json[f'{SHACL_NS}resultMessage'][0]['@value'], + ro_crate_path) - @property - def resultSeverity(self) -> Severity: - shacl_severity = self.violation_json[f'{SHACL_NS}resultSeverity'][0]['@id'] + def _get_result_severity(self) -> Severity: + shacl_severity = self._violation_json[f'{SHACL_NS}resultSeverity'][0]['@id'] # we need to map the SHACL severity term to our Severity enum values if 'http://www.w3.org/ns/shacl#Violation' == shacl_severity: return Severity.REQUIRED @@ -87,34 +72,32 @@ def resultSeverity(self) -> Severity: else: raise RuntimeError(f"Unrecognized SHACL severity term {shacl_severity}") + @property + def node(self) -> Node: + return self._violation_node + + @property + def graph(self) -> Graph: + return self._graph + @property def focusNode(self): - return self.violation_json[f'{SHACL_NS}focusNode'][0]['@id'] + return self._violation_json[f'{SHACL_NS}focusNode'][0]['@id'] @property def resultPath(self): - return self.violation_json[f'{SHACL_NS}resultPath'][0]['@id'] + return self._violation_json[f'{SHACL_NS}resultPath'][0]['@id'] @property def value(self): - value = self.violation_json.get(f'{SHACL_NS}value', None) + value = self._violation_json.get(f'{SHACL_NS}value', None) if not value: return None return value[0]['@id'] - def make_uris_relative(self, text: str): - # globally replace the string "file://" with "./ - return text.replace(f'file://{self.check.ro_crate_path}', '.') - - @property - def resultMessage(self): - return self.make_uris_relative( - self.violation_json[f'{SHACL_NS}resultMessage'][0]['@value'] - ) - @property def sourceConstraintComponent(self): - return self.violation_json[f'{SHACL_NS}sourceConstraintComponent'][0]['@id'] + return self._violation_json[f'{SHACL_NS}sourceConstraintComponent'][0]['@id'] @property def sourceShape(self) -> ViolationShape: @@ -126,17 +109,14 @@ def sourceShape(self) -> ViolationShape: logger.exception(e) return None - @property - def message(self): - return self.resultMessage - @property def description(self): return self.sourceShape.description - @property - def severity(self): - return Severity.REQUIRED + @staticmethod + def _make_uris_relative(text: str, ro_crate_path: Union[Path, str]) -> str: + # globally replace the string "file://" with "./ + return text.replace(f'file://{ro_crate_path}', '.') class ValidationResult: @@ -203,22 +183,22 @@ def violations(self) -> list: def text(self) -> str: return self._text - @staticmethod - def from_serialized_results_graph(file_path: str, format: str = 'turtle'): - # check the input - assert format in ['turtle', 'n3', 'nt', - 'xml', 'rdf', 'json-ld'], "Invalid format" - assert file_path, "Invalid file path" - assert os.path.exists(file_path), "File does not exist" - # Load the graph - logger.debug("Loading graph from file: %s" % file_path) - g = Graph() - g.parse(file_path, format=format) - logger.debug("Graph loaded from file: %s" % file_path) - - # return the validation result - assert False, "missing Validator argument to constructor call" - return ValidationResult(g) + # @staticmethod + # def from_serialized_results_graph(file_path: str, format: str = 'turtle'): + # # check the input + # assert format in ['turtle', 'n3', 'nt', + # 'xml', 'rdf', 'json-ld'], "Invalid format" + # assert file_path, "Invalid file path" + # assert os.path.exists(file_path), "File does not exist" + # # Load the graph + # logger.debug("Loading graph from file: %s" % file_path) + # g = Graph() + # _ = g.parse(file_path, format=format) + # logger.debug("Graph loaded from file: %s" % file_path) + + # # return the validation result + # assert False, "missing Validator argument to constructor call" + # return ValidationResult(g) class Validator: From f0b1823d488babf988b1574f9573984271b19544 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 4 Apr 2024 17:48:41 +0200 Subject: [PATCH 257/902] Simplify validator code --- rocrate_validator/models.py | 116 ++++++++++++++---------------------- tests/shared.py | 2 +- 2 files changed, 46 insertions(+), 72 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index cfd57dde..f1cd6ebc 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -409,25 +409,25 @@ def __do_validate__(self, context: ValidationContext) -> bool: @property def validator(self): - if self._validation_context is None and self._checks_initialized: + if self._validation_context is None or not self._checks_initialized: raise OutOfValidationContext("Validation context has not been initialized") return self._validation_context.validator @property def validation_result(self): - if self._validation_context is None and self._checks_initialized: + if self._validation_context is None or not self._checks_initialized: raise OutOfValidationContext("Validation context has not been initialized") return self._validation_context.result @property def validation_context(self): - if self._validation_context is None and self._checks_initialized: + if self._validation_context is None or not self._checks_initialized: raise OutOfValidationContext("Validation context has not been initialized") return self._validation_context @property def validation_settings(self): - if self._validation_context is None and self._checks_initialized: + if self._validation_context is None or not self._checks_initialized: raise OutOfValidationContext("Validation context has not been initialized") return self._validation_context.settings @@ -480,20 +480,18 @@ def load(profile: Profile, if file_path.suffix == ".py": from rocrate_validator.requirements.python import PyRequirement py_requirements = PyRequirement.load(profile, requirement_level, file_path) - logger.debug("Loaded Python requirements: %r", py_requirements) requirements.extend(py_requirements) - logger.debug("Added Requirement: %r", py_requirements) + logger.debug("Loaded Python requirements: %r", py_requirements) elif file_path.suffix == ".ttl": # from rocrate_validator.requirements.shacl.checks import SHACLCheck from rocrate_validator.requirements.shacl.requirements import \ SHACLRequirement shapes_requirements = SHACLRequirement.load(profile, requirement_level, file_path, publicID=publicID) - logger.debug("Loaded SHACL requirements: %r", shapes_requirements) requirements.extend(shapes_requirements) - logger.debug("Added Requirement: %r", shapes_requirements) + logger.debug("Loaded SHACL requirements: %r", shapes_requirements) else: - logger.warning("Requirement type not supported: %s", file_path.suffix) + logger.warning("Requirement type not supported: %s. Ignoring file %s", file_path.suffix, file_path) return requirements @@ -504,18 +502,18 @@ def __init__(self, requirement: Requirement, name: str, check_function: Callable, - description: str = None): + description: Optional[str] = None): self._requirement: Requirement = requirement self._order_number = 0 self._name = name self._description = description self._check_function = check_function # declare the reference to the validation context - self._validation_context: ValidationContext = None + self._validation_context: Optional[ValidationContext] = None # declare the reference to the validator - self._validator: Validator = None + self._validator: Optional[Validator] = None # declare the result of the check - self._result: ValidationResult = None + self._result: Optional[ValidationResult] = None @property def order_number(self) -> int: @@ -584,17 +582,19 @@ def ro_crate_path(self) -> Path: assert self.validator, "ro-crate path not set before the check" return self.validator.rocrate_path - @property - def issues(self) -> list[CheckIssue]: - """Return the issues found during the check""" - assert self._result, "Issues not set before the check" - return self._result.get_issues_by_check(self, Severity.OPTIONAL) + # TODO: delete these? + # + # @property + # def issues(self) -> list[CheckIssue]: + # """Return the issues found during the check""" + # assert self._result, "Issues not set before the check" + # return self._result.get_issues_by_check(self, Severity.OPTIONAL) - def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: - return self._result.get_issues_by_check(self, severity) + # def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: + # return self._result.get_issues_by_check(self, severity) - def get_issues_by_severity(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: - return self._result.get_issues_by_check_and_severity(self, severity) + # def get_issues_by_severity(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: + # return self._result.get_issues_by_check_and_severity(self, severity) def check(self) -> bool: return self.check_function(self) @@ -617,20 +617,20 @@ def __eq__(self, other: other) -> bool: raise ValueError(f"Cannot compare RequirementCheck with {type(other)}") return self.requirement == other.requirement and self.name == other.name - def __ne__(self, other: RequirementCheck): - if not isinstance(other, RequirementCheck): - raise ValueError(f"Cannot compare RequirementCheck with {type(other)}") - return self.requirement != other.requirement or self.name != other.name + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) def __hash__(self) -> int: return hash((self.requirement, self.name or "")) -def issue_types(issues: list[Type[CheckIssue]]) -> Type[RequirementCheck]: - def class_decorator(cls): - cls.issue_types = issues - return cls - return class_decorator +# TODO: delete this? + +# def issue_types(issues: list[Type[CheckIssue]]) -> Type[RequirementCheck]: +# def class_decorator(cls): +# cls.issue_types = issues +# return cls +# return class_decorator class CheckIssue: @@ -776,59 +776,33 @@ def add_issue(self, issue: CheckIssue): # TODO: check if the issue belongs to the current validation context self._issues.append(issue) - def add_issues(self, issues: list[CheckIssue]): - # TODO: check if the issues belong to the current validation context - self._issues.extend(issues) - - def add_check_issue(self, message: str, check: RequirementCheck, code: int = None): - c = CheckIssue(check.requirement.severity, message, code, check=check) + def add_check_issue(self, message: str, check: RequirementCheck, code: Optional[int] = None): + c = CheckIssue(check.requirement.severity, check, message, code) self._issues.append(c) - def add_error(self, message: str, check: RequirementCheck, code: int = None): - self.add_check_issue(message, check, code) - - def add_warning(self, message: str, check: RequirementCheck, code: int = None): - self.add_check_issue(message, check, code) - - def add_optional(self, message: str, check: RequirementCheck, code: int = None): - self.add_check_issue(message, check, code) - - def add_info(self, message: str, check: RequirementCheck, code: int = None): + def add_error(self, message: str, check: RequirementCheck, code: Optional[int] = None): self.add_check_issue(message, check, code) - def add_may(self, message: str, check: RequirementCheck, code: int = None): - self.add_check_issue(message, check, code) + # TODO: delete these? - def add_recommended(self, message: str, check: RequirementCheck, code: int = None): - self.add_check_issue(message, check, code) + # def add_warning(self, message: str, check: RequirementCheck, code: Optional[int] = None): + # self.add_check_issue(message, check, code) - def add_should(self, message: str, check: RequirementCheck, code: int = None): - self.add_check_issue(message, check, code) - - def add_should_not(self, message: str, check: RequirementCheck, code: int = None): - self.add_check_issue(message, check, code) - - def add_must(self, message: str, check: RequirementCheck, code: int = None): - self.add_check_issue(message, check, code) - - def add_must_not(self, message: str, check: RequirementCheck, code: int = None): - self.add_check_issue(message, check, code) + # def add_optional(self, message: str, check: RequirementCheck, code: Optional[int] = None): + # self.add_check_issue(message, check, code) @property def issues(self) -> list[CheckIssue]: return self._issues - def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: - return [issue for issue in self._issues if issue.severity >= severity] - - def get_issues_by_severity(self, severity: Severity) -> list[CheckIssue]: - return [issue for issue in self._issues if issue.severity == severity] + def get_issues(self, min_severity: Severity) -> list[CheckIssue]: + return [issue for issue in self._issues if issue.severity >= min_severity] - def get_issues_by_check(self, check: RequirementCheck, severity: Severity.RECOMMENDED) -> list[CheckIssue]: - return [issue for issue in self.issues if issue.check == check and issue.severity.value >= severity.value] + # def get_issues_by_check(self, check: RequirementCheck, severity: Severity) -> list[CheckIssue]: + # return [issue for issue in self.issues if issue.check == check and issue.severity.value >= severity.value] - def get_issues_by_check_and_severity(self, check: RequirementCheck, severity: Severity) -> list[CheckIssue]: - return [issue for issue in self.issues if issue.check == check and issue.severity == severity] + # def get_issues_by_check_and_severity(self, check: RequirementCheck, severity: Severity) -> list[CheckIssue]: + # return [issue for issue in self.issues if issue.check == check and issue.severity == severity] def has_issues(self, severity: Severity = Severity.RECOMMENDED) -> bool: return any(issue.severity >= severity for issue in self._issues) diff --git a/tests/shared.py b/tests/shared.py index d9de33e7..c6adb42f 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -65,7 +65,7 @@ def do_entity_test( f"\"{expected_triggered_requirement}\" was not found in the failed requirements" # check requirement issues - detected_issues = [issue.message for issue in result.get_issues()] + detected_issues = [issue.message for issue in result.get_issues(models.Severity.RECOMMENDED)] logger.debug("Detected issues: %s", detected_issues) logger.debug("Expected issues: %s", expected_triggered_issues) for expected_issue in expected_triggered_issues: From 31ae30deec8958876093889e7ebf3897104fdf1d Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 4 Apr 2024 18:25:59 +0200 Subject: [PATCH 258/902] refactor: remove color from models module --- rocrate_validator/cli/commands/profiles.py | 4 ++-- rocrate_validator/models.py | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index f765a356..c00d4ab2 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -25,7 +25,6 @@ def profiles(ctx, profiles_path: str = "./profiles"): """ [magenta]rocrate-validator:[/magenta] Manage profiles """ - pass @profiles.command("list") @@ -94,7 +93,8 @@ def describe_profile(ctx, table_rows = [] levels_list = set() for requirement in profile.requirements: - level_info = f"[{requirement.color}]{requirement.severity.name}[/{requirement.color}]" + color = get_severity_color(requirement.severity) + level_info = f"[{color}]{requirement.severity.name}[/{color}]" levels_list.add(level_info) table_rows.append((str(requirement.order_number), requirement.name, Markdown(requirement.description.strip()), diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index f1cd6ebc..3db6d3c0 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -338,11 +338,6 @@ def level(self) -> RequirementLevel: def severity(self) -> Severity: return self.level.severity - @property - def color(self) -> str: - from .colors import get_severity_color - return get_severity_color(self.severity) - @property def profile(self) -> Profile: return self._profile From 49244eb19ad5810be9ad4385d1b4ebd85be18ead Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Fri, 5 Apr 2024 17:40:56 +0200 Subject: [PATCH 259/902] Add some tests for the validate CLI command --- tests/test_cli.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/test_cli.py diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..071f2205 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,29 @@ + +from click.testing import CliRunner +from pytest import fixture + +from rocrate_validator.cli.main import cli +from rocrate_validator.utils import get_version +from tests.ro_crates import InvalidFileDescriptor, ValidROC + + +@fixture +def cli_runner() -> CliRunner: + return CliRunner() + + +def test_version(cli_runner: CliRunner): + result = cli_runner.invoke(cli, ["--version"]) + assert result.exit_code == 0 + assert get_version() in result.output + + +def test_validate_subcmd_valid_rocrate(cli_runner: CliRunner): + result = cli_runner.invoke(cli, ['validate', str(ValidROC().wrroc_paper_long_date)]) + assert result.exit_code == 0 + assert 'RO-Crate is valid' in result.output + + +def test_validate_subcmd_invalid_rocrate1(cli_runner: CliRunner): + result = cli_runner.invoke(cli, ['validate', str(InvalidFileDescriptor().invalid_json_format)]) + assert result.exit_code == 1 From 3eee60b36364ff7820840cdbb62b803514e3ff7c Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Fri, 5 Apr 2024 17:45:01 +0200 Subject: [PATCH 260/902] Refactor CLI for parameterized console; exit with sys.exit --- rocrate_validator/cli/__init__.py | 5 +- rocrate_validator/cli/commands/profiles.py | 4 +- rocrate_validator/cli/commands/validate.py | 12 ++++- rocrate_validator/cli/main.py | 60 +++++++++++++--------- 4 files changed, 52 insertions(+), 29 deletions(-) diff --git a/rocrate_validator/cli/__init__.py b/rocrate_validator/cli/__init__.py index 6d98b3c4..e1435c8e 100644 --- a/rocrate_validator/cli/__init__.py +++ b/rocrate_validator/cli/__init__.py @@ -1,4 +1,5 @@ -from .main import cli, click, console + from .commands import profiles, validate +from .main import cli -__all__ = ["cli", "click", "console", "profiles", "validate"] +__all__ = ["cli", "profiles", "validate"] diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index c00d4ab2..1b97efe6 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -5,7 +5,7 @@ from ... import services from ...colors import get_severity_color -from .. import cli, click, console +from ..main import cli, click # set up logging logger = logging.getLogger(__name__) @@ -33,6 +33,7 @@ def list_profiles(ctx, profiles_path: str = "./profiles"): """ List available profiles """ + console = ctx.obj['console'] profiles = services.get_profiles(profiles_path=profiles_path) # console.print("\nAvailable profiles:", style="white bold") console.print("\n", style="white bold") @@ -80,6 +81,7 @@ def describe_profile(ctx, """ Show a profile """ + console = ctx.obj['console'] # Get the profile try: profile = services.get_profile(profiles_path=profiles_path, profile_name=profile_name) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 32eea55c..c1caa3e4 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -1,14 +1,16 @@ import logging import os +import sys from pathlib import Path from typing import Optional from rich.align import Align +from rich.console import Console from ... import services from ...colors import get_severity_color from ...models import Severity, ValidationResult -from .. import cli, click, console +from ..main import cli, click # from rich.markdown import Markdown # from rich.table import Table @@ -86,6 +88,7 @@ def validate(ctx, """ [magenta]rocrate-validator:[/magenta] Validate a RO-Crate against a profile """ + console = ctx.obj['console'] # Log the input parameters for debugging logger.debug("profiles_path: %s", os.path.abspath(profiles_path)) logger.debug("profile_name: %s", profile_name) @@ -116,8 +119,11 @@ def validate(ctx, ) # Print the validation result - __print_validation_result__(result) + __print_validation_result__(console, result) + # using ctx.exit seems to raise an Exception that gets caught below, + # so we use sys.exit instead. + sys.exit(0 if result.passed() else 1) except Exception as e: console.print( f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", @@ -125,9 +131,11 @@ def validate(ctx, ) if logger.isEnabledFor(logging.DEBUG): console.print_exception() + sys.exit(2) def __print_validation_result__( + console: Console, result: ValidationResult, severity: Severity = Severity.RECOMMENDED): """ diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index 165d67eb..f5f34544 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -1,4 +1,5 @@ import logging +import sys import rich_click as click from rich.console import Console @@ -9,9 +10,7 @@ # set up logging logger = logging.getLogger(__name__) - -# Create a Rich Console instance for enhanced output -console = Console() +__all__ = ["cli", "click"] @click.group(invoke_without_command=True) @@ -29,32 +28,45 @@ help="Show the version of the rocrate-validator package", default=False ) +@click.option( + '--disable-color', + is_flag=True, + help="Disable colored console output", + default=False +) @click.pass_context -def cli(ctx, debug: bool = False, version: bool = False): - # If the version flag is set, print the version and exit - if version: - console.print( - f"[bold]rocrate-validator [cyan]{get_version()}[/cyan][/bold]") - exit(0) - # Set the log level - if debug: - configure_logging(level=logging.DEBUG) - else: - configure_logging(level=logging.WARNING) - # If no subcommand is provided, invoke the default command - if ctx.invoked_subcommand is None: - # If no subcommand is provided, invoke the default command - from .commands.validate import validate - ctx.invoke(validate) +def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool): + ctx.ensure_object(dict) + console = Console(no_color=disable_color) + # pass the console to subcommands through the click context, after configuration + ctx.obj['console'] = console - -if __name__ == "__main__": try: - cli() + # If the version flag is set, print the version and exit + if version: + console.print( + f"[bold]rocrate-validator [cyan]{get_version()}[/cyan][/bold]") + sys.exit(0) + # Set the log level + if debug: + configure_logging(level=logging.DEBUG) + else: + configure_logging(level=logging.WARNING) + # If no subcommand is provided, invoke the default command + if ctx.invoked_subcommand is None: + # If no subcommand is provided, invoke the default command + from .commands.validate import validate + ctx.invoke(validate) except Exception as e: console.print( f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", style="white") if logger.isEnabledFor(logging.DEBUG): console.print_exception() - else: - exit(1) + sys.exit(2) + + +if __name__ == "__main__": + try: + cli() + except Exception: + exit(2) From b9d49cf12f556000909142ce334502f5e250839b Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Fri, 5 Apr 2024 17:45:55 +0200 Subject: [PATCH 261/902] Uncomment required methods --- rocrate_validator/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 3db6d3c0..90bd2bff 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -585,8 +585,8 @@ def ro_crate_path(self) -> Path: # assert self._result, "Issues not set before the check" # return self._result.get_issues_by_check(self, Severity.OPTIONAL) - # def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: - # return self._result.get_issues_by_check(self, severity) + def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: + return self._result.get_issues_by_check(self, severity) # def get_issues_by_severity(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: # return self._result.get_issues_by_check_and_severity(self, severity) @@ -793,8 +793,8 @@ def issues(self) -> list[CheckIssue]: def get_issues(self, min_severity: Severity) -> list[CheckIssue]: return [issue for issue in self._issues if issue.severity >= min_severity] - # def get_issues_by_check(self, check: RequirementCheck, severity: Severity) -> list[CheckIssue]: - # return [issue for issue in self.issues if issue.check == check and issue.severity.value >= severity.value] + def get_issues_by_check(self, check: RequirementCheck, severity: Severity) -> list[CheckIssue]: + return [issue for issue in self.issues if issue.check == check and issue.severity.value >= severity.value] # def get_issues_by_check_and_severity(self, check: RequirementCheck, severity: Severity) -> list[CheckIssue]: # return [issue for issue in self.issues if issue.check == check and issue.severity == severity] From fc5b4330b1c6146fcabe4e19c4f829c67696f024 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Mon, 8 Apr 2024 11:44:11 +0200 Subject: [PATCH 262/902] Change project license to Apache 2.0 --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 3 +- 2 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md index 3630469d..39028acd 100644 --- a/README.md +++ b/README.md @@ -94,4 +94,5 @@ Contributions are welcome! Please read our [contributing guidelines](CONTRIBUTIN ## License -This project is licensed under the terms of the MIT license. See the [LICENSE](LICENSE) file for details. +This project is licensed under the terms of the Apache License 2.0. See the +[LICENSE](LICENSE) file for details. From 7bbc8a5f3f9797b219896b9483f310b84442613b Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Mon, 8 Apr 2024 12:02:20 +0200 Subject: [PATCH 263/902] :bug: Add missing property RequirementCheck.severity --- rocrate_validator/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 90bd2bff..327d3b4a 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -544,6 +544,10 @@ def requirement(self) -> Requirement: def level(self) -> RequirementLevel: return self.requirement.level + @property + def severity(self) -> Severity: + return self.requirement.level.severity + @property def check_function(self) -> Callable: return self._check_function From 31cc781d0c885dc7be4767dea7688b41c4935f86 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 8 Apr 2024 13:40:48 +0200 Subject: [PATCH 264/902] docs(license): :page_facing_up: update license badge --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 39028acd..55c556e8 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ [![PyPI version](https://badge.fury.io/py/rocrate-validator.svg)](https://badge.fury.io/py/rocrate-validator) -[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) + +[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) A Python package to validate [ROCrate](https://researchobject.github.io/ro-crate/) packages. From b26af7740a6ed15373b665ea24cb1dd94f750406 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 8 Apr 2024 13:43:50 +0200 Subject: [PATCH 265/902] docs(readme): :memo: remove CRs between badges --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 55c556e8..c63faf01 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ # `rocrate-validator` [![Build Status](https://travis-ci.com/crs4/rocrate-validator.svg?branch=main)](https://travis-ci.com/crs4/rocrate-validator) - - - [![PyPI version](https://badge.fury.io/py/rocrate-validator.svg)](https://badge.fury.io/py/rocrate-validator) - [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + + A Python package to validate [ROCrate](https://researchobject.github.io/ro-crate/) packages. From 69e562db8db2ea033d6c4c633a26f8340e0535bf Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Mon, 8 Apr 2024 16:07:18 +0200 Subject: [PATCH 266/902] Simplify Profile loading --- rocrate_validator/models.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 327d3b4a..a802f46f 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -817,6 +817,10 @@ def __repr__(self): class Validator: + """ + Can validate conformance to a single Profile (including any requirements + inherited by parent profiles). + """ def __init__(self, rocrate_path: Path, @@ -895,16 +899,11 @@ def get_data_graph(self, refresh: bool = False): def data_graph(self) -> Graph: return self.get_data_graph() - def load_profile(self): - # load profile - profile = Profile.load(self.profile_path, publicID=self.publicID) - logger.debug("Profile: %s", profile) - return profile - def get_profile(self, refresh: bool = False): # load the profile if not self._profile or refresh: - self._profile = self.load_profile() + self._profile = Profile.load(self.profile_path, publicID=self.publicID) + logger.debug("Loaded profile: %s", self._profile) return self._profile @property From 13b0c504cbedd399e8c9c582b9d1b0b13f2f50fd Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Mon, 8 Apr 2024 17:42:32 +0200 Subject: [PATCH 267/902] Remove redundant references in RequirementCheck --- .../ro-crate/must/0_file_descriptor_format.py | 1 + rocrate_validator/models.py | 37 ++++++------------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/profiles/ro-crate/must/0_file_descriptor_format.py b/profiles/ro-crate/must/0_file_descriptor_format.py index 6f2da23d..8e4dad91 100644 --- a/profiles/ro-crate/must/0_file_descriptor_format.py +++ b/profiles/ro-crate/must/0_file_descriptor_format.py @@ -43,6 +43,7 @@ class FileDescriptorJsonFormat(RequirementCheck): def check(self) -> Tuple[int, Optional[str]]: # check if the file descriptor is in the correct format try: + logger.debug("Checking validity of JSON file at %s", self.file_descriptor_path) with open(self.file_descriptor_path, "r") as file: json.load(file) return True diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index a802f46f..ac73099c 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -505,10 +505,6 @@ def __init__(self, self._check_function = check_function # declare the reference to the validation context self._validation_context: Optional[ValidationContext] = None - # declare the reference to the validator - self._validator: Optional[Validator] = None - # declare the result of the check - self._result: Optional[ValidationResult] = None @property def order_number(self) -> int: @@ -548,10 +544,6 @@ def level(self) -> RequirementLevel: def severity(self) -> Severity: return self.requirement.level.severity - @property - def check_function(self) -> Callable: - return self._check_function - @property def rocrate_path(self) -> Path: assert self.validator, "ro-crate path not set before the check" @@ -568,13 +560,13 @@ def validation_context(self) -> ValidationContext: @property def validator(self) -> Validator: - assert self._validator, "Validator not set before the check" - return self._validator + assert self._validation_context.validator, "Validator not set before the check" + return self._validation_context.validator @property def result(self) -> ValidationResult: - assert self._result, "Result not set before the check" - return self._result + assert self._validation_context.result, "Result not set before the check" + return self._validation_context.result @property def ro_crate_path(self) -> Path: @@ -586,17 +578,17 @@ def ro_crate_path(self) -> Path: # @property # def issues(self) -> list[CheckIssue]: # """Return the issues found during the check""" - # assert self._result, "Issues not set before the check" - # return self._result.get_issues_by_check(self, Severity.OPTIONAL) + # assert self.result, "Issues not set before the check" + # return self.result.get_issues_by_check(self, Severity.OPTIONAL) def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: - return self._result.get_issues_by_check(self, severity) + return self.result.get_issues_by_check(self, severity) # def get_issues_by_severity(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: - # return self._result.get_issues_by_check_and_severity(self, severity) + # return self.result.get_issues_by_check_and_severity(self, severity) def check(self) -> bool: - return self.check_function(self) + return self._check_function(self) def __do_check__(self, context: ValidationContext) -> bool: """ @@ -604,14 +596,10 @@ def __do_check__(self, context: ValidationContext) -> bool: """ # Set the validation context self._validation_context = context - # Set the validator - self._validator = context.validator - # Set the result - self._result = context.result # Perform the check return self.check() - def __eq__(self, other: other) -> bool: + def __eq__(self, other: object) -> bool: if not isinstance(other, RequirementCheck): raise ValueError(f"Cannot compare RequirementCheck with {type(other)}") return self.requirement == other.requirement and self.name == other.name @@ -899,7 +887,7 @@ def get_data_graph(self, refresh: bool = False): def data_graph(self) -> Graph: return self.get_data_graph() - def get_profile(self, refresh: bool = False): + def _lazy_load_profile(self, refresh: bool = False): # load the profile if not self._profile or refresh: self._profile = Profile.load(self.profile_path, publicID=self.publicID) @@ -908,7 +896,7 @@ def get_profile(self, refresh: bool = False): @property def profile(self) -> Profile: - return self.get_profile() + return self._lazy_load_profile() @property def publicID(self) -> str: @@ -994,7 +982,6 @@ def __do_validate__(self, requirements: list[Requirement] = None) -> ValidationR # initialize the validation context context = ValidationContext(self, validation_result) - # for profile in profiles: logger.debug("Validating profile %s", profile.name) # perform the requirements validation From 0a34c67fd08fa53d1cc9338415504d3353a24b8e Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Tue, 9 Apr 2024 11:23:14 +0200 Subject: [PATCH 268/902] Separate SHACL validation classes from the Requirement hierarchy --- rocrate_validator/models.py | 83 ++++++++++--------- .../requirements/shacl/__init__.py | 5 +- .../requirements/shacl/checks.py | 45 +++++----- .../requirements/shacl/errors.py | 6 +- .../requirements/shacl/requirements.py | 1 - .../requirements/shacl/validator.py | 34 +++----- 6 files changed, 85 insertions(+), 89 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index ac73099c..cb281eb8 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -763,12 +763,18 @@ def add_issue(self, issue: CheckIssue): # TODO: check if the issue belongs to the current validation context self._issues.append(issue) - def add_check_issue(self, message: str, check: RequirementCheck, code: Optional[int] = None): - c = CheckIssue(check.requirement.severity, check, message, code) + def add_check_issue(self, + message: str, + check: RequirementCheck, + severity: Optional[Severity] = None, + code: Optional[int] = None) -> CheckIssue: + sev_value = severity if severity is not None else check.requirement.severity + c = CheckIssue(sev_value, check, message, code) self._issues.append(c) + return c - def add_error(self, message: str, check: RequirementCheck, code: Optional[int] = None): - self.add_check_issue(message, check, code) + def add_error(self, message: str, check: RequirementCheck, code: Optional[int] = None) -> CheckIssue: + return self.add_check_issue(message, check, code) # TODO: delete these? @@ -853,8 +859,6 @@ def __init__(self, # reference to the profile self._profile = None # reference to the graph of shapes - self._shapes_graphs = {} - # reference to the graph of ontologies self._ontologies_graph = None @property @@ -905,39 +909,6 @@ def publicID(self) -> str: return f"{path}/" return path - @classmethod - def load_graph_of_shapes(cls, requirement: Requirement, publicID: Optional[str] = None) -> Graph: - shapes_graph = Graph() - _ = shapes_graph.parse(requirement.path, format="ttl", publicID=publicID) - return shapes_graph - - def load_graphs_of_shapes(self): - # load the graph of shapes - shapes_graphs = {} - for requirement in self._profile.requirements: - if requirement.path.suffix == ".ttl": - shapes_graph = Graph() - shapes_graph.parse(requirement.path, format="ttl", - publicID=self.publicID) - shapes_graphs[requirement.name] = shapes_graph - return shapes_graphs - - def get_graphs_of_shapes(self, refresh: bool = False): - # load the graph of shapes - if not self._shapes_graphs or refresh: - self._shapes_graphs = self.load_graphs_of_shapes() - return self._shapes_graphs - - @property - def shapes_graphs(self) -> dict[str, Graph]: - return self.get_graphs_of_shapes() - - def get_graph_of_shapes(self, requirement_name: str, refresh: bool = False): - # load the graph of shapes - if not self._shapes_graphs or refresh: - self._shapes_graphs = self.load_graphs_of_shapes() - return self._shapes_graphs.get(requirement_name) - def load_ontologies_graph(self): # load the graph of ontologies ontologies_graph = Graph() @@ -1003,6 +974,40 @@ def __do_validate__(self, requirements: list[Requirement] = None) -> ValidationR return validation_result + # ------------ Dead code? ------------ + # @classmethod + # def load_graph_of_shapes(cls, requirement: Requirement, publicID: Optional[str] = None) -> Graph: + # shapes_graph = Graph() + # _ = shapes_graph.parse(requirement.path, format="ttl", publicID=publicID) + # return shapes_graph + + # def load_graphs_of_shapes(self): + # # load the graph of shapes + # shapes_graphs = {} + # for requirement in self._profile.requirements: + # if requirement.path.suffix == ".ttl": + # shapes_graph = Graph() + # shapes_graph.parse(requirement.path, format="ttl", + # publicID=self.publicID) + # shapes_graphs[requirement.name] = shapes_graph + # return shapes_graphs + + # def get_graphs_of_shapes(self, refresh: bool = False): + # # load the graph of shapes + # if not self._shapes_graphs or refresh: + # self._shapes_graphs = self.load_graphs_of_shapes() + # return self._shapes_graphs + + # @property + # def shapes_graphs(self) -> dict[str, Graph]: + # return self.get_graphs_of_shapes() + + # def get_graph_of_shapes(self, requirement_name: str, refresh: bool = False): + # # load the graph of shapes + # if not self._shapes_graphs or refresh: + # self._shapes_graphs = self.load_graphs_of_shapes() + # return self._shapes_graphs.get(requirement_name) + class ValidationContext: diff --git a/rocrate_validator/requirements/shacl/__init__.py b/rocrate_validator/requirements/shacl/__init__.py index 3bf66a35..d198f817 100644 --- a/rocrate_validator/requirements/shacl/__init__.py +++ b/rocrate_validator/requirements/shacl/__init__.py @@ -1,6 +1,5 @@ from .checks import SHACLCheck -from .validator import ValidationResult -from .validator import Validator from .errors import SHACLValidationError +from .validator import SHACLValidationResult, SHACLValidator -__all__ = ["SHACLCheck", "Validator", "ValidationResult", "SHACLValidationError"] +__all__ = ["SHACLCheck", "SHACLValidator", "SHACLValidationResult", "SHACLValidationError"] diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 943d1c80..7cef0216 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -1,8 +1,8 @@ import logging -from ...models import Requirement, RequirementCheck -from .models import ShapeProperty +from rocrate_validator.models import Requirement, RequirementCheck +from rocrate_validator.requirements.shacl.models import ShapeProperty logger = logging.getLogger(__name__) @@ -28,20 +28,6 @@ def __init__(self, def shapeProperty(self) -> ShapeProperty: return self._shapeProperty - # @property - # def severity(self): - # return self.requirement.severity - - @classmethod - def get_description(cls, requirement: Requirement): - from ...models import Validator - graph_of_shapes = Validator.load_graph_of_shapes(requirement) - return cls.query_description(graph_of_shapes) - - @property - def shapes_graph(self): - return self.validator.get_graph_of_shapes(self.requirement.name) - def check(self): ontology_graph = self.validator.ontologies_graph data_graph = self.validator.data_graph @@ -50,17 +36,19 @@ def check(self): shapes_graph = self.shapeProperty.shape_property_graph \ if self.shapeProperty else self.requirement.shape.shape_graph - from .validator import Validator as SHACLValidator - shacl_validator = SHACLValidator(self, shapes_graph=shapes_graph, ont_graph=ontology_graph) + from .validator import SHACLValidator + shacl_validator = SHACLValidator(shapes_graph=shapes_graph, ont_graph=ontology_graph) result = shacl_validator.validate(data_graph=data_graph, **self.validator.validation_settings) logger.debug("Validation '%s' conforms: %s", self.name, result.conforms) if not result.conforms: logger.debug("Validation failed") logger.debug("Validation result: %s", result) - for issue in result.violations: - logger.debug("Validation issue: %s", issue.message) - self.result.add_issue(issue) + for violation in result.violations: + c = self.result.add_check_issue(message=violation.get_result_message(self.ro_crate_path), + check=self, + severity=violation.get_result_severity()) + logger.debug("Validation issue: %s", c.message) return False return True @@ -78,3 +66,18 @@ def __eq__(self, __value: object) -> bool: def __hash__(self) -> int: return super().__hash__() + (hash(self._shapeProperty) if self._shapeProperty else 0) + + # ------------ Dead code? ------------ + # @property + # def severity(self): + # return self.requirement.severity + + # @classmethod + # def get_description(cls, requirement: Requirement): + # from ...models import Validator + # graph_of_shapes = Validator.load_graph_of_shapes(requirement) + # return cls.query_description(graph_of_shapes) + + # @property + # def shapes_graph(self): + # return self.validator.get_graph_of_shapes(self.requirement.name) diff --git a/rocrate_validator/requirements/shacl/errors.py b/rocrate_validator/requirements/shacl/errors.py index 085b42cd..ebcf708f 100644 --- a/rocrate_validator/requirements/shacl/errors.py +++ b/rocrate_validator/requirements/shacl/errors.py @@ -1,12 +1,12 @@ from ...errors import ValidationError -from .validator import ValidationResult +from .validator import SHACLValidationResult class SHACLValidationError(ValidationError): def __init__( self, - result: ValidationResult = None, + result: SHACLValidationResult = None, message: str = "Document does not conform to SHACL shapes.", path: str = ".", code: int = 500, @@ -15,7 +15,7 @@ def __init__( self._result = result @property - def result(self) -> ValidationResult: + def result(self) -> SHACLValidationResult: return self._result def __repr__(self): diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index 7ed44947..40cc84e9 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -39,7 +39,6 @@ def __init_checks__(self) -> list[RequirementCheck]: # if no property checks, add a generic one if len(checks) == 0: checks.append(SHACLCheck(self)) - # return checks return checks @property diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index e5e6504b..eb04e681 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -10,19 +10,19 @@ from rdflib import Graph from rdflib.term import Node, URIRef +from rocrate_validator.models import Severity + from ...constants import (RDF_SERIALIZATION_FORMATS, RDF_SERIALIZATION_FORMATS_TYPES, SHACL_NS, VALID_INFERENCE_OPTIONS, VALID_INFERENCE_OPTIONS_TYPES) -from ...models import CheckIssue, Severity -from ...requirements.shacl.models import ViolationShape -from .checks import SHACLCheck +from .models import ViolationShape # set up logging logger = logging.getLogger(__name__) -class Violation(CheckIssue): +class SHACLViolation: def __init__(self, result: ValidationResult, violation_node: Node, graph: Graph) -> None: # check the input @@ -45,22 +45,17 @@ def __init__(self, result: ValidationResult, violation_node: Node, graph: Graph) violation_obj = json.loads(violation_graph.serialize(format="json-ld")) self._violation_json = violation_obj[0] - # initialize the parent class - super().__init__(severity=self._get_result_severity(), - check=result.validator.check, - message=self._get_result_message(result.validator.check.ro_crate_path)) - # get the source shape shapes = list(graph.triples( (violation_node, URIRef(f"{SHACL_NS}sourceShape"), None))) self.source_shape_node = shapes[0][2] - def _get_result_message(self, ro_crate_path: Union[Path, str]) -> str: + def get_result_message(self, ro_crate_path: Union[Path, str]) -> str: return self._make_uris_relative( self._violation_json[f'{SHACL_NS}resultMessage'][0]['@value'], ro_crate_path) - def _get_result_severity(self) -> Severity: + def get_result_severity(self) -> Severity: shacl_severity = self._violation_json[f'{SHACL_NS}resultSeverity'][0]['@id'] # we need to map the SHACL severity term to our Severity enum values if 'http://www.w3.org/ns/shacl#Violation' == shacl_severity: @@ -119,7 +114,7 @@ def _make_uris_relative(text: str, ro_crate_path: Union[Path, str]) -> str: return text.replace(f'file://{ro_crate_path}', '.') -class ValidationResult: +class SHACLValidationResult: def __init__(self, validator: Validator, results_graph: Graph, conforms: Optional[bool] = None, results_text: str = None) -> None: @@ -162,7 +157,7 @@ def _parse_results_graph(self, results_graph: Graph): violations = [] for r in query_results: violation_node = r[0] - violation = Violation(self, violation_node, results_graph) + violation = SHACLViolation(self, violation_node, results_graph) violations.append(violation) return violations @@ -183,6 +178,7 @@ def violations(self) -> list: def text(self) -> str: return self._text + # ------------ Dead code? ------------ # @staticmethod # def from_serialized_results_graph(file_path: str, format: str = 'turtle'): # # check the input @@ -201,11 +197,10 @@ def text(self) -> str: # return ValidationResult(g) -class Validator: +class SHACLValidator: def __init__( self, - check: SHACLCheck, shapes_graph: Optional[Union[GraphLike, str, bytes]], ont_graph: Optional[Union[GraphLike, str, bytes]] = None, ) -> None: @@ -222,7 +217,6 @@ def __init__( """ self._shapes_graph = shapes_graph self._ont_graph = ont_graph - self._check = check @property def shapes_graph(self) -> Optional[Union[GraphLike, str, bytes]]: @@ -232,10 +226,6 @@ def shapes_graph(self) -> Optional[Union[GraphLike, str, bytes]]: def ont_graph(self) -> Optional[Union[GraphLike, str, bytes]]: return self._ont_graph - @property - def check(self) -> SHACLCheck: - return self._check - def validate( self, data_graph: Union[GraphLike, str, bytes], @@ -249,7 +239,7 @@ def validate( serialization_output_format: Optional[RDF_SERIALIZATION_FORMATS_TYPES] = "turtle", **kwargs, - ) -> ValidationResult: + ) -> SHACLValidationResult: f""" Validate a data graph using SHACL shapes as constraints @@ -330,4 +320,4 @@ def validate( serialization_output_path, format=serialization_output_format ) # return the validation result - return ValidationResult(self, results_graph, conforms, results_text) + return SHACLValidationResult(self, results_graph, conforms, results_text) From 7f0db13691007b78b0cc76479b0ac8212d9dba29 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Tue, 9 Apr 2024 12:50:28 +0200 Subject: [PATCH 269/902] Move code around --- rocrate_validator/models.py | 79 +++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index cb281eb8..b994b81d 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -149,12 +149,6 @@ def description(self) -> str: self._description = "RO-Crate profile" return self._description - # def get_requirement(self, name: str) -> Requirement: - # for requirement in self.requirements: - # if requirement.name == name: - # return requirement - # return None - def _load_requirements(self) -> None: """ Load the requirements from the profile directory @@ -194,11 +188,6 @@ def get_requirements( if (not exact_match and requirement.severity >= severity) or (exact_match and requirement.severity == severity)] - # @property - # def requirements_by_severity_map(self) -> dict[Severity, list[Requirement]]: - # return {level.severity: self.get_requirements_by_type(level.severity) - # for level in LevelCollection.all()} - @property def inherited_profiles(self) -> list[Profile]: profiles = [ @@ -208,21 +197,12 @@ def inherited_profiles(self) -> list[Profile]: logger.debug("Inherited profiles: %s", profiles) return profiles - # def has_requirement(self, name: str) -> bool: - # return self.get_requirement(name) is not None - - # def get_requirements_by_type(self, type: RequirementLevel) -> list[Requirement]: - # return [requirement for requirement in self.requirements if requirement.severity == type] - def add_requirement(self, requirement: Requirement): self._requirements.append(requirement) def remove_requirement(self, requirement: Requirement): self._requirements.remove(requirement) - def validate(self, rocrate_path: Path) -> ValidationResult: - raise NotImplementedError() - def __eq__(self, other: object) -> bool: return isinstance(other, Profile) \ and self.name == other.name \ @@ -245,6 +225,23 @@ def __repr__(self) -> str: def __str__(self) -> str: return self.name + # def get_requirement(self, name: str) -> Requirement: + # for requirement in self.requirements: + # if requirement.name == name: + # return requirement + # return None + + # @property + # def requirements_by_severity_map(self) -> dict[Severity, list[Requirement]]: + # return {level.severity: self.get_requirements_by_type(level.severity) + # for level in LevelCollection.all()} + + # def has_requirement(self, name: str) -> bool: + # return self.get_requirement(name) is not None + + # def get_requirements_by_type(self, type: RequirementLevel) -> list[Requirement]: + # return [requirement for requirement in self.requirements if requirement.severity == type] + @staticmethod def load(path: Union[str, Path], publicID: str = None) -> Profile: # if the path is a string, convert it to a Path @@ -276,14 +273,6 @@ def load_profiles(profiles_path: Union[str, Path], publicID: str = None) -> dict return profiles -def check(name: Optional[str] = None): - def decorator(func): - func.check = True - func.name = name if name else func.__name__ - return func - return decorator - - class Requirement(ABC): def __init__(self, @@ -573,20 +562,9 @@ def ro_crate_path(self) -> Path: assert self.validator, "ro-crate path not set before the check" return self.validator.rocrate_path - # TODO: delete these? - # - # @property - # def issues(self) -> list[CheckIssue]: - # """Return the issues found during the check""" - # assert self.result, "Issues not set before the check" - # return self.result.get_issues_by_check(self, Severity.OPTIONAL) - def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: return self.result.get_issues_by_check(self, severity) - # def get_issues_by_severity(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: - # return self.result.get_issues_by_check_and_severity(self, severity) - def check(self) -> bool: return self._check_function(self) @@ -610,6 +588,17 @@ def __ne__(self, other: object) -> bool: def __hash__(self) -> int: return hash((self.requirement, self.name or "")) + # TODO: delete these? + # + # @property + # def issues(self) -> list[CheckIssue]: + # """Return the issues found during the check""" + # assert self.result, "Issues not set before the check" + # return self.result.get_issues_by_check(self, Severity.OPTIONAL) + + # def get_issues_by_severity(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: + # return self.result.get_issues_by_check_and_severity(self, severity) + # TODO: delete this? @@ -620,6 +609,18 @@ def __hash__(self) -> int: # return class_decorator +def check(name: Optional[str] = None): + """ + A decorator to mark functions as "checks" (by setting an attribute + `check=True`) and optionally annotating them with a human-legible name. + """ + def decorator(func): + func.check = True + func.name = name if name else func.__name__ + return func + return decorator + + class CheckIssue: """ Class to store an issue found during a check From 3c8a56485f73595e73dbe96eecc20b3fde2f78ff Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Tue, 9 Apr 2024 12:50:47 +0200 Subject: [PATCH 270/902] Typing and module export definitions --- rocrate_validator/requirements/shacl/checks.py | 10 +++++++--- rocrate_validator/requirements/shacl/validator.py | 3 +++ rocrate_validator/utils.py | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 7cef0216..a46b741d 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -1,21 +1,23 @@ import logging +from typing import Optional from rocrate_validator.models import Requirement, RequirementCheck from rocrate_validator.requirements.shacl.models import ShapeProperty +from .validator import SHACLValidator + logger = logging.getLogger(__name__) class SHACLCheck(RequirementCheck): - """ A SHACL check for a specific shape property """ def __init__(self, requirement: Requirement, - shapeProperty: ShapeProperty = None) -> None: + shapeProperty: Optional[ShapeProperty] = None) -> None: self._shapeProperty = shapeProperty super().__init__(requirement, shapeProperty.name @@ -36,7 +38,6 @@ def check(self): shapes_graph = self.shapeProperty.shape_property_graph \ if self.shapeProperty else self.requirement.shape.shape_graph - from .validator import SHACLValidator shacl_validator = SHACLValidator(shapes_graph=shapes_graph, ont_graph=ontology_graph) result = shacl_validator.validate(data_graph=data_graph, **self.validator.validation_settings) @@ -81,3 +82,6 @@ def __hash__(self) -> int: # @property # def shapes_graph(self): # return self.validator.get_graph_of_shapes(self.requirement.name) + + +__all__ = ["SHACLCheck"] diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index eb04e681..2d90230a 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -321,3 +321,6 @@ def validate( ) # return the validation result return SHACLValidationResult(self, results_graph, conforms, results_text) + + +__all__ = ["SHACLValidator", "SHACLValidationResult", "SHACLViolation"] diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index de0c6da7..55e02d10 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -132,7 +132,7 @@ def get_full_graph( def get_classes_from_file(file_path: Path, filter_class: Optional[type] = None, - class_name_suffix: Optional[str] = None) -> dict: + class_name_suffix: Optional[str] = None) -> dict[str, type]: """Get all classes in a Python file """ # ensure the file path is a Path object assert file_path, "The file path is required" From 4724bd6daf13ac8b98b64946b50f1d08d233119f Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Tue, 9 Apr 2024 15:44:02 +0200 Subject: [PATCH 271/902] Print out stack trace when the program is exiting abnormally --- rocrate_validator/cli/commands/validate.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index c1caa3e4..ca669ed4 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -88,7 +88,7 @@ def validate(ctx, """ [magenta]rocrate-validator:[/magenta] Validate a RO-Crate against a profile """ - console = ctx.obj['console'] + console: Console = ctx.obj['console'] # Log the input parameters for debugging logger.debug("profiles_path: %s", os.path.abspath(profiles_path)) logger.debug("profile_name: %s", profile_name) @@ -129,8 +129,11 @@ def validate(ctx, f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", style="white", ) - if logger.isEnabledFor(logging.DEBUG): - console.print_exception() + 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) From 1836f4b83c0aeaf83251c01fe1fc1faa19c12aeb Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Tue, 9 Apr 2024 15:45:39 +0200 Subject: [PATCH 272/902] Refactor to remove validation from state of Requirement* objects --- .../ro-crate/must/0_file_descriptor_format.py | 90 ++++----- rocrate_validator/cli/commands/validate.py | 2 +- rocrate_validator/models.py | 172 ++++++------------ .../requirements/shacl/checks.py | 17 +- .../requirements/shacl/validator.py | 11 +- 5 files changed, 109 insertions(+), 183 deletions(-) diff --git a/profiles/ro-crate/must/0_file_descriptor_format.py b/profiles/ro-crate/must/0_file_descriptor_format.py index 8e4dad91..2fbf0581 100644 --- a/profiles/ro-crate/must/0_file_descriptor_format.py +++ b/profiles/ro-crate/must/0_file_descriptor_format.py @@ -1,8 +1,8 @@ import json import logging -from typing import Optional, Tuple +from typing import Optional -from rocrate_validator.models import RequirementCheck, check +from rocrate_validator.models import RequirementCheck, ValidationContext, check # set up logging logger = logging.getLogger(__name__) @@ -11,26 +11,26 @@ class FileDescriptorExistence(RequirementCheck): """The file descriptor MUST be present in the RO-Crate and MUST not be empty.""" @check(name="File Description Existence") - def test_existence(self) -> bool: + def test_existence(self, context: ValidationContext) -> bool: """ Check if the file descriptor is present in the RO-Crate """ - if not self.file_descriptor_path.exists(): - self.result.add_error(f'RO-Crate "{self.file_descriptor_path}" file descriptor is not present', self) + if not context.file_descriptor_path.exists(): + context.result.add_error(f'RO-Crate "{context.file_descriptor_path}" file descriptor is not present', self) return False return True @check(name="File size check") - def test_size(self) -> bool: + def test_size(self, context: ValidationContext) -> bool: """ Check if the file descriptor is not empty """ - if not self.file_descriptor_path.exists(): - self.result.add_error( + if not context.file_descriptor_path.exists(): + context.result.add_error( f'RO-Crate "{self.file_descriptor_path}" is empty: file descriptor is not present', self) return False - if self.file_descriptor_path.stat().st_size == 0: - self.result.add_error(f'RO-Crate "{self.file_descriptor_path}" file descriptor is empty', self) + if context.file_descriptor_path.stat().st_size == 0: + context.result.add_error(f'RO-Crate "{context.file_descriptor_path}" file descriptor is empty', self) return False return True @@ -40,16 +40,16 @@ class FileDescriptorJsonFormat(RequirementCheck): The file descriptor MUST be a valid JSON file """ @check(name="Check JSON Format of the file descriptor") - def check(self) -> Tuple[int, Optional[str]]: + 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", self.file_descriptor_path) - with open(self.file_descriptor_path, "r") as file: + logger.debug("Checking validity of JSON file at %s", context.file_descriptor_path) + with open(context.file_descriptor_path, "r") as file: json.load(file) return True except Exception as e: - self.result.add_error( - f'RO-Crate "{self.file_descriptor_path}" "\ + context.result.add_error( + f'RO-Crate "{context.file_descriptor_path}" "\ "file descriptor is not in the correct format', self) if logger.isEnabledFor(logging.DEBUG): logger.exception(e) @@ -61,54 +61,54 @@ class FileDescriptorJsonLdFormat(RequirementCheck): The file descriptor MUST be a valid JSON-LD file """ - _json_dict: Optional[dict] = None + _json_dict_cache: Optional[dict] = None - @property - def json_dict(self): - if self._json_dict is None: - self._json_dict = self.get_json_dict() - return self._json_dict - - def get_json_dict(self): - try: - with open(self.file_descriptor_path, "r") as file: - return json.load(file) - except Exception as e: - self.result.add_error( - f"RO-Crate \"{self.file_descriptor_path}\" " - "file descriptor is not in the correct format", self) - if logger.isEnabledFor(logging.DEBUG): - logger.exception(e) - return {} + def get_json_dict(self, context: ValidationContext) -> dict: + if self._json_dict_cache is None or \ + self._json_dict_cache['file_descriptor_path'] != context.file_descriptor_path: + # invalid cache + try: + with open(context.file_descriptor_path, "r") as file: + self._json_dict_cache = dict( + json=json.load(file), + file_descriptor_path=context.file_descriptor_path) + except Exception as e: + context.result.add_error( + f"RO-Crate \"{context.file_descriptor_path}\" " + "file descriptor is not in the correct format", self) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return {} + return self._json_dict_cache['json'] @check(name="Check if the @context property is present in the file descriptor") - def check_context(self) -> Tuple[int, Optional[str]]: - json_dict = self.json_dict + def check_context(self, validation_context: ValidationContext) -> bool: + json_dict = self.get_json_dict(validation_context) if "@context" not in json_dict: - self.result.add_error( - f"RO-Crate \"{self.file_descriptor_path}\" " + validation_context.result.add_error( + f"RO-Crate \"{validation_context.file_descriptor_path}\" " "file descriptor does not contain a context", self) return False return True @check(name="Check if descriptor entities have the @id property") - def check_identifiers(self) -> Tuple[int, Optional[str]]: - json_dict = self.json_dict + def check_identifiers(self, context: ValidationContext) -> bool: + json_dict = self.get_json_dict(context) for entity in json_dict["@graph"]: if "@id" not in entity: - self.result.add_error( + context.result.add_error( f"Entity \"{entity.get('name', None) or entity}\" " - f"of RO-Crate \"{self.file_descriptor_path}\" " + f"of RO-Crate \"{context.file_descriptor_path}\" " "file descriptor does not contain the @id attribute", self) return False @check(name="Check if descriptor entities have the @type property") - def check_types(self) -> Tuple[int, Optional[str]]: - json_dict = self.json_dict + def check_types(self, context: ValidationContext) -> bool: + json_dict = self.get_json_dict(context) for entity in json_dict["@graph"]: if "@type" not in entity: - self.result.add_error( + context.result.add_error( f"Entity \"{entity.get('name', None) or entity}\" " - f"of RO-Crate \"{self.file_descriptor_path}\" " + f"of RO-Crate \"{context.file_descriptor_path}\" " "file descriptor does not contain the @type attribute", self) return False diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index ca669ed4..5f5aaef0 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -177,7 +177,7 @@ def __print_validation_result__( f"{' '*4}- " f"[magenta]{check.name}[/magenta]: {check.description}") console.print(f"\n{' '*6}Detected issues:", style="white bold") - for issue in check.get_issues(): + for issue in result.get_issues_by_check(check): console.print( f"{' '*6}- [[{issue_color}]Violation[/{issue_color}] of " f"[magenta]{issue.check.identifier}[/magenta]]: {issue.message}") diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index b994b81d..28a6d6f5 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -289,9 +289,6 @@ def __init__(self, self._path = path # path of code implementing the requirement self._checks: list[RequirementCheck] = [] - # reference to the current validation context - self._validation_context: Optional[ValidationContext] = None - if not name and path: self._name = get_requirement_name_from_file(path) else: @@ -366,54 +363,21 @@ def __do_validate__(self, context: ValidationContext) -> bool: logger.debug("Validating Requirement %s (level=%s) with %s checks", self.name, self.level, len(self._checks)) - # TODO: consider refactoring checks to exclusively use the context they receive as an - # argument. Fetching the context from Requirement seems akin to using a global variable - # and complicates encapsulation. - self._validation_context = context - try: - logger.debug("Running %s checks for Requirement '%s'", len(self._checks), self.name) - for check in self._checks: - try: - # TODO: if __do_check__ is internal, why are we calling it from here? - logger.debug("Running check '%s' - Desc: %s", check.name, check.description) - result = check.__do_check__(context) - logger.debug("Ran check '%s'. Got result %s", check.name, result) - except Exception as e: - self.validation_result.add_error(f"Unexpected error during check: {e}", check=check) - if logger.isEnabledFor(logging.DEBUG): - logger.exception(e) - logger.debug("Checks for Requirement '%s' completed. Checks passed? %s", - self.name, self.validation_result.passed()) - # Return the result - return self.validation_result.passed() - finally: - # Clear the validation context - self._validation_context = None - logger.debug("Clearing validation context") - - @property - def validator(self): - if self._validation_context is None or not self._checks_initialized: - raise OutOfValidationContext("Validation context has not been initialized") - return self._validation_context.validator - - @property - def validation_result(self): - if self._validation_context is None or not self._checks_initialized: - raise OutOfValidationContext("Validation context has not been initialized") - return self._validation_context.result - - @property - def validation_context(self): - if self._validation_context is None or not self._checks_initialized: - raise OutOfValidationContext("Validation context has not been initialized") - return self._validation_context - - @property - def validation_settings(self): - if self._validation_context is None or not self._checks_initialized: - raise OutOfValidationContext("Validation context has not been initialized") - return self._validation_context.settings + logger.debug("Running %s checks for Requirement '%s'", len(self._checks), self.name) + for check in self._checks: + try: + # TODO: if __do_check__ is internal, why are we calling it from here? + logger.debug("Running check '%s' - Desc: %s", check.name, check.description) + result = check.__do_check__(context) + logger.debug("Ran check '%s'. Got result %s", check.name, result) + except Exception as e: + context.result.add_error(f"Unexpected error during check: {e}", check=check) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + logger.debug("Checks for Requirement '%s' completed. Checks passed? %s", + self.name, context.result.passed()) + # Return the result + return context.result.passed() def __eq__(self, other: object) -> bool: if not isinstance(other, Requirement): @@ -492,8 +456,6 @@ def __init__(self, self._name = name self._description = description self._check_function = check_function - # declare the reference to the validation context - self._validation_context: Optional[ValidationContext] = None @property def order_number(self) -> int: @@ -533,49 +495,14 @@ def level(self) -> RequirementLevel: def severity(self) -> Severity: return self.requirement.level.severity - @property - def rocrate_path(self) -> Path: - assert self.validator, "ro-crate path not set before the check" - return self.validation_context.rocrate_path - - @property - def file_descriptor_path(self) -> Path: - return self.rocrate_path / ROCRATE_METADATA_FILE - - @property - def validation_context(self) -> ValidationContext: - assert self._validation_context, "Validation context not set before the check" - return self._validation_context - - @property - def validator(self) -> Validator: - assert self._validation_context.validator, "Validator not set before the check" - return self._validation_context.validator - - @property - def result(self) -> ValidationResult: - assert self._validation_context.result, "Result not set before the check" - return self._validation_context.result - - @property - def ro_crate_path(self) -> Path: - assert self.validator, "ro-crate path not set before the check" - return self.validator.rocrate_path - - def get_issues(self, severity: Severity = Severity.RECOMMENDED) -> list[CheckIssue]: - return self.result.get_issues_by_check(self, severity) - - def check(self) -> bool: - return self._check_function(self) + def check(self, context: ValidationContext) -> bool: + return self._check_function(self, context) def __do_check__(self, context: ValidationContext) -> bool: """ Internal method to perform the check """ - # Set the validation context - self._validation_context = context - # Perform the check - return self.check() + return self.check(context) def __eq__(self, other: object) -> bool: if not isinstance(other, RequirementCheck): @@ -637,13 +564,11 @@ class CheckIssue: # without having it provided through an additional argument. def __init__(self, severity: Severity, check: RequirementCheck, - message: Optional[str] = None, - code: int = None): + message: Optional[str] = None): if not isinstance(severity, Severity): raise TypeError(f"CheckIssue constructed with a severity '{severity}' of type {type(severity)}") self._severity = severity self._message = message - self._code = code self._check: RequirementCheck = check @property @@ -670,26 +595,27 @@ def check(self) -> RequirementCheck: """The check that generated the issue""" return self._check - @property - def code(self) -> int: - # If the code has not been set, calculate it - if not self._code: - """ - Calculate the code based on the severity, the class name and the message. - - All issues with the same severity, class name and message will have the same code. - - All issues with the same severity and class name but different message will have different codes. - - All issues with the same severity but different class name and message will have different codes. - - All issues with the same severity should start with the same number. - - All codes should be positive numbers. - """ - # Concatenate the level, class name and message into a single string - issue_string = self.level.name + self.__class__.__name__ + str(self.message) - - # Use the built-in hash function to generate a unique code for this string - # The modulo operation ensures that the code is a positive number - self._code = hash(issue_string) % ((1 << 31) - 1) - # Return the code - return self._code + # @property + # def code(self) -> int: + # breakpoint() + # # If the code has not been set, calculate it + # if not self._code: + # """ + # Calculate the code based on the severity, the class name and the message. + # - All issues with the same severity, class name and message will have the same code. + # - All issues with the same severity and class name but different message will have different codes. + # - All issues with the same severity but different class name and message will have different codes. + # - All issues with the same severity should start with the same number. + # - All codes should be positive numbers. + # """ + # # Concatenate the level, class name and message into a single string + # issue_string = self.level.name + self.__class__.__name__ + str(self.message) + # + # # Use the built-in hash function to generate a unique code for this string + # # The modulo operation ensures that the code is a positive number + # self._code = hash(issue_string) % ((1 << 31) - 1) + # # Return the code + # return self._code class ValidationResult: @@ -767,10 +693,9 @@ def add_issue(self, issue: CheckIssue): def add_check_issue(self, message: str, check: RequirementCheck, - severity: Optional[Severity] = None, - code: Optional[int] = None) -> CheckIssue: + severity: Optional[Severity] = None) -> CheckIssue: sev_value = severity if severity is not None else check.requirement.severity - c = CheckIssue(sev_value, check, message, code) + c = CheckIssue(sev_value, check, message) self._issues.append(c) return c @@ -792,8 +717,10 @@ def issues(self) -> list[CheckIssue]: def get_issues(self, min_severity: Severity) -> list[CheckIssue]: return [issue for issue in self._issues if issue.severity >= min_severity] - def get_issues_by_check(self, check: RequirementCheck, severity: Severity) -> list[CheckIssue]: - return [issue for issue in self.issues if issue.check == check and issue.severity.value >= severity.value] + def get_issues_by_check(self, + check: RequirementCheck, + min_severity: Severity = Severity.OPTIONAL) -> list[CheckIssue]: + return [issue for issue in self.issues if issue.check == check and issue.severity >= min_severity] # def get_issues_by_check_and_severity(self, check: RequirementCheck, severity: Severity) -> list[CheckIssue]: # return [issue for issue in self.issues if issue.check == check and issue.severity == severity] @@ -1030,6 +957,9 @@ def settings(self) -> dict: @property def rocrate_path(self) -> Path: - if isinstance(self.validator.rocrate_path, str): - return Path(self.validator.rocrate_path) + assert isinstance(self.validator.rocrate_path, Path) return self.validator.rocrate_path + + @property + def file_descriptor_path(self) -> Path: + return self.rocrate_path / ROCRATE_METADATA_FILE diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index a46b741d..2fe206af 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -2,7 +2,8 @@ import logging from typing import Optional -from rocrate_validator.models import Requirement, RequirementCheck +from rocrate_validator.models import (Requirement, RequirementCheck, + ValidationContext) from rocrate_validator.requirements.shacl.models import ShapeProperty from .validator import SHACLValidator @@ -30,25 +31,25 @@ def __init__(self, def shapeProperty(self) -> ShapeProperty: return self._shapeProperty - def check(self): - ontology_graph = self.validator.ontologies_graph - data_graph = self.validator.data_graph + def check(self, context: ValidationContext): + ontology_graph = context.validator.ontologies_graph + data_graph = context.validator.data_graph # constraint the shapes graph to the current property shape shapes_graph = self.shapeProperty.shape_property_graph \ if self.shapeProperty else self.requirement.shape.shape_graph shacl_validator = SHACLValidator(shapes_graph=shapes_graph, ont_graph=ontology_graph) - result = shacl_validator.validate(data_graph=data_graph, **self.validator.validation_settings) + result = shacl_validator.validate(data_graph=data_graph, **context.validator.validation_settings) logger.debug("Validation '%s' conforms: %s", self.name, result.conforms) if not result.conforms: logger.debug("Validation failed") logger.debug("Validation result: %s", result) for violation in result.violations: - c = self.result.add_check_issue(message=violation.get_result_message(self.ro_crate_path), - check=self, - severity=violation.get_result_severity()) + c = context.result.add_check_issue(message=violation.get_result_message(context.rocrate_path), + check=self, + severity=violation.get_result_severity()) logger.debug("Validation issue: %s", c.message) return False diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 2d90230a..d9646e59 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -116,7 +116,7 @@ def _make_uris_relative(text: str, ro_crate_path: Union[Path, str]) -> str: class SHACLValidationResult: - def __init__(self, validator: Validator, results_graph: Graph, + def __init__(self, results_graph: Graph, conforms: Optional[bool] = None, results_text: str = None) -> None: # validate the results graph input assert results_graph is not None, "Invalid graph" @@ -127,7 +127,6 @@ def __init__(self, validator: Validator, results_graph: Graph, # store the input properties self.results_graph = results_graph self._text = results_text - self._validator = validator # parse the results graph self._violations = self._parse_results_graph(results_graph) # initialize the conforms property @@ -162,16 +161,12 @@ def _parse_results_graph(self, results_graph: Graph): return violations - @property - def validator(self) -> Validator: - return self._validator - @property def conforms(self) -> bool: return self._conforms @property - def violations(self) -> list: + def violations(self) -> list[SHACLViolation]: return self._violations @property @@ -320,7 +315,7 @@ def validate( serialization_output_path, format=serialization_output_format ) # return the validation result - return SHACLValidationResult(self, results_graph, conforms, results_text) + return SHACLValidationResult(results_graph, conforms, results_text) __all__ = ["SHACLValidator", "SHACLValidationResult", "SHACLViolation"] From 536a12e09a12a4ffa28e65bf6879bafe8b321cfd Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Tue, 9 Apr 2024 15:46:49 +0200 Subject: [PATCH 273/902] Print requirement id as "namenumber" --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 28a6d6f5..01095318 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -310,7 +310,7 @@ def order_number(self) -> int: @property def identifier(self) -> str: - return f"{self.level.name}.{self.order_number}" + return f"{self.level.name} {self.order_number}" @property def name(self) -> str: From 520d4cff7b059c5aebbc2f8fadbf83914422e97e Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Tue, 9 Apr 2024 16:23:34 +0200 Subject: [PATCH 274/902] Remove unused OutOfValidationContext exception --- rocrate_validator/errors.py | 18 ------------------ rocrate_validator/models.py | 2 -- 2 files changed, 20 deletions(-) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 568dc3d1..5858f3f9 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -8,24 +8,6 @@ class ROCValidatorError(Exception): pass -class OutOfValidationContext(ROCValidatorError): - """Raised when a validation check is called outside of a validation context.""" - - def __init__(self, message: Optional[str] = None): - self._message = message - - @property - def message(self) -> Optional[str]: - """The error message.""" - return self._message - - def __str__(self) -> str: - return str(self._message) - - def __repr__(self): - return f"OutOfValidationContext({self._message!r})" - - class InvalidSerializationFormat(ROCValidatorError): """Raised when an invalid serialization format is provided.""" diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 01095318..265aaafd 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -20,8 +20,6 @@ VALID_INFERENCE_OPTIONS_TYPES) from rocrate_validator.utils import get_requirement_name_from_file -from .errors import OutOfValidationContext - logger = logging.getLogger(__name__) BaseTypes = Union[str, Path, bool, int, None] From 4d83db19dc479d4dddfa3233ea3455ac02976974 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Tue, 9 Apr 2024 16:24:39 +0200 Subject: [PATCH 275/902] Revise accessors to ValidationResults --- rocrate_validator/cli/commands/validate.py | 8 +- rocrate_validator/models.py | 105 +++++++-------------- 2 files changed, 37 insertions(+), 76 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 5f5aaef0..05b42e05 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -123,7 +123,7 @@ def validate(ctx, # using ctx.exit seems to raise an Exception that gets caught below, # so we use sys.exit instead. - sys.exit(0 if result.passed() else 1) + sys.exit(0 if result.passed(Severity.RECOMMENDED) else 1) except Exception as e: console.print( f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", @@ -158,7 +158,7 @@ def __print_validation_result__( console.print("\n[bold]The following requirements have not meet: [/bold]\n", style="white") - for requirement in result.failed_requirements: + for requirement in sorted(result.failed_requirements): issue_color = get_severity_color(requirement.severity) console.print( Align(f" [severity: [{issue_color}]{requirement.severity.name}[/{issue_color}], " @@ -171,13 +171,13 @@ def __print_validation_result__( console.print(f"\n{' '*4}{requirement.description}\n", style="white italic") console.print(f"{' '*4}Failed checks:\n", style="white bold") - for check in result.get_failed_checks_by_requirement(requirement): + for check in sorted(result.get_failed_checks_by_requirement(requirement)): issue_color = get_severity_color(check.level.severity) console.print( f"{' '*4}- " f"[magenta]{check.name}[/magenta]: {check.description}") console.print(f"\n{' '*6}Detected issues:", style="white bold") - for issue in result.get_issues_by_check(check): + for issue in sorted(result.get_issues_by_check(check)): console.print( f"{' '*6}- [[{issue_color}]Violation[/{issue_color}] of " f"[magenta]{issue.check.identifier}[/magenta]]: {issue.message}") diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 265aaafd..97dbcc73 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -5,10 +5,11 @@ import logging import os from abc import ABC, abstractmethod +from collections.abc import Iterable from dataclasses import dataclass from functools import total_ordering from pathlib import Path -from typing import Callable, Optional, Set, Union +from typing import Callable, Optional, Union from rdflib import Graph @@ -624,10 +625,6 @@ def __init__(self, rocrate_path: Path, validation_settings: Optional[dict[str, B # reference to the validation settings self._validation_settings: dict[str, BaseTypes] = \ validation_settings if validation_settings is not None else {} - # keep track of the requirements that have been checked - self._validated_requirements: Set[Requirement] = set() - # keep track of the checks that have been performed - self._checks: Set[RequirementCheck] = set() # keep track of the issues found during the validation self._issues: list[CheckIssue] = [] @@ -637,55 +634,29 @@ def get_rocrate_path(self): def get_validation_settings(self): return self._validation_settings - # TODO: see which of these accessors are really needed - - @property - def validated_requirements(self) -> Set[Requirement]: - return self._validated_requirements.copy() - + # --- Issues --- @property - def failed_requirements(self) -> Set[Requirement]: - return sorted(set([issue.check.requirement for issue in self._issues]), key=lambda x: x.order_number) - - @property - def passed_requirements(self) -> Set[Requirement]: - return sorted(self._validated_requirements - self.failed_requirements, key=lambda req: req.order_number) - - @property - def checks(self) -> Set[RequirementCheck]: - return sorted(set(self._checks), key=lambda x: x.order_number) + def issues(self) -> list[CheckIssue]: + return self._issues - @property - def failed_checks(self) -> Set[RequirementCheck]: - return sorted(set([issue.check for issue in self._issues]), key=lambda x: x.order_number) + def get_issues(self, min_severity: Severity) -> list[CheckIssue]: + return [issue for issue in self._issues if issue.severity >= min_severity] - @property - def passed_checks(self) -> Set[RequirementCheck]: - return sorted(self._checks - self.failed_checks, key=lambda x: x.order_number) + def get_issues_by_check(self, + check: RequirementCheck, + min_severity: Severity = Severity.OPTIONAL) -> list[CheckIssue]: + return [issue for issue in self._issues if issue.check == check and issue.severity >= min_severity] - def get_passed_checks_by_requirement(self, requirement: Requirement) -> Set[RequirementCheck]: - return sorted( - set([check for check in self.passed_checks if check.requirement == requirement]), - key=lambda x: x.order_number - ) + # def get_issues_by_check_and_severity(self, check: RequirementCheck, severity: Severity) -> list[CheckIssue]: + # return [issue for issue in self.issues if issue.check == check and issue.severity == severity] - def get_failed_checks_by_requirement(self, requirement: Requirement) -> Set[RequirementCheck]: - return sorted( - set([check for check in self.failed_checks if check.requirement == requirement]), - key=lambda x: x.order_number - ) + def has_issues(self, severity: Severity = Severity.OPTIONAL) -> bool: + return any(issue.severity >= severity for issue in self._issues) - def get_failed_checks_by_requirement_and_severity( - self, requirement: Requirement, severity: Severity) -> Set[RequirementCheck]: - return sorted( - set([check for check in self.failed_checks - if check.requirement == requirement - and check.severity == severity]), - key=lambda x: x.order_number - ) + def passed(self, severity: Severity = Severity.OPTIONAL) -> bool: + return not any(issue.severity >= severity for issue in self._issues) def add_issue(self, issue: CheckIssue): - # TODO: check if the issue belongs to the current validation context self._issues.append(issue) def add_check_issue(self, @@ -697,37 +668,27 @@ def add_check_issue(self, self._issues.append(c) return c - def add_error(self, message: str, check: RequirementCheck, code: Optional[int] = None) -> CheckIssue: - return self.add_check_issue(message, check, code) - - # TODO: delete these? - - # def add_warning(self, message: str, check: RequirementCheck, code: Optional[int] = None): - # self.add_check_issue(message, check, code) - - # def add_optional(self, message: str, check: RequirementCheck, code: Optional[int] = None): - # self.add_check_issue(message, check, code) + def add_error(self, message: str, check: RequirementCheck) -> CheckIssue: + return self.add_check_issue(message, check, Severity.REQUIRED) + # --- Requirements --- @property - def issues(self) -> list[CheckIssue]: - return self._issues - - def get_issues(self, min_severity: Severity) -> list[CheckIssue]: - return [issue for issue in self._issues if issue.severity >= min_severity] + def failed_requirements(self) -> Iterable[Requirement]: + return set(issue.check.requirement for issue in self._issues) - def get_issues_by_check(self, - check: RequirementCheck, - min_severity: Severity = Severity.OPTIONAL) -> list[CheckIssue]: - return [issue for issue in self.issues if issue.check == check and issue.severity >= min_severity] - - # def get_issues_by_check_and_severity(self, check: RequirementCheck, severity: Severity) -> list[CheckIssue]: - # return [issue for issue in self.issues if issue.check == check and issue.severity == severity] + # --- Checks --- + @property + def failed_checks(self) -> Iterable[RequirementCheck]: + return set(issue.check for issue in self._issues) - def has_issues(self, severity: Severity = Severity.RECOMMENDED) -> bool: - return any(issue.severity >= severity for issue in self._issues) + def get_failed_checks_by_requirement(self, requirement: Requirement) -> Iterable[RequirementCheck]: + return [check for check in self.failed_checks if check.requirement == requirement] - def passed(self, severity: Severity = Severity.RECOMMENDED) -> bool: - return not any(issue.severity >= severity for issue in self._issues) + def get_failed_checks_by_requirement_and_severity( + self, requirement: Requirement, severity: Severity) -> Iterable[RequirementCheck]: + return [check for check in self.failed_checks + if check.requirement == requirement + and check.severity == severity] def __str__(self) -> str: return f"Validation result: {len(self._issues)} issues" From a084be1ef062fa34e174aaeab2fc2a5184385da8 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Tue, 9 Apr 2024 16:26:21 +0200 Subject: [PATCH 276/902] Typing --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 97dbcc73..f13c7241 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -448,7 +448,7 @@ class RequirementCheck: def __init__(self, requirement: Requirement, name: str, - check_function: Callable, + check_function: Callable[[ValidationContext], bool], description: Optional[str] = None): self._requirement: Requirement = requirement self._order_number = 0 From dd86505784e16fc36eefeef9d03def0925c9c0af Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 10 Apr 2024 15:58:12 +0200 Subject: [PATCH 277/902] Typing issues --- rocrate_validator/models.py | 10 +++++----- rocrate_validator/requirements/python/__init__.py | 11 ++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index f13c7241..71f4a295 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -5,7 +5,7 @@ import logging import os from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Collection from dataclasses import dataclass from functools import total_ordering from pathlib import Path @@ -673,19 +673,19 @@ def add_error(self, message: str, check: RequirementCheck) -> CheckIssue: # --- Requirements --- @property - def failed_requirements(self) -> Iterable[Requirement]: + def failed_requirements(self) -> Collection[Requirement]: return set(issue.check.requirement for issue in self._issues) # --- Checks --- @property - def failed_checks(self) -> Iterable[RequirementCheck]: + def failed_checks(self) -> Collection[RequirementCheck]: return set(issue.check for issue in self._issues) - def get_failed_checks_by_requirement(self, requirement: Requirement) -> Iterable[RequirementCheck]: + def get_failed_checks_by_requirement(self, requirement: Requirement) -> Collection[RequirementCheck]: return [check for check in self.failed_checks if check.requirement == requirement] def get_failed_checks_by_requirement_and_severity( - self, requirement: Requirement, severity: Severity) -> Iterable[RequirementCheck]: + self, requirement: Requirement, severity: Severity) -> Collection[RequirementCheck]: return [check for check in self.failed_checks if check.requirement == requirement and check.severity == severity] diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index bbeb5f99..d2cde138 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -1,6 +1,7 @@ import inspect import logging from pathlib import Path +from typing import Optional from ...models import Profile, Requirement, RequirementCheck, RequirementLevel from ...utils import get_classes_from_file @@ -14,9 +15,9 @@ class PyRequirement(Requirement): def __init__(self, level: RequirementLevel, profile: Profile, - name: str = None, - description: str = None, - path: Path = None, + name: str = "", + description: Optional[str] = None, + path: Optional[Path] = None, requirement_check_class=None): self.requirement_check_class = requirement_check_class super().__init__(level, profile, name, description, path, initialize_checks=True) @@ -35,7 +36,7 @@ def __init_checks__(self): check = self.requirement_check_class(self, check_name, member, check_description) self._checks.append(check) logger.debug("Added check: %s %r", check_name, check) - # return the checks + return checks @staticmethod @@ -45,7 +46,7 @@ def load(profile: Profile, requirement_level: RequirementLevel, file_path: Path) # get the classes from the file classes = get_classes_from_file(file_path, filter_class=RequirementCheck) - logger.debug("Classes: %r" % classes) + logger.debug("Classes: %r", classes) # instantiate a requirement for each class for requirement_name, requirement_class in classes.items(): From f32cdf33e7216866177d328ac9ae73b7fc079e63 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 10 Apr 2024 15:59:32 +0200 Subject: [PATCH 278/902] Fix sorting issues with Requirement* classes --- rocrate_validator/models.py | 21 ++++++++++++++++++++- tests/conftest.py | 4 ++-- tests/shared.py | 10 +++++++++- tests/test_models.py | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 71f4a295..9aa136eb 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -272,6 +272,7 @@ def load_profiles(profiles_path: Union[str, Path], publicID: str = None) -> dict return profiles +@total_ordering class Requirement(ABC): def __init__(self, @@ -394,7 +395,7 @@ def __hash__(self): def __lt__(self, other: object) -> bool: if not isinstance(other, Requirement): raise ValueError(f"Cannot compare Requirement with {type(other)}") - return self.severity >= other.severity or self.name >= other.name + return (self.level, self._order_number, self.name) < (other.level, other._order_number, other.name) def __repr__(self): return ( @@ -443,6 +444,7 @@ def load(profile: Profile, return requirements +@total_ordering class RequirementCheck: def __init__(self, @@ -508,6 +510,11 @@ def __eq__(self, other: object) -> bool: raise ValueError(f"Cannot compare RequirementCheck with {type(other)}") return self.requirement == other.requirement and self.name == other.name + def __lt__(self, other: object) -> bool: + if not isinstance(other, RequirementCheck): + raise ValueError(f"Cannot compare RequirementCheck with {type(other)}") + return (self.requirement, self.name) < (other.requirement, other.name) + def __ne__(self, other: object) -> bool: return not self.__eq__(other) @@ -547,6 +554,7 @@ def decorator(func): return decorator +@total_ordering class CheckIssue: """ Class to store an issue found during a check @@ -594,6 +602,17 @@ def check(self) -> RequirementCheck: """The check that generated the issue""" return self._check + def __eq__(self, other: object) -> bool: + return isinstance(other, CheckIssue) and \ + self._check == other._check and \ + self._severity == other._severity and \ + self._message == other._message + + def __lt__(self, other: object) -> bool: + if not isinstance(other, CheckIssue): + raise TypeError(f"Cannot compare {type(self)} with {type(other)}") + return (self._check, self._severity, self._message) < (other._check, other._severity, other._message) + # @property # def code(self) -> int: # breakpoint() diff --git a/tests/conftest.py b/tests/conftest.py index c4636460..e51483cc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,11 @@ # calculate the absolute path of the rocrate-validator package # and add it to the system path +import logging import os from pytest import fixture -from rocrate_validator.config import configure_logging -import logging +from rocrate_validator.config import configure_logging # set up logging configure_logging(level=logging.DEBUG) diff --git a/tests/shared.py b/tests/shared.py index c6adb42f..383ad902 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -3,14 +3,22 @@ """ import logging +from collections.abc import Collection from pathlib import Path -from typing import Optional, Union +from typing import Optional, TypeVar, Union from rocrate_validator import models, services logger = logging.getLogger(__name__) +T = TypeVar("T") + + +def first(c: Collection[T]) -> T: + return next(iter(c)) + + def do_entity_test( rocrate_path: Union[Path, str], requirement_severity: models.Severity, diff --git a/tests/test_models.py b/tests/test_models.py index 13ed991b..a142ddbd 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,8 +1,11 @@ import pytest +from rocrate_validator import models, services from rocrate_validator.models import (LevelCollection, RequirementLevel, Severity) +from tests.ro_crates import InvalidFileDescriptor, InvalidRootDataEntity +from tests.shared import first def test_severity_ordering(): @@ -53,3 +56,37 @@ def test_level_collection(): assert 'SHOULD_NOT' in level_names assert 'RECOMMENDED' in level_names assert 'REQUIRED' in level_names + + +def test_sortability_requirements(): + result: models.ValidationResult = services.validate(InvalidRootDataEntity().invalid_root_type, + requirement_severity=Severity.OPTIONAL, + abort_on_first=False) + failed_requirements = sorted(result.failed_requirements, reverse=True) + assert len(failed_requirements) > 1 + assert failed_requirements[0] >= failed_requirements[1] + assert failed_requirements[0].level >= failed_requirements[1].level + + +def test_sortability_checks(): + result: models.ValidationResult = services.validate(InvalidFileDescriptor().invalid_json_format, + requirement_severity=Severity.OPTIONAL, + abort_on_first=False) + failed_checks = sorted(result.failed_checks, reverse=True) + assert len(failed_checks) > 1 + i_checks = iter(failed_checks) + one, two = next(i_checks), next(i_checks) + assert one >= two + assert one.requirement >= two.requirement + + +def test_sortability_issues(): + result: models.ValidationResult = services.validate(InvalidFileDescriptor().invalid_json_format, + requirement_severity=Severity.OPTIONAL, + abort_on_first=False) + issues = sorted(result.get_issues(min_severity=Severity.OPTIONAL), reverse=True) + assert len(issues) > 1 + i_issues = iter(issues) + one, two = next(i_issues), next(i_issues) + assert one >= two + assert one.check >= two.check From 79ed56b28753ffce9606b5fe435d4780452fb028 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 10 Apr 2024 16:12:51 +0200 Subject: [PATCH 279/902] Fix missing return values from some checks --- profiles/ro-crate/must/0_file_descriptor_format.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/profiles/ro-crate/must/0_file_descriptor_format.py b/profiles/ro-crate/must/0_file_descriptor_format.py index 2fbf0581..1dc36bba 100644 --- a/profiles/ro-crate/must/0_file_descriptor_format.py +++ b/profiles/ro-crate/must/0_file_descriptor_format.py @@ -101,6 +101,7 @@ def check_identifiers(self, context: ValidationContext) -> bool: f"of RO-Crate \"{context.file_descriptor_path}\" " "file descriptor does not contain the @id attribute", self) return False + return True @check(name="Check if descriptor entities have the @type property") def check_types(self, context: ValidationContext) -> bool: @@ -112,3 +113,4 @@ def check_types(self, context: ValidationContext) -> bool: f"of RO-Crate \"{context.file_descriptor_path}\" " "file descriptor does not contain the @type attribute", self) return False + return True From ce0ff9c66e8585adb1418ec155ca520e76be1e57 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 10 Apr 2024 16:14:10 +0200 Subject: [PATCH 280/902] Check function sig in @check decorator --- rocrate_validator/models.py | 10 +++++++++- rocrate_validator/requirements/python/__init__.py | 3 ++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 9aa136eb..860525e4 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -548,8 +548,16 @@ def check(name: Optional[str] = None): `check=True`) and optionally annotating them with a human-legible name. """ def decorator(func): + check_name = name if name else func.__name__ + sig = inspect.signature(func) + if len(sig.parameters) != 2: + raise RuntimeError(f"Invalid check {check_name}. Checks are expected to " + "accept two arguments but this only takes {len(sig.parameters)}") + if sig.return_annotation not in (bool, inspect.Signature.empty): + raise RuntimeError(f"Invalid check {check_name}. Checks are expected to " + "return bool but this only returns {sig.return_annotation}") func.check = True - func.name = name if name else func.__name__ + func.name = check_name return func return decorator diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index d2cde138..89c35c20 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -26,7 +26,8 @@ def __init_checks__(self): # initialize the list of checks checks = [] for name, member in inspect.getmembers(self.requirement_check_class, inspect.isfunction): - if hasattr(member, "check"): + # verify that the attribute set by the check decorator is present + if hasattr(member, "check") and member.check is True: check_name = None try: check_name = member.name.strip() From ebe2537d973d0af90e2c16e07ee42fb350928995 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 10 Apr 2024 16:15:13 +0200 Subject: [PATCH 281/902] Small typing and logging changes --- rocrate_validator/models.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 860525e4..e1e087a6 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -113,7 +113,7 @@ def get(name: str) -> RequirementLevel: @total_ordering class Profile: - def __init__(self, name: str, path: Path = None, + def __init__(self, name: str, path: Path, requirements: Optional[list[Requirement]] = None, publicID: Optional[str] = None): self._path = path @@ -851,12 +851,7 @@ def validate_requirements(self, requirements: list[Requirement]) -> ValidationRe def validate(self) -> ValidationResult: return self.__do_validate__() - def __do_validate__(self, requirements: list[Requirement] = None) -> ValidationResult: - - # initialize the validation result - validation_result = ValidationResult( - rocrate_path=self.rocrate_path, validation_settings=self.validation_settings) - + def __do_validate__(self, requirements: Optional[list[Requirement]] = None) -> ValidationResult: # list of profiles to validate profiles = [self.profile] logger.debug("Disable profile inheritance: %s", self.disable_profile_inheritance) @@ -865,6 +860,8 @@ def __do_validate__(self, requirements: list[Requirement] = None) -> ValidationR logger.debug("Profiles to validate: %s", profiles) # initialize the validation context + validation_result = ValidationResult( + rocrate_path=self.rocrate_path, validation_settings=self.validation_settings) context = ValidationContext(self, validation_result) for profile in profiles: @@ -876,17 +873,17 @@ def __do_validate__(self, requirements: list[Requirement] = None) -> ValidationR logger.debug("For profile %s, validating these %s requirements: %s", profile.name, len(requirements), requirements) for requirement in requirements: - result = requirement.__do_validate__(context) - logger.debug("Validation Requirement result: %s", result) - if result: - logger.debug("Validation Requirement passed: %s", requirement) + passed = requirement.__do_validate__(context) + logger.debug("Number of issues: %s", len(context.result.issues)) + if passed: + logger.debug("Validation Requirement passed") else: logger.debug(f"Validation Requirement {requirement} failed ") - if self.validation_settings.get("abort_on_first"): - logger.debug("Aborting on first failure") + if self.validation_settings.get("abort_on_first") is True: + logger.debug("Aborting on first requirement failure") return validation_result - return validation_result + return context.result # ------------ Dead code? ------------ # @classmethod From 320cbeec9a9b6d0d140d5ff0ae2859852ecfbc60 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 10 Apr 2024 22:02:32 +0200 Subject: [PATCH 282/902] Walk/load profile files with pathlib --- rocrate_validator/models.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index e1e087a6..63e09313 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -3,7 +3,6 @@ import enum import inspect import logging -import os from abc import ABC, abstractmethod from collections.abc import Collection from dataclasses import dataclass @@ -152,25 +151,25 @@ def _load_requirements(self) -> None: """ Load the requirements from the profile directory """ + def ok_file(p: Path) -> bool: + return p.is_file() \ + and p.suffix in PROFILE_FILE_EXTENSIONS \ + and not p.name.startswith('.') \ + and not p.name.startswith('_') + + files = sorted((p for p in self.path.rglob('*.*') if ok_file(p)), + key=lambda x: (not x.suffix == '.py', x)) + req_id = 0 self._requirements = [] - for root, dirs, files in os.walk(self.path): - dirs[:] = [d for d in dirs - if not d.startswith('.') and not d.startswith('_')] - requirement_root = Path(root) - requirement_level = requirement_root.name - # Filter out files that start with a dot or underscore - files = [_ for _ in files if not _.startswith('.') - and not _.startswith('_') - and Path(_).suffix in PROFILE_FILE_EXTENSIONS] - for file in sorted(files, key=lambda x: (not x.endswith('.py'), x)): - requirement_path = requirement_root / file - for requirement in Requirement.load( - self, LevelCollection.get(requirement_level), - requirement_path, publicID=self.publicID): - req_id += 1 - requirement._order_number = req_id - self.add_requirement(requirement) + for requirement_path in files: + requirement_level = requirement_path.parent.name + for requirement in Requirement.load( + self, LevelCollection.get(requirement_level), + requirement_path, publicID=self.publicID): + req_id += 1 + requirement._order_number = req_id + self.add_requirement(requirement) logger.debug("Profile %s loaded %s requiremens: %s", self.name, len(self._requirements), self._requirements) From 5a89d9d4caf0132ce174912162a6ad0e3c909b6d Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 10 Apr 2024 22:04:25 +0200 Subject: [PATCH 283/902] Refactor Checks to keep the same interface across types --- .../ro-crate/must/0_file_descriptor_format.py | 11 +-- rocrate_validator/errors.py | 2 - rocrate_validator/models.py | 33 ++------ .../requirements/python/__init__.py | 79 ++++++++++++++++--- .../requirements/shacl/checks.py | 3 +- rocrate_validator/utils.py | 2 +- 6 files changed, 80 insertions(+), 50 deletions(-) diff --git a/profiles/ro-crate/must/0_file_descriptor_format.py b/profiles/ro-crate/must/0_file_descriptor_format.py index 1dc36bba..5ead8c6f 100644 --- a/profiles/ro-crate/must/0_file_descriptor_format.py +++ b/profiles/ro-crate/must/0_file_descriptor_format.py @@ -2,13 +2,14 @@ import logging from typing import Optional -from rocrate_validator.models import RequirementCheck, ValidationContext, check +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check # set up logging logger = logging.getLogger(__name__) -class FileDescriptorExistence(RequirementCheck): +class FileDescriptorExistence(PyFunctionCheck): """The file descriptor MUST be present in the RO-Crate and MUST not be empty.""" @check(name="File Description Existence") def test_existence(self, context: ValidationContext) -> bool: @@ -27,7 +28,7 @@ def test_size(self, context: ValidationContext) -> bool: """ if not context.file_descriptor_path.exists(): context.result.add_error( - f'RO-Crate "{self.file_descriptor_path}" is empty: file descriptor is not present', self) + f'RO-Crate "{context.file_descriptor_path}" is empty: file descriptor is not present', self) return False if context.file_descriptor_path.stat().st_size == 0: context.result.add_error(f'RO-Crate "{context.file_descriptor_path}" file descriptor is empty', self) @@ -35,7 +36,7 @@ def test_size(self, context: ValidationContext) -> bool: return True -class FileDescriptorJsonFormat(RequirementCheck): +class FileDescriptorJsonFormat(PyFunctionCheck): """ The file descriptor MUST be a valid JSON file """ @@ -56,7 +57,7 @@ def check(self, context: ValidationContext) -> bool: return False -class FileDescriptorJsonLdFormat(RequirementCheck): +class FileDescriptorJsonLdFormat(PyFunctionCheck): """ The file descriptor MUST be a valid JSON-LD file """ diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 5858f3f9..a7fea08d 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -1,8 +1,6 @@ from typing import Optional -# from .models import RequirementCheck - class ROCValidatorError(Exception): pass diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 63e09313..7cb35366 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from functools import total_ordering from pathlib import Path -from typing import Callable, Optional, Union +from typing import Optional, Union from rdflib import Graph @@ -444,18 +444,16 @@ def load(profile: Profile, @total_ordering -class RequirementCheck: +class RequirementCheck(ABC): def __init__(self, requirement: Requirement, name: str, - check_function: Callable[[ValidationContext], bool], description: Optional[str] = None): self._requirement: Requirement = requirement self._order_number = 0 self._name = name self._description = description - self._check_function = check_function @property def order_number(self) -> int: @@ -495,14 +493,15 @@ def level(self) -> RequirementLevel: def severity(self) -> Severity: return self.requirement.level.severity - def check(self, context: ValidationContext) -> bool: - return self._check_function(self, context) + @abstractmethod + def execute_check(self, context: ValidationContext) -> bool: + raise NotImplementedError() def __do_check__(self, context: ValidationContext) -> bool: """ Internal method to perform the check """ - return self.check(context) + return self.execute_check(context) def __eq__(self, other: object) -> bool: if not isinstance(other, RequirementCheck): @@ -541,26 +540,6 @@ def __hash__(self) -> int: # return class_decorator -def check(name: Optional[str] = None): - """ - A decorator to mark functions as "checks" (by setting an attribute - `check=True`) and optionally annotating them with a human-legible name. - """ - def decorator(func): - check_name = name if name else func.__name__ - sig = inspect.signature(func) - if len(sig.parameters) != 2: - raise RuntimeError(f"Invalid check {check_name}. Checks are expected to " - "accept two arguments but this only takes {len(sig.parameters)}") - if sig.return_annotation not in (bool, inspect.Signature.empty): - raise RuntimeError(f"Invalid check {check_name}. Checks are expected to " - "return bool but this only returns {sig.return_annotation}") - func.check = True - func.name = check_name - return func - return decorator - - @total_ordering class CheckIssue: """ diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index 89c35c20..88471ce5 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -1,24 +1,54 @@ import inspect import logging from pathlib import Path -from typing import Optional +from typing import Callable, Optional, Type -from ...models import Profile, Requirement, RequirementCheck, RequirementLevel +from ...models import (Profile, Requirement, RequirementCheck, + RequirementLevel, ValidationContext) from ...utils import get_classes_from_file # set up logging logger = logging.getLogger(__name__) +class PyFunctionCheck(RequirementCheck): + """ + Concrete class that implements a check that calls a function. + """ + + def __init__(self, + requirement: Requirement, + name: str, + check_function: Callable[[RequirementCheck, ValidationContext], bool], + description: Optional[str] = None): + """ + check_function: a function that accepts an instance of PyFunctionCheck and a ValidationContext. + """ + super().__init__(requirement, name, description) + + sig = inspect.signature(check_function) + if len(sig.parameters) != 2: + raise RuntimeError("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): + raise RuntimeError("Invalid PyFunctionCheck function. Checks are expected to " + f"return bool but this only returns {sig.return_annotation}") + + self._check_function = check_function + + def execute_check(self, context: ValidationContext) -> bool: + return self._check_function(self, context) + + class PyRequirement(Requirement): def __init__(self, level: RequirementLevel, profile: Profile, + requirement_check_class: Type[PyFunctionCheck], name: str = "", description: Optional[str] = None, - path: Optional[Path] = None, - requirement_check_class=None): + path: Optional[Path] = None): self.requirement_check_class = requirement_check_class super().__init__(level, profile, name, description, path, initialize_checks=True) @@ -34,7 +64,10 @@ def __init_checks__(self): except Exception: check_name = name.strip() check_description = member.__doc__.strip() if member.__doc__ else "" - check = self.requirement_check_class(self, check_name, member, check_description) + check = self.requirement_check_class(requirement=self, + name=check_name, + check_function=member, + description=check_description) self._checks.append(check) logger.debug("Added check: %s %r", check_name, check) @@ -45,21 +78,41 @@ def load(profile: Profile, requirement_level: RequirementLevel, file_path: Path) # instantiate a list to store the requirements requirements: list[Requirement] = [] - # get the classes from the file - classes = get_classes_from_file(file_path, filter_class=RequirementCheck) + # Get the classes in the file that are subclasses of RequirementCheck + classes = get_classes_from_file(file_path, filter_class=PyFunctionCheck) logger.debug("Classes: %r", classes) # instantiate a requirement for each class - for requirement_name, requirement_class in classes.items(): + for requirement_name, check_class in classes.items(): logger.debug("Processing requirement: %r", requirement_name) r = PyRequirement( - requirement_level, profile, + requirement_level, + profile, + requirement_check_class=check_class, name=requirement_name.strip() if requirement_name else "", - description=requirement_class.__doc__.strip() if requirement_class.__doc__ else "", - path=file_path, - requirement_check_class=requirement_class - ) + description=check_class.__doc__.strip() if check_class.__doc__ else "", + path=file_path) logger.debug("Created requirement: %r", r) requirements.append(r) return requirements + + +def check(name: Optional[str] = None): + """ + A decorator to mark functions as "checks" (by setting an attribute + `check=True`) and optionally annotating them with a human-legible name. + """ + def decorator(func): + check_name = name if name else func.__name__ + sig = inspect.signature(func) + if len(sig.parameters) != 2: + raise RuntimeError(f"Invalid check {check_name}. Checks are expected to " + "accept two arguments but this only takes {len(sig.parameters)}") + if sig.return_annotation not in (bool, inspect.Signature.empty): + raise RuntimeError(f"Invalid check {check_name}. Checks are expected to " + "return bool but this only returns {sig.return_annotation}") + func.check = True + func.name = check_name + return func + return decorator diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 2fe206af..66c9ccf0 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -23,7 +23,6 @@ def __init__(self, super().__init__(requirement, shapeProperty.name if shapeProperty and shapeProperty.name else None, - self.check, shapeProperty.description if shapeProperty and shapeProperty.description else None) @@ -31,7 +30,7 @@ def __init__(self, def shapeProperty(self) -> ShapeProperty: return self._shapeProperty - def check(self, context: ValidationContext): + def execute_check(self, context: ValidationContext): ontology_graph = context.validator.ontologies_graph data_graph = context.validator.data_graph diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 55e02d10..3df65c7a 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -158,7 +158,7 @@ def get_classes_from_file(file_path: Path, module = import_module(module_name) logger.debug("Module: %r", module) - # Get all classes in the module that are subclasses of Check + # Get all classes in the module that are subclasses of filter_class classes = {name: cls for name, cls in inspect.getmembers(module, inspect.isclass) if cls.__module__ == module_name and (not class_name_suffix or cls.__name__.endswith(class_name_suffix)) From 556a27b8411337f7754ebd10cb321f1a96ea77f3 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 10 Apr 2024 22:13:43 +0200 Subject: [PATCH 284/902] Return individual requirement validation based on its own checks --- rocrate_validator/models.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 7cb35366..e9fc0388 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -358,25 +358,34 @@ def __reorder_checks__(self) -> None: def __do_validate__(self, context: ValidationContext) -> bool: """ Internal method to perform the validation + Returns whether all checks in this requirement passed. """ logger.debug("Validating Requirement %s (level=%s) with %s checks", self.name, self.level, len(self._checks)) logger.debug("Running %s checks for Requirement '%s'", len(self._checks), self.name) + all_passed = True for check in self._checks: try: - # TODO: if __do_check__ is internal, why are we calling it from here? logger.debug("Running check '%s' - Desc: %s", check.name, check.description) - result = check.__do_check__(context) - logger.debug("Ran check '%s'. Got result %s", check.name, result) + check_result = check.execute_check(context) + logger.debug("Ran check '%s'. Got result %s", check.name, 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) + raise RuntimeError(f"Ignoring invalid result from check {check.name}") + all_passed = all_passed and check_result except Exception as e: context.result.add_error(f"Unexpected error during check: {e}", check=check) if logger.isEnabledFor(logging.DEBUG): logger.exception(e) - logger.debug("Checks for Requirement '%s' completed. Checks passed? %s", - self.name, context.result.passed()) - # Return the result - return context.result.passed() + # It is probably better to ignore checks that fail with an exception, and ensure + # that all checks end by returning a boolean pass/fail value. This way, + # we can ignore failed checks as far as the final result go and RO-Crates + # would be "valid until proven invalid". + all_passed = False + + logger.debug("Checks for Requirement '%s' completed. Checks passed? %s", self.name, all_passed) + return all_passed def __eq__(self, other: object) -> bool: if not isinstance(other, Requirement): From e1e36237b9167d4d32744170f3a07a79cd74c3b5 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 10 Apr 2024 23:45:10 +0200 Subject: [PATCH 285/902] Revise output of Python requirement checks --- .../ro-crate/must/0_file_descriptor_format.py | 23 +++++++++---------- rocrate_validator/cli/commands/validate.py | 5 ++-- rocrate_validator/models.py | 10 ++++++-- tests/test_cli.py | 4 +++- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/profiles/ro-crate/must/0_file_descriptor_format.py b/profiles/ro-crate/must/0_file_descriptor_format.py index 5ead8c6f..0dcafd2d 100644 --- a/profiles/ro-crate/must/0_file_descriptor_format.py +++ b/profiles/ro-crate/must/0_file_descriptor_format.py @@ -17,7 +17,8 @@ def test_existence(self, context: ValidationContext) -> bool: Check if the file descriptor is present in the RO-Crate """ if not context.file_descriptor_path.exists(): - context.result.add_error(f'RO-Crate "{context.file_descriptor_path}" file descriptor is not present', self) + message = f'file descriptor "{context.rel_fd_path}" is not present' + context.result.add_error(message, self) return False return True @@ -27,11 +28,11 @@ def test_size(self, context: ValidationContext) -> bool: Check if the file descriptor is not empty """ if not context.file_descriptor_path.exists(): - context.result.add_error( - f'RO-Crate "{context.file_descriptor_path}" is empty: file descriptor is not present', self) + message = f'file descriptor {context.rel_fd_path} is empty' + context.result.add_error(message, self) return False if context.file_descriptor_path.stat().st_size == 0: - context.result.add_error(f'RO-Crate "{context.file_descriptor_path}" file descriptor is empty', self) + context.result.add_error(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) return False return True @@ -50,8 +51,7 @@ def check(self, context: ValidationContext) -> bool: return True except Exception as e: context.result.add_error( - f'RO-Crate "{context.file_descriptor_path}" "\ - "file descriptor is not in the correct format', self) + f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self) if logger.isEnabledFor(logging.DEBUG): logger.exception(e) return False @@ -75,8 +75,7 @@ def get_json_dict(self, context: ValidationContext) -> dict: file_descriptor_path=context.file_descriptor_path) except Exception as e: context.result.add_error( - f"RO-Crate \"{context.file_descriptor_path}\" " - "file descriptor is not in the correct format", self) + f'file descriptor "{context.rel_fd_path}" is not in the correct format', self) if logger.isEnabledFor(logging.DEBUG): logger.exception(e) return {} @@ -87,8 +86,8 @@ def check_context(self, validation_context: ValidationContext) -> bool: json_dict = self.get_json_dict(validation_context) if "@context" not in json_dict: validation_context.result.add_error( - f"RO-Crate \"{validation_context.file_descriptor_path}\" " - "file descriptor does not contain a context", self) + f'RO-Crate file descriptor "{validation_context.rel_fd_path}" ' + "does not contain a context", self) return False return True @@ -99,7 +98,7 @@ def check_identifiers(self, context: ValidationContext) -> bool: if "@id" not in entity: context.result.add_error( f"Entity \"{entity.get('name', None) or entity}\" " - f"of RO-Crate \"{context.file_descriptor_path}\" " + f"of RO-Crate \"{context.rel_fd_path}\" " "file descriptor does not contain the @id attribute", self) return False return True @@ -111,7 +110,7 @@ def check_types(self, context: ValidationContext) -> bool: if "@type" not in entity: context.result.add_error( f"Entity \"{entity.get('name', None) or entity}\" " - f"of RO-Crate \"{context.file_descriptor_path}\" " + f"of RO-Crate \"{context.rel_fd_path}\" " "file descriptor does not contain the @type attribute", self) return False return True diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 05b42e05..c40b5681 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -144,15 +144,16 @@ def __print_validation_result__( """ Print the validation result """ + rel_roc_path = Path(result.rocrate_path).relative_to(Path.cwd()) if result.passed(severity=severity): console.print( - "\n\n[bold][[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", + f"\n\n[bold][[green]OK[/green]] RO-Crate {rel_roc_path} is [green]valid[/green] !!![/bold]\n\n", style="white", ) else: console.print( - "\n\n[bold][[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", + f"\n\n[bold][[red]FAILED[/red]] RO-Crate {rel_roc_path} is [red]not valid[/red] !!![/bold]\n", style="white", ) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index e9fc0388..88bbcce4 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -642,10 +642,12 @@ def __init__(self, rocrate_path: Path, validation_settings: Optional[dict[str, B # keep track of the issues found during the validation self._issues: list[CheckIssue] = [] - def get_rocrate_path(self): + @property + def rocrate_path(self): return self._rocrate_path - def get_validation_settings(self): + @property + def validation_settings(self): return self._validation_settings # --- Issues --- @@ -933,3 +935,7 @@ def rocrate_path(self) -> Path: @property def file_descriptor_path(self) -> Path: return self.rocrate_path / ROCRATE_METADATA_FILE + + @property + def rel_fd_path(self) -> Path: + return Path(ROCRATE_METADATA_FILE) diff --git a/tests/test_cli.py b/tests/test_cli.py index 071f2205..a01b4156 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,6 @@ +import re + from click.testing import CliRunner from pytest import fixture @@ -21,7 +23,7 @@ def test_version(cli_runner: CliRunner): def test_validate_subcmd_valid_rocrate(cli_runner: CliRunner): result = cli_runner.invoke(cli, ['validate', str(ValidROC().wrroc_paper_long_date)]) assert result.exit_code == 0 - assert 'RO-Crate is valid' in result.output + assert re.search(r'RO-Crate.*is valid', result.output) def test_validate_subcmd_invalid_rocrate1(cli_runner: CliRunner): From 68519e46c575e0f24deff745ddc6ca8e0c347ce7 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Wed, 10 Apr 2024 23:45:42 +0200 Subject: [PATCH 286/902] Remove dead code --- rocrate_validator/models.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 88bbcce4..672d589e 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -506,12 +506,6 @@ def severity(self) -> Severity: def execute_check(self, context: ValidationContext) -> bool: raise NotImplementedError() - def __do_check__(self, context: ValidationContext) -> bool: - """ - Internal method to perform the check - """ - return self.execute_check(context) - def __eq__(self, other: object) -> bool: if not isinstance(other, RequirementCheck): raise ValueError(f"Cannot compare RequirementCheck with {type(other)}") @@ -562,7 +556,7 @@ class CheckIssue: """ # TODO: - # 2. CheckIssue has the check, to it is able to determine the level and the Severity + # 2. CheckIssue has the check, so it is able to determine the level and the Severity # without having it provided through an additional argument. def __init__(self, severity: Severity, check: RequirementCheck, From 27314b7af1ad3257c12144dcab476010b07f7f05 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 11 Apr 2024 11:52:43 +0200 Subject: [PATCH 287/902] Fix issue sorting on CLI --- rocrate_validator/cli/commands/validate.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index c40b5681..41c9954f 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -159,7 +159,8 @@ def __print_validation_result__( console.print("\n[bold]The following requirements have not meet: [/bold]\n", style="white") - for requirement in sorted(result.failed_requirements): + for requirement in sorted(result.failed_requirements, + key=lambda x: (-x.severity.value, x)): issue_color = get_severity_color(requirement.severity) console.print( Align(f" [severity: [{issue_color}]{requirement.severity.name}[/{issue_color}], " @@ -172,13 +173,15 @@ def __print_validation_result__( console.print(f"\n{' '*4}{requirement.description}\n", style="white italic") console.print(f"{' '*4}Failed checks:\n", style="white bold") - for check in sorted(result.get_failed_checks_by_requirement(requirement)): + for check in sorted(result.get_failed_checks_by_requirement(requirement), + key=lambda x: (-x.severity.value, x)): issue_color = get_severity_color(check.level.severity) console.print( f"{' '*4}- " f"[magenta]{check.name}[/magenta]: {check.description}") console.print(f"\n{' '*6}Detected issues:", style="white bold") - for issue in sorted(result.get_issues_by_check(check)): + for issue in sorted(result.get_issues_by_check(check), + key=lambda x: (-x.severity.value, x)): console.print( f"{' '*6}- [[{issue_color}]Violation[/{issue_color}] of " f"[magenta]{issue.check.identifier}[/magenta]]: {issue.message}") From e8b8c8d11f24f43c865882fbf3c2772ac1140d90 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 11 Apr 2024 11:53:13 +0200 Subject: [PATCH 288/902] Add type checking in __lt__ Profile comparison --- rocrate_validator/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 672d589e..10432933 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -208,6 +208,8 @@ def __eq__(self, other: object) -> bool: and self.requirements == other.requirements def __lt__(self, other: object) -> bool: + if not isinstance(other, Profile): + raise TypeError(f"Cannot compare {type(self)} with {type(other)}") return self.name < other.name def __hash__(self) -> int: From c671c9a1a82011a32ebe89bd99512e1019db0f3a Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 11 Apr 2024 11:53:50 +0200 Subject: [PATCH 289/902] Don't fail validation if check fails to run (raises exception) --- rocrate_validator/models.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 10432933..594332cd 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -377,14 +377,11 @@ def __do_validate__(self, context: ValidationContext) -> bool: raise RuntimeError(f"Ignoring invalid result from check {check.name}") all_passed = all_passed and check_result except Exception as e: - context.result.add_error(f"Unexpected error during check: {e}", check=check) + # Ignore the fact that the check failed as far as the validation result is concerned. + 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(e) - # It is probably better to ignore checks that fail with an exception, and ensure - # that all checks end by returning a boolean pass/fail value. This way, - # we can ignore failed checks as far as the final result go and RO-Crates - # would be "valid until proven invalid". - all_passed = False logger.debug("Checks for Requirement '%s' completed. Checks passed? %s", self.name, all_passed) return all_passed From 90634e04d4d41aac89a66f1aa7575c9e03ed576a Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 11 Apr 2024 15:36:03 +0200 Subject: [PATCH 290/902] Add Luca as co-author --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9cc4bf37..49058abf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "rocrate-validator" version = "0.1.0" description = "" -authors = ["Marco Enrico Piras "] +authors = ["Marco Enrico Piras ", "Luca Pireddu "] readme = "README.md" packages = [{ include = "rocrate_validator", from = "." }] From 4b86b2767665675a77459c0ff19f50fcf76e6e54 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 11 Apr 2024 15:37:47 +0200 Subject: [PATCH 291/902] Typing and None checks. Resolves all pyright diagnostics for the project --- rocrate_validator/models.py | 9 +++++---- .../requirements/python/__init__.py | 5 ++++- rocrate_validator/services.py | 17 ++++++++--------- rocrate_validator/utils.py | 6 +++--- tests/shared.py | 3 ++- tests/test_models.py | 1 - 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 594332cd..6af390cd 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -243,7 +243,7 @@ def __str__(self) -> str: # return [requirement for requirement in self.requirements if requirement.severity == type] @staticmethod - def load(path: Union[str, Path], publicID: str = None) -> Profile: + def load(path: Union[str, Path], publicID: Optional[str] = None) -> Profile: # if the path is a string, convert it to a Path if isinstance(path, str): path = Path(path) @@ -255,7 +255,7 @@ def load(path: Union[str, Path], publicID: str = None) -> Profile: return profile @staticmethod - def load_profiles(profiles_path: Union[str, Path], publicID: str = None) -> dict[str, Profile]: + def load_profiles(profiles_path: Union[str, Path], publicID: Optional[str] = None) -> dict[str, Profile]: # if the path is a string, convert it to a Path if isinstance(profiles_path, str): profiles_path = Path(profiles_path) @@ -283,7 +283,7 @@ def __init__(self, description: Optional[str] = None, path: Optional[Path] = None, initialize_checks: bool = True): - self._order_number: int = None + self._order_number: Optional[int] = None self._level = level self._profile = profile self._description = description @@ -307,6 +307,7 @@ def __init__(self, @property def order_number(self) -> int: + assert self._order_number is not None return self._order_number @property @@ -727,7 +728,7 @@ def __init__(self, allow_infos: Optional[bool] = False, allow_warnings: Optional[bool] = False, serialization_output_path: Optional[Path] = None, - serialization_output_format: Optional[RDF_SERIALIZATION_FORMATS_TYPES] = "turtle", + serialization_output_format: RDF_SERIALIZATION_FORMATS_TYPES = "turtle", **kwargs): self.rocrate_path = rocrate_path self.profiles_path = profiles_path diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index 88471ce5..0b005584 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -74,7 +74,10 @@ def __init_checks__(self): return checks @staticmethod - def load(profile: Profile, requirement_level: RequirementLevel, file_path: Path) -> list[Requirement]: + def load(profile: Profile, + requirement_level: RequirementLevel, + file_path: Path, + publicID: Optional[str] = None) -> list[Requirement]: # instantiate a list to store the requirements requirements: list[Requirement] = [] diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 523e83b5..3e946467 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -1,11 +1,10 @@ import logging from pathlib import Path -from typing import Literal, Optional, Union +from typing import Optional, Union -from pyshacl.pytypes import GraphLike - -from .models import (LevelCollection, Profile, RequirementLevel, Severity, - ValidationResult, Validator) +from .constants import (RDF_SERIALIZATION_FORMATS_TYPES, + VALID_INFERENCE_OPTIONS_TYPES) +from .models import Profile, Severity, ValidationResult, Validator # set up logging logger = logging.getLogger(__name__) @@ -18,7 +17,7 @@ def validate( inherit_profiles: bool = True, ontologies_path: Optional[Path] = None, advanced: Optional[bool] = False, - inference: Optional[Literal["owl", "rdfs"]] = None, + inference: Optional[VALID_INFERENCE_OPTIONS_TYPES] = None, inplace: Optional[bool] = False, abort_on_first: Optional[bool] = True, allow_infos: Optional[bool] = False, @@ -26,7 +25,7 @@ def validate( requirement_severity: Union[str, Severity] = Severity.REQUIRED, requirement_severity_only: bool = False, serialization_output_path: Optional[Path] = None, - serialization_output_format: str = "turtle", + serialization_output_format: RDF_SERIALIZATION_FORMATS_TYPES = "turtle", **kwargs, ) -> ValidationResult: """ @@ -60,7 +59,7 @@ def validate( return result -def get_profiles(profiles_path: str = "./profiles", publicID: str = None) -> dict[str, Profile]: +def get_profiles(profiles_path: str = "./profiles", publicID: Optional[str] = None) -> dict[str, Profile]: """ Load the profiles from the given path """ @@ -70,7 +69,7 @@ def get_profiles(profiles_path: str = "./profiles", publicID: str = None) -> dic def get_profile(profiles_path: str = "./profiles", - profile_name: str = "ro-crate", publicID: str = None) -> Profile: + profile_name: str = "ro-crate", publicID: Optional[str] = None) -> Profile: """ Load the profiles from the given path """ diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 3df65c7a..6b9984e4 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -32,7 +32,7 @@ def get_version() -> str: return config["tool"]["poetry"]["version"] -def get_config(property: str = None) -> dict: +def get_config(property: Optional[str] = None) -> dict: """ Get the configuration for the package or a specific property @@ -98,8 +98,8 @@ def get_all_files( return file_paths -def get_graphs_paths( - graphs_dir: str = CURRENT_DIR, serialization_format="turtle") -> list[str]: +def get_graphs_paths(graphs_dir: str = CURRENT_DIR, + serialization_format: constants.RDF_SERIALIZATION_FORMATS_TYPES = "turtle") -> list[str]: """ Get the paths to all the graphs in the directory diff --git a/tests/shared.py b/tests/shared.py index 383ad902..6c7922a2 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -73,7 +73,8 @@ def do_entity_test( f"\"{expected_triggered_requirement}\" was not found in the failed requirements" # check requirement issues - detected_issues = [issue.message for issue in result.get_issues(models.Severity.RECOMMENDED)] + detected_issues = [issue.message for issue in result.get_issues(models.Severity.RECOMMENDED) + if issue.message is not None] logger.debug("Detected issues: %s", detected_issues) logger.debug("Expected issues: %s", expected_triggered_issues) for expected_issue in expected_triggered_issues: diff --git a/tests/test_models.py b/tests/test_models.py index a142ddbd..86df528c 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -5,7 +5,6 @@ from rocrate_validator.models import (LevelCollection, RequirementLevel, Severity) from tests.ro_crates import InvalidFileDescriptor, InvalidRootDataEntity -from tests.shared import first def test_severity_ordering(): From bbda8083e9fc9e055eb86c88be725d300b7a9589 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Mon, 15 Apr 2024 15:49:21 +0200 Subject: [PATCH 292/902] Remove RO-Crate path from main output message. Resolves issue #2 --- rocrate_validator/cli/commands/validate.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 41c9954f..7397a7c0 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -144,16 +144,14 @@ def __print_validation_result__( """ Print the validation result """ - rel_roc_path = Path(result.rocrate_path).relative_to(Path.cwd()) - if result.passed(severity=severity): console.print( - f"\n\n[bold][[green]OK[/green]] RO-Crate {rel_roc_path} is [green]valid[/green] !!![/bold]\n\n", + f"\n\n[bold][[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", style="white", ) else: console.print( - f"\n\n[bold][[red]FAILED[/red]] RO-Crate {rel_roc_path} is [red]not valid[/red] !!![/bold]\n", + f"\n\n[bold][[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", style="white", ) From d231645e384118a8ee922536e7ed0bb3376e8924 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 16 Apr 2024 11:21:08 +0200 Subject: [PATCH 293/902] fix(utils): :bug: make the pyproject path relative to the tool root --- rocrate_validator/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 6b9984e4..ac2d64d4 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) # Read the pyproject.toml file -config = toml.load("pyproject.toml") +config = toml.load(Path(CURRENT_DIR).parent / "pyproject.toml") def get_version() -> str: From 557deef76c78e8dbb7187d0458b9438ad50a085d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 16 Apr 2024 11:24:16 +0200 Subject: [PATCH 294/902] build(core): :wrench: include pyproject.toml, LICENSE and README files in the distribution package --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 49058abf..276775ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ description = "" authors = ["Marco Enrico Piras ", "Luca Pireddu "] readme = "README.md" packages = [{ include = "rocrate_validator", from = "." }] +include = ["pyproject.toml", "README.md", "LICENSE"] [tool.poetry.dependencies] python = "^3.8.1" From 207a7171f0377487c4c2a1e8e0e3abbd6d987c1a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 16 Apr 2024 16:30:56 +0200 Subject: [PATCH 295/902] feat(utils): :sparkles: add function to configure default profiles path --- rocrate_validator/utils.py | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index ac2d64d4..5ff3b67c 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -54,6 +54,48 @@ def get_file_descriptor_path(rocrate_path: Path) -> Path: return Path(rocrate_path) / constants.ROCRATE_METADATA_FILE +def get_default_profiles_paths() -> list[Path]: + """ + Get the paths to the profiles directory + + :return: The paths to the profiles directory + """ + return [ + Path("profiles"), + Path.home() / ".config/rocrate-validator/profiles", + Path(CURRENT_DIR).parent / "profiles" + ] + + +def get_profiles_path(not_exist_ok: bool = True) -> Path: + """ + Get the path to the profiles directory from the default paths + + :param not_exist_ok: If True, return the path even if it does not exist + + :return: The path to the profiles directory + """ + profiles_path = None + # Get the default profiles paths + default_profiles_paths = get_default_profiles_paths() + logger.debug("Default profiles paths: %r", default_profiles_paths) + for path in default_profiles_paths: + if path.exists(): + profiles_path = path + break + # Check if the profiles directory is found + if not profiles_path: + # If the profiles directory is not found, raise an error + if not not_exist_ok: + # Raise an error if the profiles directory provided with the package is not found + raise errors.ProfilesDirectoryNotFound(profiles_path=str(default_profiles_paths[-1])) + else: + # Use the last path as the profiles directory, i.e., the one provided with the package + profiles_path = default_profiles_paths[-1] + # Return the profiles directory + return profiles_path + + def get_format_extension(serialization_format: constants.RDF_SERIALIZATION_FORMATS_TYPES) -> str: """ Get the file extension for the RDF serialization format From aba0529d1f65e7e3e15ea9dfd739a9491e369733 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 16 Apr 2024 16:38:38 +0200 Subject: [PATCH 296/902] refactor(core): --- rocrate_validator/cli/commands/profiles.py | 13 +++++++++---- rocrate_validator/cli/commands/validate.py | 8 ++++++-- rocrate_validator/models.py | 8 ++++++-- rocrate_validator/services.py | 19 +++++++++++++------ 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 1b97efe6..4f677d0b 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -1,12 +1,17 @@ import logging +from pathlib import Path from rich.markdown import Markdown from rich.table import Table from ... import services from ...colors import get_severity_color +from ...utils import get_profiles_path from ..main import cli, click +# set the default profiles path +DEFAULT_PROFILES_PATH = get_profiles_path() + # set up logging logger = logging.getLogger(__name__) @@ -16,12 +21,12 @@ "-p", "--profiles-path", type=click.Path(exists=True), - default="./profiles", + default=DEFAULT_PROFILES_PATH, show_default=True, help="Path containing the profiles files" ) @click.pass_context -def profiles(ctx, profiles_path: str = "./profiles"): +def profiles(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH): """ [magenta]rocrate-validator:[/magenta] Manage profiles """ @@ -29,7 +34,7 @@ def profiles(ctx, profiles_path: str = "./profiles"): @profiles.command("list") @click.pass_context -def list_profiles(ctx, profiles_path: str = "./profiles"): +def list_profiles(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH): """ List available profiles """ @@ -77,7 +82,7 @@ def list_profiles(ctx, profiles_path: str = "./profiles"): @click.pass_context def describe_profile(ctx, profile_name: str = "ro-crate", - profiles_path: str = "./profiles"): + profiles_path: Path = DEFAULT_PROFILES_PATH): """ Show a profile """ diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 7397a7c0..f6342e91 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -10,11 +10,15 @@ from ... import services from ...colors import get_severity_color from ...models import Severity, ValidationResult +from ...utils import get_profiles_path from ..main import cli, click # from rich.markdown import Markdown # from rich.table import Table +# set the default profiles path +DEFAULT_PROFILES_PATH = get_profiles_path() + # set up logging logger = logging.getLogger(__name__) @@ -32,7 +36,7 @@ @click.option( "--profiles-path", type=click.Path(exists=True), - default="./profiles", + default=DEFAULT_PROFILES_PATH, show_default=True, help="Path containing the profiles files", ) @@ -77,7 +81,7 @@ # ) @click.pass_context def validate(ctx, - profiles_path: Path = Path("./profiles"), + profiles_path: Path = DEFAULT_PROFILES_PATH, profile_name: str = "ro-crate", disable_profile_inheritance: bool = False, requirement_severity: str = Severity.REQUIRED.name, diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 6af390cd..9e69b223 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -18,7 +18,11 @@ RDF_SERIALIZATION_FORMATS_TYPES, ROCRATE_METADATA_FILE, VALID_INFERENCE_OPTIONS_TYPES) -from rocrate_validator.utils import get_requirement_name_from_file +from rocrate_validator.utils import (get_profiles_path, + get_requirement_name_from_file) + +# set the default profiles path +DEFAULT_PROFILES_PATH = get_profiles_path() logger = logging.getLogger(__name__) @@ -715,7 +719,7 @@ class Validator: def __init__(self, rocrate_path: Path, - profiles_path: Path = Path("./profiles"), + profiles_path: Path = DEFAULT_PROFILES_PATH, profile_name: str = "ro-crate", disable_profile_inheritance: bool = False, requirement_severity: Severity = Severity.REQUIRED, diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 3e946467..53adfeb2 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -2,9 +2,16 @@ from pathlib import Path from typing import Optional, Union +from pyshacl.pytypes import GraphLike + from .constants import (RDF_SERIALIZATION_FORMATS_TYPES, VALID_INFERENCE_OPTIONS_TYPES) -from .models import Profile, Severity, ValidationResult, Validator +from .models import (LevelCollection, Profile, RequirementLevel, Severity, + ValidationResult, Validator) +from .utils import get_profiles_path + +# set the default profiles path +DEFAULT_PROFILES_PATH = get_profiles_path() # set up logging logger = logging.getLogger(__name__) @@ -12,7 +19,7 @@ def validate( rocrate_path: Path, - profiles_path: Path = Path("./profiles"), + profiles_path: Path = DEFAULT_PROFILES_PATH, profile_name: str = "ro-crate", inherit_profiles: bool = True, ontologies_path: Optional[Path] = None, @@ -59,7 +66,7 @@ def validate( return result -def get_profiles(profiles_path: str = "./profiles", publicID: Optional[str] = None) -> dict[str, Profile]: +def get_profiles(profiles_path: Path = DEFAULT_PROFILES_PATH, publicID: str = None) -> dict[str, Profile]: """ Load the profiles from the given path """ @@ -68,12 +75,12 @@ def get_profiles(profiles_path: str = "./profiles", publicID: Optional[str] = No return profiles -def get_profile(profiles_path: str = "./profiles", - profile_name: str = "ro-crate", publicID: Optional[str] = None) -> Profile: +def get_profile(profiles_path: Path = DEFAULT_PROFILES_PATH, + profile_name: str = "ro-crate", publicID: str = None) -> Profile: """ Load the profiles from the given path """ - profile_path = f"{profiles_path}/{profile_name}" + profile_path = profiles_path / profile_name if not Path(profiles_path).exists(): raise FileNotFoundError(f"Profile not found: {profile_path}") profile = Profile.load(f"{profiles_path}/{profile_name}", publicID=publicID) From 20924a94746aa2d96d80138d0a726bfbe9836935 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 16 Apr 2024 16:41:32 +0200 Subject: [PATCH 297/902] refactor(cli): :goal_net: catch all unexpected errors at the CLI entry point --- rocrate_validator/cli/commands/profiles.py | 80 ++++++++++------------ rocrate_validator/cli/commands/validate.py | 48 +++++-------- rocrate_validator/cli/main.py | 17 ++++- rocrate_validator/errors.py | 18 +++++ 4 files changed, 89 insertions(+), 74 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 4f677d0b..8380066f 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -88,46 +88,40 @@ def describe_profile(ctx, """ console = ctx.obj['console'] # Get the profile - try: - profile = services.get_profile(profiles_path=profiles_path, profile_name=profile_name) - - console.print("\n", style="white bold") - console.print(f"[bold]Profile: {profile_name}[/bold]", style="magenta bold") - console.print("\n", style="white bold") - console.print(Markdown(profile.description.strip())) - console.print("\n", style="white bold") - - table_rows = [] - levels_list = set() - for requirement in profile.requirements: - color = get_severity_color(requirement.severity) - level_info = f"[{color}]{requirement.severity.name}[/{color}]" - levels_list.add(level_info) - table_rows.append((str(requirement.order_number), requirement.name, - Markdown(requirement.description.strip()), - str(len(requirement.get_checks())), - level_info)) - - table = Table(show_header=True, - title="Profile Requirements", - header_style="bold cyan", - border_style="bright_black", - show_footer=False, - caption=f"(*) Requirement level: {', '.join(levels_list)}") - - # Define columns - table.add_column("#", style="yellow bold", justify="right") - table.add_column("Name", style="magenta bold", justify="center") - table.add_column("Description", style="white italic") - table.add_column("# Checks", style="white", justify="center") - table.add_column("Requirement Level (*)", style="white", justify="center") - # Add data to the table - for row in table_rows: - table.add_row(*row) - # Print the table - console.print(table) - except Exception as e: - console.print(f"\n[red]ERROR:[/red] {e}\n", style="white") - if logger.isEnabledFor(logging.DEBUG): - console.print_exception(show_locals=True) - ctx.exit(1) + profile = services.get_profile(profiles_path=profiles_path, profile_name=profile_name) + + console.print("\n", style="white bold") + console.print(f"[bold]Profile: {profile_name}[/bold]", style="magenta bold") + console.print("\n", style="white bold") + console.print(Markdown(profile.description.strip())) + console.print("\n", style="white bold") + + table_rows = [] + levels_list = set() + for requirement in profile.requirements: + color = get_severity_color(requirement.severity) + level_info = f"[{color}]{requirement.severity.name}[/{color}]" + levels_list.add(level_info) + table_rows.append((str(requirement.order_number), requirement.name, + Markdown(requirement.description.strip()), + str(len(requirement.get_checks())), + level_info)) + + table = Table(show_header=True, + title="Profile Requirements", + header_style="bold cyan", + border_style="bright_black", + show_footer=False, + caption=f"(*) Requirement level: {', '.join(levels_list)}") + + # Define columns + table.add_column("#", style="yellow bold", justify="right") + table.add_column("Name", style="magenta bold", justify="center") + table.add_column("Description", style="white italic") + table.add_column("# Checks", style="white", justify="center") + table.add_column("Requirement Level (*)", style="white", justify="center") + # Add data to the table + for row in table_rows: + table.add_row(*row) + # Print the table + console.print(table) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index f6342e91..5f7bc687 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -109,36 +109,24 @@ def validate(ctx, if rocrate_path: logger.debug("rocrate_path: %s", os.path.abspath(rocrate_path)) - try: - # Validate the RO-Crate - result: ValidationResult = services.validate( - profiles_path=profiles_path, - profile_name=profile_name, - requirement_severity=requirement_severity, - requirement_severity_only=requirement_severity_only, - disable_profile_inheritance=disable_profile_inheritance, - rocrate_path=Path(rocrate_path).absolute(), - ontologies_path=Path(ontologies_path).absolute() if ontologies_path else None, - abort_on_first=not no_fail_fast - ) - - # Print the validation result - __print_validation_result__(console, result) - - # using ctx.exit seems to raise an Exception that gets caught below, - # so we use sys.exit instead. - sys.exit(0 if result.passed(Severity.RECOMMENDED) else 1) - 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) + # Validate the RO-Crate + result: ValidationResult = services.validate( + profiles_path=profiles_path, + profile_name=profile_name, + requirement_severity=requirement_severity, + requirement_severity_only=requirement_severity_only, + disable_profile_inheritance=disable_profile_inheritance, + rocrate_path=Path(rocrate_path).absolute(), + ontologies_path=Path(ontologies_path).absolute() if ontologies_path else None, + abort_on_first=not no_fail_fast + ) + + # Print the validation result + __print_validation_result__(console, result) + + # using ctx.exit seems to raise an Exception that gets caught below, + # so we use sys.exit instead. + sys.exit(0 if result.passed(Severity.RECOMMENDED) else 1) def __print_validation_result__( diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index f5f34544..b74f29e2 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -5,6 +5,7 @@ from rich.console import Console from rocrate_validator.config import configure_logging +from rocrate_validator.errors import ProfilesDirectoryNotFound from rocrate_validator.utils import get_version # set up logging @@ -57,16 +58,30 @@ def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool): # If no subcommand is provided, invoke the default command from .commands.validate import validate ctx.invoke(validate) + except ProfilesDirectoryNotFound as e: + error_message = f""" + The profile folder could not be located at the specified path: [red]{e.profiles_path}[/red]. + Please ensure that the path is correct and try again. + """ + console.print( + f"\n\n[bold][[red]ERROR[/red]] {error_message} !!![/bold]\n", style="white") + sys.exit(2) except Exception as e: console.print( f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", style="white") if logger.isEnabledFor(logging.DEBUG): console.print_exception() + console.print(""" + This error may be due to a bug. Please report it to the issue tracker + along with the following stack trace: + """) sys.exit(2) if __name__ == "__main__": try: cli() - except Exception: + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(f"An unexpected error occurred: {e}") exit(2) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index a7fea08d..1a7e8199 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -6,6 +6,24 @@ class ROCValidatorError(Exception): pass +class ProfilesDirectoryNotFound(ROCValidatorError): + """Raised when the profiles directory is not found.""" + + def __init__(self, profiles_path: Optional[str] = None): + self._profiles_path = profiles_path + + @property + def profiles_path(self) -> Optional[str]: + """The path to the profiles directory.""" + return self._profiles_path + + def __str__(self) -> str: + return f"Profiles directory not found: {self._profiles_path!r}" + + def __repr__(self): + return f"ProfilesDirectoryNotFound({self._profiles_path!r})" + + class InvalidSerializationFormat(ROCValidatorError): """Raised when an invalid serialization format is provided.""" From 0713aaf385129bd6300be67d94a395ffae204865 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 16 Apr 2024 16:43:42 +0200 Subject: [PATCH 298/902] build(profiles): :building_construction: include the 'profiles' folder in the distribution packages --- pyproject.toml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 276775ce..bdc6c8ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,24 @@ description = "" authors = ["Marco Enrico Piras ", "Luca Pireddu "] readme = "README.md" packages = [{ include = "rocrate_validator", from = "." }] -include = ["pyproject.toml", "README.md", "LICENSE"] +include = [ + { path = "pyproject.toml", format = [ + "sdist", + "wheel", + ] }, + { path = "README.md", format = [ + "sdist", + "wheel", + ] }, + { path = "LICENSE", format = [ + "sdist", + "wheel", + ] }, + { path = "profiles", format = [ + "sdist", + "wheel", + ] }, +] [tool.poetry.dependencies] python = "^3.8.1" From 5e4f6069ec82b25880091753a54122e741bc926a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 16 Apr 2024 16:50:50 +0200 Subject: [PATCH 299/902] refactor(profiles): :truck: move profiles to the folder 'rocrate_profiles' --- pyproject.toml | 2 +- .../ro-crate/may/2_license_entity.ttl | 0 .../ro-crate/must/0_file_descriptor_format.py | 0 .../ro-crate/must/1_file-descriptor_metadata.ttl | 0 .../ro-crate/must/2_root_data_entity_metadata.ttl | 0 .../ro-crate/should/2_root_data_entity_metadata.ttl | 0 rocrate_validator/utils.py | 6 +++--- 7 files changed, 4 insertions(+), 4 deletions(-) rename {profiles => rocrate_profiles}/ro-crate/may/2_license_entity.ttl (100%) rename {profiles => rocrate_profiles}/ro-crate/must/0_file_descriptor_format.py (100%) rename {profiles => rocrate_profiles}/ro-crate/must/1_file-descriptor_metadata.ttl (100%) rename {profiles => rocrate_profiles}/ro-crate/must/2_root_data_entity_metadata.ttl (100%) rename {profiles => rocrate_profiles}/ro-crate/should/2_root_data_entity_metadata.ttl (100%) diff --git a/pyproject.toml b/pyproject.toml index bdc6c8ba..694283f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ include = [ "sdist", "wheel", ] }, - { path = "profiles", format = [ + { path = "rocrate_profiles", format = [ "sdist", "wheel", ] }, diff --git a/profiles/ro-crate/may/2_license_entity.ttl b/rocrate_profiles/ro-crate/may/2_license_entity.ttl similarity index 100% rename from profiles/ro-crate/may/2_license_entity.ttl rename to rocrate_profiles/ro-crate/may/2_license_entity.ttl diff --git a/profiles/ro-crate/must/0_file_descriptor_format.py b/rocrate_profiles/ro-crate/must/0_file_descriptor_format.py similarity index 100% rename from profiles/ro-crate/must/0_file_descriptor_format.py rename to rocrate_profiles/ro-crate/must/0_file_descriptor_format.py diff --git a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_profiles/ro-crate/must/1_file-descriptor_metadata.ttl similarity index 100% rename from profiles/ro-crate/must/1_file-descriptor_metadata.ttl rename to rocrate_profiles/ro-crate/must/1_file-descriptor_metadata.ttl diff --git a/profiles/ro-crate/must/2_root_data_entity_metadata.ttl b/rocrate_profiles/ro-crate/must/2_root_data_entity_metadata.ttl similarity index 100% rename from profiles/ro-crate/must/2_root_data_entity_metadata.ttl rename to rocrate_profiles/ro-crate/must/2_root_data_entity_metadata.ttl diff --git a/profiles/ro-crate/should/2_root_data_entity_metadata.ttl b/rocrate_profiles/ro-crate/should/2_root_data_entity_metadata.ttl similarity index 100% rename from profiles/ro-crate/should/2_root_data_entity_metadata.ttl rename to rocrate_profiles/ro-crate/should/2_root_data_entity_metadata.ttl diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 5ff3b67c..f6a83c6f 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -61,9 +61,9 @@ def get_default_profiles_paths() -> list[Path]: :return: The paths to the profiles directory """ return [ - Path("profiles"), - Path.home() / ".config/rocrate-validator/profiles", - Path(CURRENT_DIR).parent / "profiles" + Path("rocrate_profiles"), + Path.home() / ".config/rocrate-validator/rocrate_profiles", + Path(CURRENT_DIR).parent / "rocrate_profiles" ] From ccb16c5eec0016a83b2ed7bc5533c6c7cbdfb684 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 16 Apr 2024 16:58:52 +0200 Subject: [PATCH 300/902] fix(services): :rotating_light: remove unused imports --- rocrate_validator/services.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 53adfeb2..ae960acf 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -2,12 +2,9 @@ from pathlib import Path from typing import Optional, Union -from pyshacl.pytypes import GraphLike - from .constants import (RDF_SERIALIZATION_FORMATS_TYPES, VALID_INFERENCE_OPTIONS_TYPES) -from .models import (LevelCollection, Profile, RequirementLevel, Severity, - ValidationResult, Validator) +from .models import Profile, Severity, ValidationResult, Validator from .utils import get_profiles_path # set the default profiles path From ec110790aa3f70ffd15ad497f628c7c17cd83f02 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 16 Apr 2024 17:09:21 +0200 Subject: [PATCH 301/902] refactor(shacl): :fire: remove obsolete 'shapes' folder --- shapes/rocrate-shapes.ttl | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 shapes/rocrate-shapes.ttl diff --git a/shapes/rocrate-shapes.ttl b/shapes/rocrate-shapes.ttl deleted file mode 100644 index 9db4460a..00000000 --- a/shapes/rocrate-shapes.ttl +++ /dev/null @@ -1,30 +0,0 @@ -@prefix : <./> . -@prefix dct: . -@prefix rdf: . -@prefix schema_org: . -@prefix sh: . -@prefix xml1: . - -schema_org:CreativeWork a sh:NodeShape ; - sh:targetNode :ro-crate-metadata.json ; - sh:property [ - a sh:PropertyShape ; - sh:name "conformsTo" ; - sh:description "The RO-Crate version this metadata conforms to" ; - sh:maxCount 2; - sh:minCount 1 ; - sh:nodeKind sh:IRI ; - sh:path dct:conformsTo ; - sh:in ( ) - ] ; - sh:property [ - a sh:PropertyShape ; - sh:name "about" ; - sh:description "The main entity described in the RO-Crate" ; - sh:maxCount 1; - sh:minCount 1 ; - sh:nodeKind sh:IRI ; - sh:path schema_org:about ; - sh:hasValue <./> - ] . - From fff682f238dbeb27f6c03cc65ba4fafc1c4944a5 Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 17 Apr 2024 15:59:39 +0200 Subject: [PATCH 302/902] file descriptor metadata: allow multiple conformsTo --- .../must/1_file-descriptor_metadata.ttl | 5 +- .../data/crates/valid/workflow-roc/README.md | 3 + .../data/crates/valid/workflow-roc/blank.png | Bin 0 -> 334 bytes .../crates/valid/workflow-roc/make_crate.py | 44 +++++++ .../valid/workflow-roc/ro-crate-metadata.json | 113 +++++++++++++++++ .../workflow-roc/sort-and-change-case.cwl | 42 +++++++ .../workflow-roc/sort-and-change-case.ga | 118 ++++++++++++++++++ .../profiles/ro-crate/test_valid_ro-crate.py | 9 ++ tests/ro_crates.py | 4 + 9 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 tests/data/crates/valid/workflow-roc/README.md create mode 100644 tests/data/crates/valid/workflow-roc/blank.png create mode 100644 tests/data/crates/valid/workflow-roc/make_crate.py create mode 100644 tests/data/crates/valid/workflow-roc/ro-crate-metadata.json create mode 100644 tests/data/crates/valid/workflow-roc/sort-and-change-case.cwl create mode 100644 tests/data/crates/valid/workflow-roc/sort-and-change-case.ga diff --git a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index 6de4ebdc..82fac42a 100644 --- a/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -52,11 +52,10 @@ sh:property [ a sh:PropertyShape ; sh:name "conformsTo property" ; - sh:description "The RO-Crate specification version that this crate conforms to" ; + sh:description "The specification(s) that the RO-Crate JSON-LD conforms to" ; sh:minCount 1 ; sh:nodeKind sh:IRI ; sh:path dct:conformsTo ; - sh:in ( ) ; + sh:hasValue ; sh:message "The RO-Crate metadata file descriptor MUST have a `conformsTo` property with the RO-Crate specification version" ; ] . - diff --git a/tests/data/crates/valid/workflow-roc/README.md b/tests/data/crates/valid/workflow-roc/README.md new file mode 100644 index 00000000..33fbaf72 --- /dev/null +++ b/tests/data/crates/valid/workflow-roc/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/blank.png b/tests/data/crates/valid/workflow-roc/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/make_crate.py b/tests/data/crates/valid/workflow-roc/make_crate.py new file mode 100644 index 00000000..1143cb4c --- /dev/null +++ b/tests/data/crates/valid/workflow-roc/make_crate.py @@ -0,0 +1,44 @@ +"""\ +(Re)generate the RO-Crate metadata. Requires +https://github.com/ResearchObject/ro-crate-py. +""" + +from pathlib import Path + +from rocrate.rocrate import ROCrate + + +THIS_DIR = Path(__file__).absolute().parent +WF = THIS_DIR / "sort-and-change-case.ga" +CWL_DESC_WF = THIS_DIR / "sort-and-change-case.cwl" +DIAGRAM = "blank.png" +README = "README.md" +WF_LICENSE = "https://spdx.org/licenses/MIT.html" +CRATE_LICENSE = "https://spdx.org/licenses/Apache-2.0.html" + + +def main(): + crate = ROCrate(gen_preview=False) + crate.root_dataset["license"] = CRATE_LICENSE + wf = crate.add_workflow(WF, main=True, lang="galaxy", gen_cwl=True, properties={ + "license": WF_LICENSE, + "name": "sort-and-change-case", + "description": "sort lines and change text to upper case", + }) + cwl_desc_wf = crate.add_workflow(CWL_DESC_WF, main=False, lang="cwl", properties={ + "@type": ["File", "SoftwareSourceCode", "HowTo"], + }) + wf["subjectOf"] = cwl_desc_wf + diagram = crate.add_file(DIAGRAM, properties={ + "@type": ["File", "ImageObject"], + }) + wf["image"] = diagram + readme = crate.add_file(README, properties={ + "encodingFormat": "text/markdown", + }) + readme["about"] = crate.root_dataset + crate.metadata.write(THIS_DIR) + + +if __name__ == "__main__": + main() diff --git a/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json b/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json new file mode 100644 index 00000000..4bb00a12 --- /dev/null +++ b/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json @@ -0,0 +1,113 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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" + } + ], + "license": "https://spdx.org/licenses/Apache-2.0.html", + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@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" + ], + "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" + } + ] +} \ No newline at end of file diff --git a/tests/data/crates/valid/workflow-roc/sort-and-change-case.cwl b/tests/data/crates/valid/workflow-roc/sort-and-change-case.cwl new file mode 100644 index 00000000..679fb292 --- /dev/null +++ b/tests/data/crates/valid/workflow-roc/sort-and-change-case.cwl @@ -0,0 +1,42 @@ +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/sort-and-change-case.ga b/tests/data/crates/valid/workflow-roc/sort-and-change-case.ga new file mode 100644 index 00000000..5a199969 --- /dev/null +++ b/tests/data/crates/valid/workflow-roc/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" +} diff --git a/tests/integration/profiles/ro-crate/test_valid_ro-crate.py b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py index ac8a2ea8..0a07f0d6 100644 --- a/tests/integration/profiles/ro-crate/test_valid_ro-crate.py +++ b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py @@ -37,3 +37,12 @@ def test_valid_roc_required_with_long_datetime(): Severity.REQUIRED, True ) + + +def test_valid_workflow_roc_required(): + """Test a valid RO-Crate.""" + do_entity_test( + ValidROC().workflow_roc, + Severity.REQUIRED, + True + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 7b940ac1..2346c56c 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -24,6 +24,10 @@ def wrroc_paper(self) -> Path: def wrroc_paper_long_date(self) -> Path: return VALID_CRATES_DATA_PATH / "wrroc-paper-long-date" + @property + def workflow_roc(self) -> Path: + return VALID_CRATES_DATA_PATH / "workflow-roc" + class InvalidFileDescriptor: From 2979b07656e3356388e4bc355946cce20e5d6cb1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 18 Apr 2024 10:25:28 +0200 Subject: [PATCH 303/902] refactor(utils): :coffin: revert to a single profiles folder --- rocrate_validator/utils.py | 35 ++--------------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index f6a83c6f..005f199f 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -54,20 +54,7 @@ def get_file_descriptor_path(rocrate_path: Path) -> Path: return Path(rocrate_path) / constants.ROCRATE_METADATA_FILE -def get_default_profiles_paths() -> list[Path]: - """ - Get the paths to the profiles directory - - :return: The paths to the profiles directory - """ - return [ - Path("rocrate_profiles"), - Path.home() / ".config/rocrate-validator/rocrate_profiles", - Path(CURRENT_DIR).parent / "rocrate_profiles" - ] - - -def get_profiles_path(not_exist_ok: bool = True) -> Path: +def get_profiles_path() -> Path: """ Get the path to the profiles directory from the default paths @@ -75,25 +62,7 @@ def get_profiles_path(not_exist_ok: bool = True) -> Path: :return: The path to the profiles directory """ - profiles_path = None - # Get the default profiles paths - default_profiles_paths = get_default_profiles_paths() - logger.debug("Default profiles paths: %r", default_profiles_paths) - for path in default_profiles_paths: - if path.exists(): - profiles_path = path - break - # Check if the profiles directory is found - if not profiles_path: - # If the profiles directory is not found, raise an error - if not not_exist_ok: - # Raise an error if the profiles directory provided with the package is not found - raise errors.ProfilesDirectoryNotFound(profiles_path=str(default_profiles_paths[-1])) - else: - # Use the last path as the profiles directory, i.e., the one provided with the package - profiles_path = default_profiles_paths[-1] - # Return the profiles directory - return profiles_path + return Path(CURRENT_DIR).parent / "rocrate_profiles" def get_format_extension(serialization_format: constants.RDF_SERIALIZATION_FORMATS_TYPES) -> str: From b4cc4438c1fb86005c3cb23384a1836b61de280c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 18 Apr 2024 10:28:17 +0200 Subject: [PATCH 304/902] refactor(profiles): :truck: move profiles to `rocrate_validator/profiles` --- pyproject.toml | 6 +----- .../profiles}/ro-crate/may/2_license_entity.ttl | 0 .../profiles}/ro-crate/must/0_file_descriptor_format.py | 0 .../profiles}/ro-crate/must/1_file-descriptor_metadata.ttl | 0 .../profiles}/ro-crate/must/2_root_data_entity_metadata.ttl | 0 .../ro-crate/should/2_root_data_entity_metadata.ttl | 0 rocrate_validator/utils.py | 2 +- 7 files changed, 2 insertions(+), 6 deletions(-) rename {rocrate_profiles => rocrate_validator/profiles}/ro-crate/may/2_license_entity.ttl (100%) rename {rocrate_profiles => rocrate_validator/profiles}/ro-crate/must/0_file_descriptor_format.py (100%) rename {rocrate_profiles => rocrate_validator/profiles}/ro-crate/must/1_file-descriptor_metadata.ttl (100%) rename {rocrate_profiles => rocrate_validator/profiles}/ro-crate/must/2_root_data_entity_metadata.ttl (100%) rename {rocrate_profiles => rocrate_validator/profiles}/ro-crate/should/2_root_data_entity_metadata.ttl (100%) diff --git a/pyproject.toml b/pyproject.toml index 694283f8..44d7d160 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,11 +17,7 @@ include = [ { path = "LICENSE", format = [ "sdist", "wheel", - ] }, - { path = "rocrate_profiles", format = [ - "sdist", - "wheel", - ] }, + ] } ] [tool.poetry.dependencies] diff --git a/rocrate_profiles/ro-crate/may/2_license_entity.ttl b/rocrate_validator/profiles/ro-crate/may/2_license_entity.ttl similarity index 100% rename from rocrate_profiles/ro-crate/may/2_license_entity.ttl rename to rocrate_validator/profiles/ro-crate/may/2_license_entity.ttl diff --git a/rocrate_profiles/ro-crate/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py similarity index 100% rename from rocrate_profiles/ro-crate/must/0_file_descriptor_format.py rename to rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py diff --git a/rocrate_profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl similarity index 100% rename from rocrate_profiles/ro-crate/must/1_file-descriptor_metadata.ttl rename to rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl diff --git a/rocrate_profiles/ro-crate/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/2_root_data_entity_metadata.ttl similarity index 100% rename from rocrate_profiles/ro-crate/must/2_root_data_entity_metadata.ttl rename to rocrate_validator/profiles/ro-crate/must/2_root_data_entity_metadata.ttl diff --git a/rocrate_profiles/ro-crate/should/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_metadata.ttl similarity index 100% rename from rocrate_profiles/ro-crate/should/2_root_data_entity_metadata.ttl rename to rocrate_validator/profiles/ro-crate/should/2_root_data_entity_metadata.ttl diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 005f199f..7399bfad 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -62,7 +62,7 @@ def get_profiles_path() -> Path: :return: The path to the profiles directory """ - return Path(CURRENT_DIR).parent / "rocrate_profiles" + return Path(CURRENT_DIR) / "profiles" def get_format_extension(serialization_format: constants.RDF_SERIALIZATION_FORMATS_TYPES) -> str: From 54e17ed5653b04c92e4de169d5611261791d05fd Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 22 Apr 2024 16:31:13 +0200 Subject: [PATCH 305/902] constrain the descriptor's about to be the root --- .../must/1_file-descriptor_metadata.ttl | 1 + .../ro-crate-metadata.json | 151 ++++++++++++++++++ .../must/test_file_descriptor_entity.py | 11 ++ tests/ro_crates.py | 4 + 4 files changed, 167 insertions(+) create mode 100644 tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index 82fac42a..b4a5f6ee 100644 --- a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -46,6 +46,7 @@ sh:minCount 1 ; sh:nodeKind sh:IRI ; sh:path schema_org:about ; + sh:hasValue : ; sh:class schema_org:Dataset ; sh:message "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity" ; ] ; diff --git a/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about/ro-crate-metadata.json b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about/ro-crate-metadata.json new file mode 100644 index 00000000..6d2533a5 --- /dev/null +++ b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about/ro-crate-metadata.json @@ -0,0 +1,151 @@ +{ + "@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": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "@type": "license" + }, + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-01-22T15:36:43+00:00", + "description": "RO-Crate for MyWorkflow", + "hasPart": [ + { + "@id": "my-workflow.ga" + }, + { + "@id": "my-workflow-test.yml" + }, + { + "@id": "test-data/" + }, + { + "@id": "README.md" + } + ], + "isBasedOn": "https://github.com/kikkomep/myworkflow", + "license": { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/" + }, + "mainEntity": { + "@id": "my-workflow.ga" + }, + "mentions": [ + { + "@id": "#1d230a09-a465-411a-82bb-d7d4f3f1be02" + } + ], + "name": "MyWorkflow" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "test-data/" + }, + "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/must/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py index 06b197e6..778d702b 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py @@ -45,6 +45,17 @@ def test_missing_entity_about(): ) +def test_invalid_entity_about(): + """Test a RO-Crate with an invalid about property in the file descriptor.""" + do_entity_test( + paths.invalid_entity_about, + models.Severity.REQUIRED, + False, + ["RO-Crate Metadata File Descriptor: recommended properties"], + ["The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity"] + ) + + def test_invalid_entity_about_type(): """Test a RO-Crate with an invalid file descriptor entity type.""" do_entity_test( diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 2346c56c..13642378 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -99,6 +99,10 @@ def invalid_entity_type(self) -> Path: def missing_entity_about(self) -> Path: return self.base_path / "missing_entity_about" + @property + def invalid_entity_about(self) -> Path: + return self.base_path / "invalid_entity_about" + @property def invalid_entity_about_type(self) -> Path: return self.base_path / "invalid_entity_about_type" From 0838a0bbdd291b21e2b67905b36bdbe79f595b9c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Apr 2024 12:37:50 +0200 Subject: [PATCH 306/902] feat(shacl): :zap: improve loading of shacl shapes --- .../requirements/shacl/checks.py | 10 +- .../requirements/shacl/models.py | 253 +++++++++--------- .../requirements/shacl/requirements.py | 21 +- rocrate_validator/requirements/shacl/utils.py | 132 ++++++++- .../requirements/shacl/validator.py | 2 +- 5 files changed, 271 insertions(+), 147 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 66c9ccf0..936146fd 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -1,10 +1,9 @@ - import logging from typing import Optional from rocrate_validator.models import (Requirement, RequirementCheck, ValidationContext) -from rocrate_validator.requirements.shacl.models import ShapeProperty +from rocrate_validator.requirements.shacl.models import Shape, PropertyShape from .validator import SHACLValidator @@ -18,7 +17,7 @@ class SHACLCheck(RequirementCheck): def __init__(self, requirement: Requirement, - shapeProperty: Optional[ShapeProperty] = None) -> None: + shapeProperty: Optional[Shape]) -> None: self._shapeProperty = shapeProperty super().__init__(requirement, shapeProperty.name @@ -27,7 +26,7 @@ def __init__(self, if shapeProperty and shapeProperty.description else None) @property - def shapeProperty(self) -> ShapeProperty: + def shapeProperty(self) -> PropertyShape: return self._shapeProperty def execute_check(self, context: ValidationContext): @@ -35,8 +34,7 @@ def execute_check(self, context: ValidationContext): data_graph = context.validator.data_graph # constraint the shapes graph to the current property shape - shapes_graph = self.shapeProperty.shape_property_graph \ - if self.shapeProperty else self.requirement.shape.shape_graph + shapes_graph = self.shapeProperty.graph shacl_validator = SHACLValidator(shapes_graph=shapes_graph, ont_graph=ontology_graph) result = shacl_validator.validate(data_graph=data_graph, **context.validator.validation_settings) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 73e91164..3f97e63a 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -5,17 +5,17 @@ from pathlib import Path from typing import Optional, Union -from rdflib import RDF, Graph, Namespace, URIRef +from rdflib import Graph, Namespace from rdflib.term import Node from ...constants import SHACL_NS -from .utils import inject_attributes +from .utils import ShapesList, inject_attributes # set up logging logger = logging.getLogger(__name__) -class ShapeProperty: +class PropertyShape: # define default values name: str = None @@ -25,73 +25,33 @@ class ShapeProperty: order: int = 0 def __init__(self, - shape: Shape, - shape_property_node: Node): + node: Node, + graph: Graph, + parent: Optional[Shape] = None): # store the shape - self._shape = shape - # store the node - self._node = shape_property_node - - # create a graph for the shape property - shapes_graph = shape.shapes_graph - shape_graph = Graph() - shape_graph += shapes_graph.triples((shape.node, None, None)) - shape_property_attributes_graph = Graph() - shape_property_attributes_graph += shapes_graph.triples((shape_property_node, None, None)) - # global shape property graph - shape_property_graph = shape_graph + shape_property_attributes_graph - - # remove dangling properties - for s, p, o in shape_property_graph: - logger.debug(f"Processing {p} of property graph {shape_property_node}") - if p == URIRef("http://www.w3.org/ns/shacl#property") and o != shape_property_node: - shape_property_graph.remove((s, p, o)) - # add BNodes - for s, p, o in shape_property_attributes_graph: - shape_property_graph += shapes_graph.triples((o, None, None)) - - # Use the triples method to get all triples that are part of a list - RDF = Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#") - first_predicate = RDF.first - rest_predicate = RDF.rest - shape_property_graph += shapes_graph.triples((None, first_predicate, None)) - shape_property_graph += shapes_graph.triples((None, rest_predicate, None)) - for _, _, object in shape_property_graph: - shape_property_graph += shapes_graph.triples((object, None, None)) - + self._node = node # store the graph - self._shape_property_graph = shape_property_graph - + self._graph = graph + # store the parent shape + self._parent = parent # inject attributes of the shape property to the object - inject_attributes(shape_property_attributes_graph, self) - - @property - def shape(self): - return self._shape + inject_attributes(self, graph, node) @property - def node(self): + def node(self) -> Node: + """Return the node of the shape property""" return self._node @property - def shape_property_graph(self): - return self._shape_property_graph - - def compare_shape(self, other_model): - return self.shape == other_model.shape - - def compare_name(self, other_model): - return self.name == other_model.name - - def __eq__(self, __value: object) -> bool: - if not isinstance(__value, type(self)): - raise ValueError("Invalid comparison") - return self._shape == getattr(__value, '_shape', None)\ - and self._node == getattr(__value, '_node', None) + def graph(self) -> Graph: + """Return the graph of the shape property""" + return self._graph - def __hash__(self): - return hash(self._node) + @property + def parent(self) -> Optional[Shape]: + """Return the parent shape of the shape property""" + return self._parent class Shape: @@ -100,34 +60,15 @@ class Shape: name: str = None description: str = None - def __init__(self, node: Node, shapes_graph: Graph): + def __init__(self, node: Node, graph: Graph): # store the shape node self._node = node # store the shapes graph - self._shapes_graph = shapes_graph - # initialize the properties - self._properties = [] - - # create a graph for the shape - logger.debug("Initializing graph for the shape: %s" % node) - shape_graph = Graph() - shape_graph += shapes_graph.triples((node, None, None)) - # store the graph - self._shape_graph = shape_graph + self._graph = graph # inject attributes of the shape to the object - inject_attributes(shape_graph, self) - - # Initialize the properties - # Define the property predicate - predicate = URIRef(SHACL_NS + "property") - # Use the triples method to get all triples with the particular predicate - first_triples = shape_graph.triples((None, predicate, None)) - # For each triple from the first call, get all triples whose subject is the object of the first triple - for _, _, object in first_triples: - shape_graph += shapes_graph.triples((object, None, None)) - self._properties.append(ShapeProperty(self, object)) + inject_attributes(self, graph, node) @property def node(self): @@ -135,30 +76,9 @@ def node(self): return self._node @property - def shape_graph(self): + def graph(self): """Return the subgraph of the shape""" - return self._shape_graph - - @property - def shapes_graph(self): - """Return the graph of the shapes which contains the shape""" - return self._shapes_graph - - @property - def properties(self): - """Return the properties of the shape""" - return self._properties.copy() - - def get_properties(self) -> list[ShapeProperty]: - """Return the properties of the shape""" - return self._properties.copy() - - def get_property(self, name) -> ShapeProperty: - """Return the property of the shape with the given name""" - for prop in self._properties: - if prop.name == name: - return prop - return None + return self._graph def __str__(self): return f"{self.name}: {self.description}" @@ -174,28 +94,39 @@ def __eq__(self, other): def __hash__(self): return hash(self._node) - @classmethod - def load(cls, shapes_path: Union[str, Path], publicID: Optional[str] = None) -> dict[str, Shape]: - """ - Load the shapes from the graph - """ - shapes_graph = Graph() - shapes_graph.parse(shapes_path, format="turtle", publicID=publicID) - logger.debug("Shapes graph: %s" % shapes_graph) - - # extract shapeNode triples from the shapes_graph - shapeNode = URIRef(SHACL_NS + "NodeShape") - shapes_nodes = shapes_graph.triples((None, RDF.type, shapeNode)) - logger.debug("Shapes nodes: %s" % shapes_nodes) - # create a shape object for each shape node - shapes: dict[str, Shape] = {} - for shape_node, _, _ in shapes_nodes: - logger.debug(f"Processing Shape Node: {shape_node}") - shape = Shape(shape_node, shapes_graph) - if str(shape): - shapes[str(shape)] = shape - return shapes +class NodeShape(Shape): + + def __init__(self, node: Node, graph: Graph, properties: list[PropertyShape] = None): + super().__init__(node, graph) + # store the properties + self._properties = properties if properties else [] + + @property + def properties(self) -> list[PropertyShape]: + """Return the properties of the shape""" + return self._properties.copy() + + def get_property(self, name) -> PropertyShape: + """Return the property of the shape with the given name""" + for prop in self._properties: + if prop.name == name: + return prop + return None + + def add_property(self, property: PropertyShape): + """Add a property to the shape""" + self._properties.append(property) + + def remove_property(self, property: PropertyShape): + """Remove a property from the shape""" + self._properties.remove(property) + + def __str__(self): + return f"NodeShape({self.name})" + + def __repr__(self): + return f"NodeShape({self.name})" class ViolationShape: @@ -252,3 +183,73 @@ def nodeKind(self): if nodeKind: return nodeKind[0]['@id'] return None + + +class ShapesRegistry: + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + self._shapes = {} + + def add_shape(self, shape: Shape): + self._shapes[str(shape)] = shape + + def get_shape(self, name: str) -> Optional[Shape]: + return self._shapes.get(name, None) + + def get_shapes(self) -> dict[str, Shape]: + return self._shapes.copy() + + def load_shapes(self, shapes_path: Union[str, Path], publicID: Optional[str] = None) -> list[Shape]: + """ + Load the shapes from the graph + """ + logger.debug(f"Loading shapes from: {shapes_path}") + # load shapes (nodes and properties) from the shapes graph + shapes_list: ShapesList = ShapesList.load_from_file(shapes_path, publicID) + logger.debug(f"Shapes List: {shapes_list}") + + # list of instantiated shapes + shapes = [] + + # register Node Shapes + for node_shape in shapes_list.node_shapes: + # get the shape graph + node_graph = shapes_list.get_shape_graph(node_shape) + # create a node shape object + shape = NodeShape(node_shape, node_graph) + # load the nested properties + shacl_ns = Namespace(SHACL_NS) + nested_properties = node_graph.objects(subject=node_shape, predicate=shacl_ns.property) + for property_shape in nested_properties: + property_graph = shapes_list.get_shape_property_graph(node_shape, property_shape) + p_shape = PropertyShape( + property_shape, property_graph, node_shape) + shape.add_property(p_shape) + self.add_shape(p_shape) + # store the node shape + self.add_shape(shape) + shapes.append(shape) + + # register Property Shapes + for property_shape in shapes_list.property_shapes: + shape = PropertyShape(property_shape, shapes_list.get_shape_graph(property_shape)) + self.add_shape(shape) + shapes.append(shape) + + return shapes + + def __str__(self): + return f"ShapesRegistry: {self._shapes}" + + def __repr__(self): + return f"ShapesRegistry({self._shapes})" + + @classmethod + def get_instance(cls): + return cls() diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index 40cc84e9..9149638b 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -4,7 +4,7 @@ from ...models import Profile, Requirement, RequirementCheck, RequirementLevel from .checks import SHACLCheck -from .models import Shape +from .models import Shape, ShapesRegistry # set up logging logger = logging.getLogger(__name__) @@ -30,15 +30,17 @@ def __init__(self, def __init_checks__(self) -> list[RequirementCheck]: # assign a check to each property of the shape checks = [] - for prop in self._shape.get_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) + # create a check for each property if the shape has nested properties + if hasattr(self.shape, "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 if len(checks) == 0: - checks.append(SHACLCheck(self)) + checks.append(SHACLCheck(self, self.shape)) return checks @property @@ -48,9 +50,10 @@ def shape(self) -> Shape: @staticmethod def load(profile: Profile, requirement_level: RequirementLevel, file_path: Path, publicID: Optional[str] = None) -> list[Requirement]: - shapes: dict[str, Shape] = Shape.load(file_path, publicID=publicID) + assert file_path is not None, "The file path cannot be None" + shapes: list[Shape] = ShapesRegistry.get_instance().load_shapes(file_path, publicID) logger.debug("Loaded %s shapes: %s", len(shapes), shapes) requirements = [] - for shape in shapes.values(): + for shape in shapes: requirements.append(SHACLRequirement(requirement_level, shape, profile, file_path)) return requirements diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index a169f368..250e9602 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import logging -from rdflib import Graph, Namespace +from rdflib import RDF, BNode, Graph, Namespace from rdflib.term import Node -from rocrate_validator.constants import RDF_SYNTAX_NS +from rocrate_validator.constants import RDF_SYNTAX_NS, SHACL_NS # set up logging logger = logging.getLogger(__name__) @@ -30,15 +32,135 @@ def build_node_subgraph(graph: Graph, node: Node) -> Graph: return shape_graph -def inject_attributes(node_graph: Graph, obj: object) -> object: +def inject_attributes(obj: object, node_graph: Graph, node: Node) -> object: # inject attributes of the shape property - for node, p, o in node_graph: + for node, p, o in node_graph.triples((node, None, None)): predicate_as_string = p.toPython() logger.debug(f"Processing {predicate_as_string} of property graph {node}") - if predicate_as_string.startswith("http://www.w3.org/ns/shacl#"): + if predicate_as_string.startswith(SHACL_NS): property_name = predicate_as_string.split("#")[-1] setattr(obj, property_name, o.toPython()) logger.debug("Injecting attribute %s: %s", property_name, o.toPython()) # return the object return obj + + +class ShapesList: + def __init__(self, + node_shapes: list[Node], + property_shapes: list[Node], + shapes_graphs: dict[Node, Graph]): + self._shapes_graphs = shapes_graphs + self._node_shapes = node_shapes + self._property_shapes = property_shapes + + @property + def node_shapes(self) -> list[Node]: + return self._node_shapes.copy() + + @property + def property_shapes(self) -> list[Node]: + return self._property_shapes.copy() + + @property + def shapes(self) -> list[Node]: + return self._node_shapes + self._property_shapes + + def get_shape_graph(self, shape_node: Node) -> Graph: + return self._shapes_graphs[shape_node] + + def get_shape_property_graph(self, shape_node: Node, shape_property: Node) -> Graph: + node_graph = self.get_shape_graph(shape_node) + assert node_graph is not None, "The shape graph cannot be None" + + property_graph = Graph() + shacl_ns = Namespace(SHACL_NS) + nested_properties_to_exclude = [o for (_, _, o) in node_graph.triples( + (shape_node, shacl_ns.property, None)) if o != shape_property] + triples_to_exclude = [(s, _, o) for (s, _, o) in node_graph.triples((None, None, None)) + if s in nested_properties_to_exclude + or o in nested_properties_to_exclude] + + property_graph += node_graph - triples_to_exclude + + return property_graph + + @classmethod + def load_from_file(cls, file_path: str, publicID: str = None) -> ShapesList: + """ + Load the shapes from the file + + :param file_path: the path to the file containing the shapes + :param publicID: the public ID to use + + :return: the list of shapes + """ + return load_shapes_from_file(file_path, publicID) + + @classmethod + def load_from_graph(cls, graph: Graph) -> ShapesList: + """ + Load the shapes from the graph + + :param graph: the graph containing the shapes + :param target_node: the target node to extract the shapes from + + :return: the list of shapes + """ + return load_shapes_from_graph(graph) + + +def __extract_related_triples__(graph, subject_node): + """ + Recursively extract all triples related to a given shape. + """ + related_triples = [] + # Directly related triples + related_triples.extend((_, p, o) for (_, p, o) in graph.triples((subject_node, None, None))) + + # Recursively find triples related to nested shapes + for _, _, object_node in related_triples: + if isinstance(object_node, Node): + related_triples.extend(__extract_related_triples__(graph, object_node)) + + return related_triples + + +def load_shapes_from_file(file_path: str, publicID: str = None) -> ShapesList: + # 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) + # Extract the shapes from the graph + return load_shapes_from_graph(g) + + +def load_shapes_from_graph(g: Graph) -> ShapesList: + # define the SHACL namespace + SHACL = Namespace(SHACL_NS) + # find all NodeShapes + node_shapes = [s for (s, _, _) in g.triples( + (None, RDF.type, SHACL.NodeShape)) if not isinstance(s, BNode)] + logger.debug("Loaded Node Shapes: %s", node_shapes) + # find all PropertyShapes + property_shapes = [s for (s, _, _) in g.triples((None, RDF.type, SHACL.PropertyShape)) + if not isinstance(s, BNode)] + logger.debug("Loaded Property Shapes: %s", property_shapes) + # define the list of shapes to extract + shapes = node_shapes + property_shapes + + # Split the graph into subgraphs for each shape + subgraphs = {} + count = 0 + for shape in shapes: + count += 1 + subgraph = Graph() + # Extract all related triples for the current shape + related_triples = __extract_related_triples__(g, shape) + for s, p, o in related_triples: + subgraph.add((s, p, o)) + subgraphs[shape] = subgraph + + return ShapesList(node_shapes, property_shapes, subgraphs) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index d9646e59..64fe13bb 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -10,7 +10,7 @@ from rdflib import Graph from rdflib.term import Node, URIRef -from rocrate_validator.models import Severity +from rocrate_validator.models import Severity, ValidationResult from ...constants import (RDF_SERIALIZATION_FORMATS, RDF_SERIALIZATION_FORMATS_TYPES, SHACL_NS, From 2bdbe09677fca0221b3f5c449d849be0835e3333 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Apr 2024 12:39:04 +0200 Subject: [PATCH 307/902] feat(shacl): :sparkles: advanced feature should be enabled by default --- rocrate_validator/services.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index ae960acf..2c244389 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -20,7 +20,6 @@ def validate( profile_name: str = "ro-crate", inherit_profiles: bool = True, ontologies_path: Optional[Path] = None, - advanced: Optional[bool] = False, inference: Optional[VALID_INFERENCE_OPTIONS_TYPES] = None, inplace: Optional[bool] = False, abort_on_first: Optional[bool] = True, @@ -45,7 +44,7 @@ def validate( profile_name=profile_name, inherit_profiles=inherit_profiles, ontologies_path=ontologies_path, - advanced=advanced, + advanced=True, inference=inference, inplace=inplace, abort_on_first=abort_on_first, From f83c0c337b7322714f5be7d92d18f1df57b89408 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Apr 2024 12:41:25 +0200 Subject: [PATCH 308/902] fix(shacl): :bug: link property shape property wrapper to the parent node wrapper --- rocrate_validator/requirements/shacl/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 3f97e63a..8fc99e41 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -229,7 +229,7 @@ def load_shapes(self, shapes_path: Union[str, Path], publicID: Optional[str] = N for property_shape in nested_properties: property_graph = shapes_list.get_shape_property_graph(node_shape, property_shape) p_shape = PropertyShape( - property_shape, property_graph, node_shape) + property_shape, property_graph, shape) shape.add_property(p_shape) self.add_shape(p_shape) # store the node shape From 0a60862d5141077f4fec1b86bf59819f149bb19a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Apr 2024 12:47:27 +0200 Subject: [PATCH 309/902] refactor(shacl): :truck: rename shape object associated with the SHACLCheck --- .../requirements/shacl/checks.py | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 936146fd..83dcaa41 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -3,7 +3,7 @@ from rocrate_validator.models import (Requirement, RequirementCheck, ValidationContext) -from rocrate_validator.requirements.shacl.models import Shape, PropertyShape +from rocrate_validator.requirements.shacl.models import Shape from .validator import SHACLValidator @@ -17,28 +17,27 @@ class SHACLCheck(RequirementCheck): def __init__(self, requirement: Requirement, - shapeProperty: Optional[Shape]) -> None: - self._shapeProperty = shapeProperty + shape: Optional[Shape]) -> None: + self._shape = shape super().__init__(requirement, - shapeProperty.name - if shapeProperty and shapeProperty.name else None, - shapeProperty.description - if shapeProperty and shapeProperty.description else None) + shape.name + if shape and shape.name else None, + shape.description + if shape and shape.description else None) @property - def shapeProperty(self) -> PropertyShape: - return self._shapeProperty + def shape(self) -> Shape: + return self._shape def execute_check(self, context: ValidationContext): + # set up the input data for the validator ontology_graph = context.validator.ontologies_graph data_graph = context.validator.data_graph - - # constraint the shapes graph to the current property shape - shapes_graph = self.shapeProperty.graph - + shapes_graph = self.shape.graph + # validate the data graph shacl_validator = SHACLValidator(shapes_graph=shapes_graph, ont_graph=ontology_graph) result = shacl_validator.validate(data_graph=data_graph, **context.validator.validation_settings) - + # parse the validation result logger.debug("Validation '%s' conforms: %s", self.name, result.conforms) if not result.conforms: logger.debug("Validation failed") @@ -53,18 +52,18 @@ def execute_check(self, context: ValidationContext): return True def __str__(self) -> str: - return super().__str__() + (f" - {self._shapeProperty}" if self._shapeProperty else "") + return super().__str__() + (f" - {self._shape}" if self._shape else "") def __repr__(self) -> str: - return super().__repr__() + (f" - {self._shapeProperty}" if self._shapeProperty else "") + return super().__repr__() + (f" - {self._shape}" if self._shape else "") def __eq__(self, __value: object) -> bool: if not isinstance(__value, type(self)): return NotImplemented - return super().__eq__(__value) and self._shapeProperty == getattr(__value, '_shapeProperty', None) + return super().__eq__(__value) and self._shape == getattr(__value, '_shape', None) def __hash__(self) -> int: - return super().__hash__() + (hash(self._shapeProperty) if self._shapeProperty else 0) + return super().__hash__() + (hash(self._shape) if self._shape else 0) # ------------ Dead code? ------------ # @property From ecf6e3251dc2301f0402d5de4f28afca1735ecf0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Apr 2024 15:33:35 +0200 Subject: [PATCH 310/902] fix(shacl): :bug: fix reference to onto graph --- rocrate_validator/requirements/shacl/checks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 83dcaa41..9b43f007 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -36,7 +36,8 @@ def execute_check(self, context: ValidationContext): shapes_graph = self.shape.graph # validate the data graph shacl_validator = SHACLValidator(shapes_graph=shapes_graph, ont_graph=ontology_graph) - result = shacl_validator.validate(data_graph=data_graph, **context.validator.validation_settings) + result = shacl_validator.validate( + data_graph=data_graph, ontology_graph=ontology_graph, **context.validator.validation_settings) # parse the validation result logger.debug("Validation '%s' conforms: %s", self.name, result.conforms) if not result.conforms: From ab64c381794eaafc52472f1e7ca9fe7cfc5b793a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Apr 2024 15:37:52 +0200 Subject: [PATCH 311/902] fix(shacl): --- rocrate_validator/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 9e69b223..89cfb15f 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -740,7 +740,6 @@ def __init__(self, self.requirement_severity = requirement_severity self.requirement_severity_only = requirement_severity_only self.disable_profile_inheritance = disable_profile_inheritance - self.ontologies_path = ontologies_path self._validation_settings: dict[str, BaseTypes] = { 'advanced': advanced, @@ -759,6 +758,8 @@ def __init__(self, self._data_graph = None # reference to the profile self._profile = None + # reference to the path of the ontologies + self._ontologies_path = None # reference to the graph of shapes self._ontologies_graph = None @@ -813,8 +814,8 @@ def publicID(self) -> str: def load_ontologies_graph(self): # load the graph of ontologies ontologies_graph = Graph() - if self.ontologies_path: - ontologies_graph.parse(self.ontologies_path, format="ttl", + if self._ontologies_path: + ontologies_graph.parse(self._ontologies_path, format="ttl", publicID=self.publicID) return ontologies_graph From 5d084ca9adbaed0d99d5e77c6aacc3d313db483e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Apr 2024 15:39:38 +0200 Subject: [PATCH 312/902] fix(shacl): :construction: workaround to resolve ex: prefix on sh:select strings --- rocrate_validator/requirements/shacl/checks.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 9b43f007..5b23feb8 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -1,6 +1,10 @@ import logging +import os from typing import Optional +from rdflib import Literal, Namespace + +from rocrate_validator.constants import SHACL_NS from rocrate_validator.models import (Requirement, RequirementCheck, ValidationContext) from rocrate_validator.requirements.shacl.models import Shape @@ -34,6 +38,17 @@ def execute_check(self, context: ValidationContext): ontology_graph = context.validator.ontologies_graph data_graph = context.validator.data_graph shapes_graph = self.shape.graph + + # temporary fix to replace the ex: prefix with the rocrate path + if os.path.isdir(context.validator.rocrate_path): + shacl_ns = Namespace(SHACL_NS) + selects = shapes_graph.triples((None, shacl_ns.select, None)) + for s, p, o in selects: + shapes_graph.remove((s, p, o)) + # FIXME: write a better regex ?? + updated_node_value = str(o).replace("ex:", f"") + shapes_graph.add((s, p, Literal(updated_node_value))) + # validate the data graph shacl_validator = SHACLValidator(shapes_graph=shapes_graph, ont_graph=ontology_graph) result = shacl_validator.validate( From b4a7c2df2b0d04865ac7d01bcb7ab2fb46987fea Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 15:40:19 +0200 Subject: [PATCH 313/902] fix(shacl): :bug: singleton pattern --- rocrate_validator/requirements/shacl/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 8fc99e41..14c79ae2 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -252,4 +252,6 @@ def __repr__(self): @classmethod def get_instance(cls): - return cls() + if not cls._instance: + cls._instance = cls() + return cls._instance From 14d8f3d5123caffb7d05ad67717d1ed18b5f3001 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 15:42:51 +0200 Subject: [PATCH 314/902] feat(utils): :sparkles: add a recursive method to compute the deep hash of a shape object --- rocrate_validator/requirements/shacl/utils.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index 250e9602..998e23bc 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -46,6 +46,41 @@ def inject_attributes(obj: object, node_graph: Graph, node: Node) -> object: return obj +def __compute_values__(g: Graph, s: Node) -> list[tuple]: + """ + Compute the values of the triples in the graph (excluding BNodes) + starting from the given subject node `s`. + """ + + # Collect the values of the triples in the graph (excluding BNodes) + values = [] + # Assuming the list of triples values is stored in a variable called 'triples_values' + triples_values = list([(_, x, _) for (_, x, _) in g.triples((s, None, None)) if x != RDF.type]) + + for (s, p, o) in triples_values: + if isinstance(o, BNode): + values.extend(__compute_values__(g, o)) + else: + values.append((s, p, o) if not isinstance(s, BNode) else (p, o)) + return values + + +def compute_hash(g: Graph, s: Node): + """ + Compute the hash of the triples in the graph (excluding BNodes) + starting from the given subject node `s`. + """ + + # Collect the values of the triples in the graph (excluding BNodes) + triples_values = sorted(__compute_values__(g, s)) + # Convert the list of triples values to a string representation + triples_string = str(triples_values) + # Calculate the hash of the triples string + hash_value = hashlib.sha256(triples_string.encode()).hexdigest() + # Return the hash value + return hash_value + + class ShapesList: def __init__(self, node_shapes: list[Node], From 903067143515f3111249777d6d91758ea64afc40 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 15:46:21 +0200 Subject: [PATCH 315/902] refactor(shacl): :truck: move severity map function Move the function that maps severity values to enum objects to the 'utils' directory --- rocrate_validator/requirements/shacl/utils.py | 14 ++++++++++++++ rocrate_validator/requirements/shacl/validator.py | 6 ++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index 998e23bc..9aae8a9a 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -32,6 +32,20 @@ def build_node_subgraph(graph: Graph, node: Node) -> Graph: return shape_graph +def map_severity(shacl_severity: str) -> Severity: + """ + Map the SHACL severity term to our Severity enum values + """ + if f"{SHACL_NS}Violation" == shacl_severity: + return Severity.REQUIRED + elif f"{SHACL_NS}Warning" == shacl_severity: + return Severity.RECOMMENDED + elif f"{SHACL_NS}Info" == shacl_severity: + return Severity.OPTIONAL + else: + raise RuntimeError(f"Unrecognized SHACL severity term {shacl_severity}") + + def inject_attributes(obj: object, node_graph: Graph, node: Node) -> object: # inject attributes of the shape property for node, p, o in node_graph.triples((node, None, None)): diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 64fe13bb..c2fbbafc 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -85,10 +85,8 @@ def resultPath(self): @property def value(self): - value = self._violation_json.get(f'{SHACL_NS}value', None) - if not value: - return None - return value[0]['@id'] + # we need to map the SHACL severity term to our Severity enum values + self._severity = map_severity(severity.toPython()) @property def sourceConstraintComponent(self): From 633c1a98e751c8b1ad217e64bd0f6bef9e73aa28 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 15:48:14 +0200 Subject: [PATCH 316/902] refactor(shacl): :truck: move to utils the function for making relative URIs --- rocrate_validator/requirements/shacl/utils.py | 5 +++++ rocrate_validator/requirements/shacl/validator.py | 10 +--------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index 9aae8a9a..1c834032 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -46,6 +46,11 @@ def map_severity(shacl_severity: str) -> Severity: raise RuntimeError(f"Unrecognized SHACL severity term {shacl_severity}") +def make_uris_relative(text: str, ro_crate_path: Union[Path, str]) -> str: + # globally replace the string "file://" with "./ + return text.replace(f'file://{ro_crate_path}', '.') + + def inject_attributes(obj: object, node_graph: Graph, node: Node) -> object: # inject attributes of the shape property for node, p, o in node_graph.triples((node, None, None)): diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index c2fbbafc..87e9d3e5 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -92,15 +92,7 @@ def value(self): def sourceConstraintComponent(self): return self._violation_json[f'{SHACL_NS}sourceConstraintComponent'][0]['@id'] - @property - def sourceShape(self) -> ViolationShape: - try: - return ViolationShape(self.source_shape_node, self._graph) - except Exception as e: - logger.error("Error getting source shape: %s" % e) - if logger.isEnabledFor(logging.DEBUG): - logger.exception(e) - return None + self._result_message = make_uris_relative(message.toPython(), ro_crate_path) @property def description(self): From 1e142f785eccf6983c3bf02f724c298bcffff1e7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 15:49:26 +0200 Subject: [PATCH 317/902] fix(logging): :loud_sound: update log message --- rocrate_validator/requirements/shacl/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index 1c834032..0f70b01b 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -59,7 +59,7 @@ def inject_attributes(obj: object, node_graph: Graph, node: Node) -> object: if predicate_as_string.startswith(SHACL_NS): property_name = predicate_as_string.split("#")[-1] setattr(obj, property_name, o.toPython()) - logger.debug("Injecting attribute %s: %s", property_name, o.toPython()) + logger.debug("Injected attribute %s: %s", property_name, o.toPython()) # return the object return obj From be6f8b466f8c23fd7915bc6cd16fe03014beb30f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 15:49:49 +0200 Subject: [PATCH 318/902] refactor(utils): :recycle: reorganize imports --- rocrate_validator/requirements/shacl/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index 0f70b01b..0710eaee 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -1,11 +1,15 @@ from __future__ import annotations +import hashlib import logging +from pathlib import Path +from typing import Union from rdflib import RDF, BNode, Graph, Namespace from rdflib.term import Node from rocrate_validator.constants import RDF_SYNTAX_NS, SHACL_NS +from rocrate_validator.models import Severity # set up logging logger = logging.getLogger(__name__) From 7d5bc0c92c250151bafa1904b72ef5b814d4d64b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 15:51:42 +0200 Subject: [PATCH 319/902] refactor(shacl): :recycle: restructure the hierarchy of SHACL Shape objects --- .../requirements/shacl/models.py | 97 ++++++++++--------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 14c79ae2..665e3198 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -1,6 +1,5 @@ from __future__ import annotations -import json import logging from pathlib import Path from typing import Optional, Union @@ -9,51 +8,12 @@ from rdflib.term import Node from ...constants import SHACL_NS -from .utils import ShapesList, inject_attributes +from .utils import ShapesList, compute_hash, inject_attributes # set up logging logger = logging.getLogger(__name__) -class PropertyShape: - - # define default values - name: str = None - description: str = None - group: str = None - defaultValue: str = None - order: int = 0 - - def __init__(self, - node: Node, - graph: Graph, - parent: Optional[Shape] = None): - - # store the shape - self._node = node - # store the graph - self._graph = graph - # store the parent shape - self._parent = parent - # inject attributes of the shape property to the object - inject_attributes(self, graph, node) - - @property - def node(self) -> Node: - """Return the node of the shape property""" - return self._node - - @property - def graph(self) -> Graph: - """Return the graph of the shape property""" - return self._graph - - @property - def parent(self) -> Optional[Shape]: - """Return the parent shape of the shape property""" - return self._parent - - class Shape: # define default values @@ -66,6 +26,8 @@ def __init__(self, node: Node, graph: Graph): self._node = node # store the shapes graph self._graph = graph + # cache the hash + self._hash = None # inject attributes of the shape to the object inject_attributes(self, graph, node) @@ -81,10 +43,18 @@ def graph(self): return self._graph def __str__(self): - return f"{self.name}: {self.description}" + class_name = self.__class__.__name__ + if self.name and self.description: + return f"{class_name} - {self.name}: {self.description} ({hash(self)})" + elif self.name: + return f"{class_name} - {self.name} ({hash(self)})" + elif self.description: + return f"{class_name} - {self.description} ({hash(self)})" + else: + return f"{class_name} ({hash(self)})" def __repr__(self): - return f"Shape({self.name})" + return f"{ self.__class__.__name__}({hash(self)})" def __eq__(self, other): if not isinstance(other, Shape): @@ -92,7 +62,44 @@ def __eq__(self, other): return self._node == other._node def __hash__(self): - return hash(self._node) + if self._hash is None: + shape_hash = compute_hash(self.graph, self.node) + self._hash = hash(shape_hash) + return self._hash + + +class PropertyShape(Shape): + + # define default values + name: str = None + description: str = None + group: str = None + defaultValue: str = None + order: int = 0 + + def __init__(self, + node: Node, + graph: Graph, + parent: Optional[Shape] = None): + # call the parent constructor + super().__init__(node, graph) + # store the parent shape + self._parent = parent + + @property + def node(self) -> Node: + """Return the node of the shape property""" + return self._node + + @property + def graph(self) -> Graph: + """Return the graph of the shape property""" + return self._graph + + @property + def parent(self) -> Optional[Shape]: + """Return the parent shape of the shape property""" + return self._parent class NodeShape(Shape): @@ -122,8 +129,6 @@ def remove_property(self, property: PropertyShape): """Remove a property from the shape""" self._properties.remove(property) - def __str__(self): - return f"NodeShape({self.name})" def __repr__(self): return f"NodeShape({self.name})" From 569bb2c6de5c13f4ba5f81de490722939eee5635 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 15:52:35 +0200 Subject: [PATCH 320/902] feat(shacl): :sparkles: allow to retrive shapes by hash --- rocrate_validator/requirements/shacl/models.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 665e3198..2f3ccc11 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -202,10 +202,20 @@ def __init__(self): self._shapes = {} def add_shape(self, shape: Shape): - self._shapes[str(shape)] = shape - - def get_shape(self, name: str) -> Optional[Shape]: - return self._shapes.get(name, None) + assert isinstance(shape, Shape), "Invalid shape" + logger.debug("Adding shape: %s", shape) + self._shapes[f"{hash(shape)}"] = shape + logger.debug("Added shapes: %r", self._shapes.keys()) + + def get_shape(self, hash_value: int) -> Optional[Shape]: + logger.debug("Getting shape with hash: %s from %r", hash_value, list(self._shapes.keys())) + return self._shapes.get(f"{hash_value}", None) + + def get_shape_by_name(self, name: str) -> Optional[Shape]: + for shape in self._shapes.values(): + if shape.name == name: + return shape + return None def get_shapes(self) -> dict[str, Shape]: return self._shapes.copy() From 63f058bf0aa5cdfcee9fa8786be40c6a71918b2c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 15:53:19 +0200 Subject: [PATCH 321/902] refactor(shacl): :fire: remove dead code --- .../requirements/shacl/models.py | 60 ------------------- .../requirements/shacl/validator.py | 18 ------ 2 files changed, 78 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 2f3ccc11..b0beb223 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -130,66 +130,6 @@ def remove_property(self, property: PropertyShape): self._properties.remove(property) - def __repr__(self): - return f"NodeShape({self.name})" - - -class ViolationShape: - - def __init__(self, shape_node: Node, graph: Graph) -> None: - # check the input - assert isinstance(shape_node, Node), "Invalid shape node" - assert isinstance(graph, Graph), "Invalid graph" - - # store the input - self._shape_node = shape_node - self._graph = graph - - # create a graph for the shape - shape_graph = Graph() - shape_graph += graph.triples((shape_node, None, None)) - self.shape_graph = shape_graph - - # serialize the graph in json-ld - shape_json = shape_graph.serialize(format="json-ld") - shape_obj = json.loads(shape_json) - logger.debug("Shape JSON: %s" % shape_obj) - try: - self.shape_json = shape_obj[0] - except Exception as e: - logger.error("Error parsing shape JSON: %s" % e) - # if logger.isEnabledFor(logging.DEBUG): - # logger.exception(e) - # raise e - - @property - def node(self) -> Node: - return self._shape_node - - @property - def graph(self) -> Graph: - return self._graph - - @property - def name(self): - return self.shape_json[f'{SHACL_NS}name'][0]['@value'] - - @property - def description(self): - return self.shape_json[f'{SHACL_NS}description'][0]['@value'] - - @property - def path(self): - return self.shape_json[f'{SHACL_NS}path'][0]['@id'] - - @property - def nodeKind(self): - nodeKind = self.shape_json.get(f'{SHACL_NS}nodeKind', None) - if nodeKind: - return nodeKind[0]['@id'] - return None - - class ShapesRegistry: _instance = None diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 87e9d3e5..82796f83 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -163,24 +163,6 @@ def violations(self) -> list[SHACLViolation]: def text(self) -> str: return self._text - # ------------ Dead code? ------------ - # @staticmethod - # def from_serialized_results_graph(file_path: str, format: str = 'turtle'): - # # check the input - # assert format in ['turtle', 'n3', 'nt', - # 'xml', 'rdf', 'json-ld'], "Invalid format" - # assert file_path, "Invalid file path" - # assert os.path.exists(file_path), "File does not exist" - # # Load the graph - # logger.debug("Loading graph from file: %s" % file_path) - # g = Graph() - # _ = g.parse(file_path, format=format) - # logger.debug("Graph loaded from file: %s" % file_path) - - # # return the validation result - # assert False, "missing Validator argument to constructor call" - # return ValidationResult(g) - class SHACLValidator: From 1f68280a56f64a3890ba51f2b991635b006e176b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 15:56:20 +0200 Subject: [PATCH 322/902] refactor(shacl): :recycle: refactor parsing of SHACL violation with lazy loading strategy --- .../requirements/shacl/validator.py | 109 ++++++++---------- 1 file changed, 51 insertions(+), 58 deletions(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 82796f83..ba5544d9 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -1,6 +1,5 @@ from __future__ import annotations -import json import logging from pathlib import Path from typing import Optional, Union @@ -11,12 +10,14 @@ from rdflib.term import Node, URIRef from rocrate_validator.models import Severity, ValidationResult +from rocrate_validator.requirements.shacl.utils import (make_uris_relative, + map_severity) from ...constants import (RDF_SERIALIZATION_FORMATS, RDF_SERIALIZATION_FORMATS_TYPES, SHACL_NS, VALID_INFERENCE_OPTIONS, VALID_INFERENCE_OPTIONS_TYPES) -from .models import ViolationShape +from .models import PropertyShape # set up logging logger = logging.getLogger(__name__) @@ -26,46 +27,24 @@ class SHACLViolation: def __init__(self, result: ValidationResult, violation_node: Node, graph: Graph) -> None: # check the input + assert result is not None, "Invalid result" assert isinstance(violation_node, Node), "Invalid violation node" assert isinstance(graph, Graph), "Invalid graph" # store the input + self._result = result self._violation_node = violation_node self._graph = graph - # store the result object - self._result = result - - # create a graph for the violation - violation_graph = Graph() - violation_graph += graph.triples((violation_node, None, None)) - self.violation_graph = violation_graph - - # serialize the graph in json-ld - violation_obj = json.loads(violation_graph.serialize(format="json-ld")) - self._violation_json = violation_obj[0] - - # get the source shape - shapes = list(graph.triples( - (violation_node, URIRef(f"{SHACL_NS}sourceShape"), None))) - self.source_shape_node = shapes[0][2] - - def get_result_message(self, ro_crate_path: Union[Path, str]) -> str: - return self._make_uris_relative( - self._violation_json[f'{SHACL_NS}resultMessage'][0]['@value'], - ro_crate_path) - - def get_result_severity(self) -> Severity: - shacl_severity = self._violation_json[f'{SHACL_NS}resultSeverity'][0]['@id'] - # we need to map the SHACL severity term to our Severity enum values - if 'http://www.w3.org/ns/shacl#Violation' == shacl_severity: - return Severity.REQUIRED - elif 'http://www.w3.org/ns/shacl#Warning' == shacl_severity: - return Severity.RECOMMENDED - elif 'http://www.w3.org/ns/shacl#Info' == shacl_severity: - return Severity.OPTIONAL - else: - raise RuntimeError(f"Unrecognized SHACL severity term {shacl_severity}") + # initialize the properties for lazy loading + self._focus_node = None + self._result_message = None + self._result_path = None + self._severity = None + self._source_constraint_component = None + self._source_shape = None + self._source_shape_node = None + self._value = None @property def node(self) -> Node: @@ -76,32 +55,56 @@ def graph(self) -> Graph: return self._graph @property - def focusNode(self): - return self._violation_json[f'{SHACL_NS}focusNode'][0]['@id'] + def focusNode(self) -> Node: + if not self._focus_node: + self._focus_node = self.graph.value(self._violation_node, URIRef(f"{SHACL_NS}sourceShape")) + assert self._focus_node is not None, f"Unable to get focus node from violation node {self._violation_node}" + return self._focus_node @property def resultPath(self): - return self._violation_json[f'{SHACL_NS}resultPath'][0]['@id'] + if not self._result_path: + self._result_path = self.graph.value(self._violation_node, URIRef(f"{SHACL_NS}resultPath")) + assert self._result_path is not None, f"Unable to get result path from violation node {self._violation_node}" + return self._result_path @property def value(self): + if not self._value: + self._value = self.graph.value(self._violation_node, URIRef(f"{SHACL_NS}value")) + assert self._value is not None, f"Unable to get value from violation node {self._violation_node}" + return self._value + + def get_result_severity(self) -> Severity: + if not self._severity: + severity = self.graph.value(self._violation_node, URIRef(f"{SHACL_NS}resultSeverity")) + assert severity is not None, f"Unable to get severity from violation node {self._violation_node}" # we need to map the SHACL severity term to our Severity enum values self._severity = map_severity(severity.toPython()) + return self._severity @property def sourceConstraintComponent(self): - return self._violation_json[f'{SHACL_NS}sourceConstraintComponent'][0]['@id'] + if not self._source_constraint_component: + self._source_constraint_component = self.graph.value( + self._violation_node, URIRef(f"{SHACL_NS}sourceConstraintComponent")) + assert self._source_constraint_component is not None, f"Unable to get source constraint component from violation node {self._violation_node}" + return self._source_constraint_component + def get_result_message(self, ro_crate_path: Union[Path, str]) -> str: + if not self._result_message: + message = self.graph.value(self._violation_node, URIRef(f"{SHACL_NS}resultMessage")) + assert message is not None, f"Unable to get result message from violation node {self._violation_node}" self._result_message = make_uris_relative(message.toPython(), ro_crate_path) + return self._result_message @property - def description(self): - return self.sourceShape.description - - @staticmethod - def _make_uris_relative(text: str, ro_crate_path: Union[Path, str]) -> str: - # globally replace the string "file://" with "./ - return text.replace(f'file://{ro_crate_path}', '.') + def sourceShape(self) -> PropertyShape: + if not self._source_shape_node: + self._source_shape_node = self.graph.value(self._violation_node, URIRef(f"{SHACL_NS}sourceShape")) + assert self._source_shape_node is not None, f"Unable to get source shape node from violation node {self._violation_node}" + self._source_shape = PropertyShape(self._source_shape_node, self.graph) + return self._source_shape class SHACLValidationResult: @@ -133,19 +136,9 @@ def __init__(self, results_graph: Graph, assert self._conforms == (len(self._violations) == 0), "Invalid validation result" def _parse_results_graph(self, results_graph: Graph): - # Query for validation results - query = """ - SELECT ?subject - WHERE {{ - ?subject a <{0}ValidationResult> . - }} - """.format(SHACL_NS) - - query_results = results_graph.query(query) - + # parse the violations from the results graph violations = [] - for r in query_results: - violation_node = r[0] + for violation_node in results_graph.subjects(predicate=URIRef(f"{SHACL_NS}resultMessage")): violation = SHACLViolation(self, violation_node, results_graph) violations.append(violation) From 62c885a96ae30e57fbca94bb043b59ae02be161d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 15:59:21 +0200 Subject: [PATCH 323/902] feat(shacl): :sparkles: allow to retrieve check instances by shape hash --- rocrate_validator/requirements/shacl/checks.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 5b23feb8..545359a2 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -19,15 +19,21 @@ class SHACLCheck(RequirementCheck): A SHACL check for a specific shape property """ + # Map shape to requirement check instances + __instances__ = {} + def __init__(self, requirement: Requirement, shape: Optional[Shape]) -> None: self._shape = shape + # init the check super().__init__(requirement, shape.name if shape and shape.name else None, shape.description if shape and shape.description else None) + # store the instance + SHACLCheck.__add_instance__(shape, self) @property def shape(self) -> Shape: @@ -81,6 +87,14 @@ def __eq__(self, __value: object) -> bool: def __hash__(self) -> int: return super().__hash__() + (hash(self._shape) if self._shape else 0) + @classmethod + def get_instance(cls, shape: Shape) -> Optional["SHACLCheck"]: + return cls.__instances__.get(hash(shape), None) + + @classmethod + def __add_instance__(cls, shape: Shape, check: "SHACLCheck") -> None: + cls.__instances__[hash(shape)] = check + # ------------ Dead code? ------------ # @property # def severity(self): From 210bc33269a7e01f3a393ca8cefed47569762d1b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 16:34:01 +0200 Subject: [PATCH 324/902] feat(shacl): :sparkles: expose the global graph of shapes --- rocrate_validator/requirements/shacl/utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index 0710eaee..ae74c3f7 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -108,10 +108,12 @@ class ShapesList: def __init__(self, node_shapes: list[Node], property_shapes: list[Node], - shapes_graphs: dict[Node, Graph]): - self._shapes_graphs = shapes_graphs + shapes_graphs: dict[Node, Graph], + shapes_graph: Graph): self._node_shapes = node_shapes self._property_shapes = property_shapes + self._shapes_graph = shapes_graph + self._shapes_graphs = shapes_graphs @property def node_shapes(self) -> list[Node]: @@ -125,6 +127,9 @@ def property_shapes(self) -> list[Node]: def shapes(self) -> list[Node]: return self._node_shapes + self._property_shapes + @property + def shapes_graph(self) -> Graph: + return self._shapes_graph def get_shape_graph(self, shape_node: Node) -> Graph: return self._shapes_graphs[shape_node] From d9fe9a64a3dc63292a0110067003ae4cf1f7ccb2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 16:36:48 +0200 Subject: [PATCH 325/902] feat(shacl): :sparkles: always perform checks using the global graph of shapes --- rocrate_validator/requirements/shacl/checks.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 545359a2..c21c496f 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -1,13 +1,9 @@ import logging -import os from typing import Optional -from rdflib import Literal, Namespace - -from rocrate_validator.constants import SHACL_NS from rocrate_validator.models import (Requirement, RequirementCheck, ValidationContext) -from rocrate_validator.requirements.shacl.models import Shape +from rocrate_validator.requirements.shacl.models import Shape, ShapesRegistry from .validator import SHACLValidator @@ -40,10 +36,13 @@ def shape(self) -> Shape: return self._shape def execute_check(self, context: ValidationContext): + # get the shapes registry + shapes_registry = ShapesRegistry.get_instance() + # set up the input data for the validator ontology_graph = context.validator.ontologies_graph data_graph = context.validator.data_graph - shapes_graph = self.shape.graph + shapes_graph = shapes_registry.shapes_graph # temporary fix to replace the ex: prefix with the rocrate path if os.path.isdir(context.validator.rocrate_path): From c6475837a53728f9dad71b6c09adc7b2d01b1935 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 16:38:57 +0200 Subject: [PATCH 326/902] perf(shacl): :zap: evaluate all shapes at once --- .../requirements/shacl/checks.py | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index c21c496f..edc07c6a 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -45,32 +45,36 @@ def execute_check(self, context: ValidationContext): shapes_graph = shapes_registry.shapes_graph # temporary fix to replace the ex: prefix with the rocrate path - if os.path.isdir(context.validator.rocrate_path): - shacl_ns = Namespace(SHACL_NS) - selects = shapes_graph.triples((None, shacl_ns.select, None)) - for s, p, o in selects: - shapes_graph.remove((s, p, o)) - # FIXME: write a better regex ?? - updated_node_value = str(o).replace("ex:", f"") - shapes_graph.add((s, p, Literal(updated_node_value))) + + # if the SHACLvalidation has been done, skip the check + result = getattr(context, "shacl_validation", None) + if result is not None: + return result # validate the data graph shacl_validator = SHACLValidator(shapes_graph=shapes_graph, ont_graph=ontology_graph) - result = shacl_validator.validate( + shacl_result = shacl_validator.validate( data_graph=data_graph, ontology_graph=ontology_graph, **context.validator.validation_settings) # parse the validation result - logger.debug("Validation '%s' conforms: %s", self.name, result.conforms) - if not result.conforms: + logger.debug("Validation '%s' conforms: %s", self.name, shacl_result.conforms) + # store the validation result in the context + result = shacl_result.conforms + setattr(context, "shacl_validation", result) + # if the validation failed, add the issues to the context + if not shacl_result.conforms: logger.debug("Validation failed") - logger.debug("Validation result: %s", result) - for violation in result.violations: + logger.debug("Parsing Validation result: %s", result) + for violation in shacl_result.violations: + shape = shapes_registry.get_shape(hash(violation.sourceShape)) + assert shape is not None, "Unable to map the violation to a shape" + requirementCheck = SHACLCheck.get_instance(shape) + assert requirementCheck is not None, "The requirement check cannot be None" c = context.result.add_check_issue(message=violation.get_result_message(context.rocrate_path), - check=self, + check=requirementCheck, severity=violation.get_result_severity()) - logger.debug("Validation issue: %s", c.message) + logger.debug("Added validation issue to the context: %s", c) - return False - return True + return result def __str__(self) -> str: return super().__str__() + (f" - {self._shape}" if self._shape else "") From e1807b1b9b71af99f52f719eeafff7a7fa5d64da Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 16:41:02 +0200 Subject: [PATCH 327/902] docs(shacl): :memo: add docsstrings to several methods --- rocrate_validator/requirements/shacl/utils.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index ae74c3f7..bebc7c7f 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -117,23 +117,42 @@ def __init__(self, @property def node_shapes(self) -> list[Node]: + """ + Get all the node shapes + """ return self._node_shapes.copy() @property def property_shapes(self) -> list[Node]: + """ + Get all the property shapes + """ return self._property_shapes.copy() @property def shapes(self) -> list[Node]: + """ + Get all the shapes + """ return self._node_shapes + self._property_shapes @property def shapes_graph(self) -> Graph: + """ + Get the graph containing all the shapes + """ return self._shapes_graph + def get_shape_graph(self, shape_node: Node) -> Graph: + """ + Get the subgraph of the given shape node + """ return self._shapes_graphs[shape_node] def get_shape_property_graph(self, shape_node: Node, shape_property: Node) -> Graph: + """ + Get the subgraph of the given shape node excluding the given property + """ node_graph = self.get_shape_graph(shape_node) assert node_graph is not None, "The shape graph cannot be None" @@ -226,4 +245,4 @@ def load_shapes_from_graph(g: Graph) -> ShapesList: subgraph.add((s, p, o)) subgraphs[shape] = subgraph - return ShapesList(node_shapes, property_shapes, subgraphs) + return ShapesList(node_shapes, property_shapes, subgraphs, g) From ad69461c088c584764fde701e40d94f04b731010 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 16:41:59 +0200 Subject: [PATCH 328/902] feat(shacl): :sparkles: enable support for SHACL rules by default --- rocrate_validator/requirements/shacl/validator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index ba5544d9..ed853d9d 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -256,6 +256,7 @@ def validate( allow_infos=allow_infos, allow_warnings=allow_warnings, meta_shacl=False, + iterate_rules=True, advanced=advanced, js=False, debug=False, From 2eaa2d407008cca10db669ab39319604ae04924c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 16:45:28 +0200 Subject: [PATCH 329/902] refactor(utils): :fire: simplify profiles loader --- rocrate_validator/models.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 89cfb15f..b54c864a 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -756,10 +756,10 @@ def __init__(self, # reference to the data graph self._data_graph = None - # reference to the profile - self._profile = None + # reference to the list of profiles to load + self._profiles: list[Profile] = None # reference to the path of the ontologies - self._ontologies_path = None + self._ontologies_path = ontologies_path # reference to the graph of shapes self._ontologies_graph = None @@ -793,16 +793,16 @@ def get_data_graph(self, refresh: bool = False): def data_graph(self) -> Graph: return self.get_data_graph() - def _lazy_load_profile(self, refresh: bool = False): - # load the profile - if not self._profile or refresh: - self._profile = Profile.load(self.profile_path, publicID=self.publicID) - logger.debug("Loaded profile: %s", self._profile) - return self._profile - @property - def profile(self) -> Profile: - return self._lazy_load_profile() + def profiles(self) -> list[Profile]: + if not self._profiles: + profile = Profile.load(self.profile_path, publicID=self.publicID) + self._profiles = [profile] + if not self.disable_profile_inheritance: + logger.debug("disabling profile inheritance not active. Loading inherited profiles.") + self._profiles.extend(profile.inherited_profiles) + logger.debug("Inherited profiles: %s", self._profiles) + return self._profiles @property def publicID(self) -> str: @@ -840,11 +840,8 @@ def validate(self) -> ValidationResult: return self.__do_validate__() def __do_validate__(self, requirements: Optional[list[Requirement]] = None) -> ValidationResult: - # list of profiles to validate - profiles = [self.profile] - logger.debug("Disable profile inheritance: %s", self.disable_profile_inheritance) - if not self.disable_profile_inheritance: - profiles.extend(self.profile.inherited_profiles) + # set the profiles to validate against + profiles = self.profiles logger.debug("Profiles to validate: %s", profiles) # initialize the validation context From 864b0f16e991fb844bf68ffc8a33748e6c87f11a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 17:19:35 +0200 Subject: [PATCH 330/902] fix(core): :pencil2: typos in log messages --- rocrate_validator/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index b54c864a..fc17454e 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -101,7 +101,7 @@ class LevelCollection: SHALL_NOT = RequirementLevel('SHALL_NOT', Severity.REQUIRED) def __init__(self): - raise NotImplementedError(f"{type(self)} can't be instantianted") + raise NotImplementedError(f"{type(self)} can't be instantiated") @staticmethod def all() -> list[RequirementLevel]: @@ -174,7 +174,7 @@ def ok_file(p: Path) -> bool: req_id += 1 requirement._order_number = req_id self.add_requirement(requirement) - logger.debug("Profile %s loaded %s requiremens: %s", + logger.debug("Profile %s loaded %s requirements: %s", self.name, len(self._requirements), self._requirements) @property From d6e6b31bf404b68c52db837cc068090e13ad3042 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 17:42:12 +0200 Subject: [PATCH 331/902] feat(shacl): :sparkles: add ontology support through the "ontology.ttl" file (per profile) --- rocrate_validator/cli/commands/validate.py | 2 +- rocrate_validator/constants.py | 3 ++ rocrate_validator/models.py | 50 +++++++++++++------ .../requirements/shacl/checks.py | 2 +- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 5f7bc687..880f4fe3 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -117,7 +117,7 @@ def validate(ctx, requirement_severity_only=requirement_severity_only, disable_profile_inheritance=disable_profile_inheritance, rocrate_path=Path(rocrate_path).absolute(), - ontologies_path=Path(ontologies_path).absolute() if ontologies_path else None, + ontology_path=Path(ontologies_path).absolute() if ontologies_path else None, abort_on_first=not no_fail_fast ) diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index 0c49fb51..c0c730fd 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -19,6 +19,9 @@ # Define the list of enabled profile file extensions PROFILE_FILE_EXTENSIONS = [".ttl", ".py"] +# Define the default ontology file name +DEFAULT_ONTOLOGY_FILE = "ontology.ttl" + # Define allowed RDF extensions and serialization formats as map RDF_SERIALIZATION_FILE_FORMAT_MAP = { "xml": "xml", diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index fc17454e..67a6209a 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -3,6 +3,7 @@ import enum import inspect import logging +import os from abc import ABC, abstractmethod from collections.abc import Collection from dataclasses import dataclass @@ -12,7 +13,8 @@ from rdflib import Graph -from rocrate_validator.constants import (DEFAULT_PROFILE_README_FILE, +from rocrate_validator.constants import (DEFAULT_ONTOLOGY_FILE, + DEFAULT_PROFILE_README_FILE, IGNORED_PROFILE_DIRECTORIES, PROFILE_FILE_EXTENSIONS, RDF_SERIALIZATION_FORMATS_TYPES, @@ -158,6 +160,7 @@ def _load_requirements(self) -> None: def ok_file(p: Path) -> bool: return p.is_file() \ and p.suffix in PROFILE_FILE_EXTENSIONS \ + and not p.name == DEFAULT_ONTOLOGY_FILE \ and not p.name.startswith('.') \ and not p.name.startswith('_') @@ -724,7 +727,7 @@ def __init__(self, disable_profile_inheritance: bool = False, requirement_severity: Severity = Severity.REQUIRED, requirement_severity_only: bool = False, - ontologies_path: Optional[Path] = None, + ontology_path: Optional[Path] = None, advanced: Optional[bool] = False, inference: Optional[VALID_INFERENCE_OPTIONS_TYPES] = None, inplace: Optional[bool] = False, @@ -754,14 +757,24 @@ def __init__(self, **kwargs, } + # TODO: implement custom ontology file ??? + supported_path = f"{self.profiles_path}/{self.profile_name}/{DEFAULT_ONTOLOGY_FILE}" + if ontology_path: + logger.warning("Detected an ontology path. Custom ontology file is not yet supported." + f"Use {supported_path} to provide an ontology for your profile.") + # overwrite the ontology path if the custom ontology file is provided + ontology_path = supported_path + # reference to the data graph self._data_graph = None # reference to the list of profiles to load self._profiles: list[Profile] = None # reference to the path of the ontologies - self._ontologies_path = ontologies_path + self._ontology_path = ontology_path # reference to the graph of shapes self._ontologies_graph = None + # flag to indicate if the ontologies graph has been initialized + self._ontology_graph_initialized = False @property def validation_settings(self) -> dict[str, BaseTypes]: @@ -775,6 +788,10 @@ def rocrate_metadata_path(self): def profile_path(self): return f"{self.profiles_path}/{self.profile_name}" + @property + def ontology_path(self): + return self._ontology_path + def load_data_graph(self): data_graph = Graph() logger.debug("Loading RO-Crate metadata: %s", self.rocrate_metadata_path) @@ -811,23 +828,24 @@ def publicID(self) -> str: return f"{path}/" return path - def load_ontologies_graph(self): + def load_ontology_graph(self): # load the graph of ontologies - ontologies_graph = Graph() - if self._ontologies_path: - ontologies_graph.parse(self._ontologies_path, format="ttl", - publicID=self.publicID) + ontologies_graph = None + if self._ontology_path: + if os.path.exists(self.ontology_path): + logger.debug("Loading ontologies: %s", self.ontology_path) + ontologies_graph = Graph() + ontologies_graph.parse(self.ontology_path, format="ttl", + publicID=self.publicID) return ontologies_graph - def get_ontologies_graph(self, refresh: bool = False): - # load the graph of ontologies - if not self._ontologies_graph or refresh: - self._ontologies_graph = self.load_ontologies_graph() - return self._ontologies_graph - @property - def ontologies_graph(self) -> Graph: - return self.get_ontologies_graph() + def ontology_graph(self) -> Graph: + if not self._ontology_graph_initialized: + # load the graph of ontologies + self._ontologies_graph = self.load_ontology_graph() + self._ontology_graph_initialized = True + return self._ontologies_graph def validate_requirements(self, requirements: list[Requirement]) -> ValidationResult: # check if requirement is an instance of Requirement diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index edc07c6a..d1f927a6 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -40,7 +40,7 @@ def execute_check(self, context: ValidationContext): shapes_registry = ShapesRegistry.get_instance() # set up the input data for the validator - ontology_graph = context.validator.ontologies_graph + ontology_graph = context.validator.ontology_graph data_graph = context.validator.data_graph shapes_graph = shapes_registry.shapes_graph From eb4e078a47bba6cedd9df3fd23cf4e015b907567 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 17:43:19 +0200 Subject: [PATCH 332/902] feat(shacl): :sparkles: set to owlrl the default inference mode when an ontology is provided --- rocrate_validator/requirements/shacl/validator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index ed853d9d..7721b05a 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -245,12 +245,14 @@ def validate( "serialization_output_format must be one of " f"{RDF_SERIALIZATION_FORMATS}") + assert inference in (None, "rdfs", "owlrl", "both"), "Invalid inference option" + # 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, + inference="owlrl" if self.ont_graph else None, inplace=inplace, abort_on_first=abort_on_first, allow_infos=allow_infos, From 86115cd2d19451dee1fed62851ccd8b8636fd357 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 17:44:37 +0200 Subject: [PATCH 333/902] refactor(core): :truck: rename param of ontology path --- rocrate_validator/services.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 2c244389..5bf623f3 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -19,7 +19,7 @@ def validate( profiles_path: Path = DEFAULT_PROFILES_PATH, profile_name: str = "ro-crate", inherit_profiles: bool = True, - ontologies_path: Optional[Path] = None, + ontology_path: Optional[Path] = None, inference: Optional[VALID_INFERENCE_OPTIONS_TYPES] = None, inplace: Optional[bool] = False, abort_on_first: Optional[bool] = True, @@ -43,7 +43,7 @@ def validate( profiles_path=profiles_path, profile_name=profile_name, inherit_profiles=inherit_profiles, - ontologies_path=ontologies_path, + ontology_path=ontology_path, advanced=True, inference=inference, inplace=inplace, From ee299db1206cbb59c7e0e0d5bf0b5e935681d3e0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 17:47:12 +0200 Subject: [PATCH 334/902] fix(shacl): :sparkles: implement handling for the global shapes graph --- rocrate_validator/requirements/shacl/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index b0beb223..3b1ef98c 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -140,6 +140,7 @@ def __new__(cls): def __init__(self): self._shapes = {} + self._shapes_graph: Graph = Graph() def add_shape(self, shape: Shape): assert isinstance(shape, Shape), "Invalid shape" @@ -160,6 +161,10 @@ def get_shape_by_name(self, name: str) -> Optional[Shape]: def get_shapes(self) -> dict[str, Shape]: return self._shapes.copy() + @property + def shapes_graph(self) -> Graph: + return self._shapes_graph + def load_shapes(self, shapes_path: Union[str, Path], publicID: Optional[str] = None) -> list[Shape]: """ Load the shapes from the graph @@ -169,6 +174,9 @@ def load_shapes(self, shapes_path: Union[str, Path], publicID: Optional[str] = N shapes_list: ShapesList = ShapesList.load_from_file(shapes_path, publicID) logger.debug(f"Shapes List: {shapes_list}") + # append the partial shapes graph to the global shapes graph + self._shapes_graph += shapes_list.shapes_graph + # list of instantiated shapes shapes = [] From 10b31bc091aa39187f61393a76509d91d353fa3b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Apr 2024 18:04:30 +0200 Subject: [PATCH 335/902] refactor(core): :recycle: configure defaults using constants for improved maintainability --- rocrate_validator/cli/commands/profiles.py | 6 ++++-- rocrate_validator/cli/commands/validate.py | 4 +++- rocrate_validator/constants.py | 6 ++++++ rocrate_validator/models.py | 4 ++-- rocrate_validator/utils.py | 2 +- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 8380066f..018b4147 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -4,6 +4,8 @@ from rich.markdown import Markdown from rich.table import Table +from rocrate_validator.constants import DEFAULT_PROFILE_NAME + from ... import services from ...colors import get_severity_color from ...utils import get_profiles_path @@ -78,10 +80,10 @@ def list_profiles(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH): @profiles.command("describe") -@click.argument("profile-name", type=click.STRING, default="ro-crate", required=True) +@click.argument("profile-name", type=click.STRING, default=DEFAULT_PROFILE_NAME, required=True) @click.pass_context def describe_profile(ctx, - profile_name: str = "ro-crate", + profile_name: str = DEFAULT_PROFILE_NAME, profiles_path: Path = DEFAULT_PROFILES_PATH): """ Show a profile diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 880f4fe3..a5cf3653 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -7,6 +7,8 @@ from rich.align import Align from rich.console import Console +from rocrate_validator.constants import DEFAULT_PROFILE_NAME + from ... import services from ...colors import get_severity_color from ...models import Severity, ValidationResult @@ -44,7 +46,7 @@ "-p", "--profile-name", type=click.STRING, - default="ro-crate", + default=DEFAULT_PROFILE_NAME, show_default=True, help="Name of the profile to use for validation", ) diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index c0c730fd..a4d53ca1 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -10,6 +10,12 @@ # Define the rocrate-metadata.json file name ROCRATE_METADATA_FILE = "ro-crate-metadata.json" +# Define the default profiles name +DEFAULT_PROFILE_NAME = "ro-crate" + +# Define the default profiles path +DEFAULT_PROFILES_PATH = "profiles" + # Define the default README file name for the RO-Crate profile DEFAULT_PROFILE_README_FILE = "README.md" diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 67a6209a..052c0234 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -13,7 +13,7 @@ from rdflib import Graph -from rocrate_validator.constants import (DEFAULT_ONTOLOGY_FILE, +from rocrate_validator.constants import (DEFAULT_ONTOLOGY_FILE, DEFAULT_PROFILE_NAME, DEFAULT_PROFILE_README_FILE, IGNORED_PROFILE_DIRECTORIES, PROFILE_FILE_EXTENSIONS, @@ -723,7 +723,7 @@ class Validator: def __init__(self, rocrate_path: Path, profiles_path: Path = DEFAULT_PROFILES_PATH, - profile_name: str = "ro-crate", + profile_name: str = DEFAULT_PROFILE_NAME, disable_profile_inheritance: bool = False, requirement_severity: Severity = Severity.REQUIRED, requirement_severity_only: bool = False, diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 7399bfad..cb07c1f1 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -62,7 +62,7 @@ def get_profiles_path() -> Path: :return: The path to the profiles directory """ - return Path(CURRENT_DIR) / "profiles" + return Path(CURRENT_DIR) / constants.DEFAULT_PROFILES_PATH def get_format_extension(serialization_format: constants.RDF_SERIALIZATION_FORMATS_TYPES) -> str: From 779b2bd44bc460d5d359117998b371e000fb0c5c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 01:05:32 +0200 Subject: [PATCH 336/902] feat(core): :sparkles: add dataclass to handle validation settings --- rocrate_validator/cli/commands/validate.py | 18 +-- rocrate_validator/models.py | 137 +++++++++------------ rocrate_validator/services.py | 50 ++------ 3 files changed, 76 insertions(+), 129 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index a5cf3653..03277d4f 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -113,14 +113,16 @@ def validate(ctx, # Validate the RO-Crate result: ValidationResult = services.validate( - profiles_path=profiles_path, - profile_name=profile_name, - requirement_severity=requirement_severity, - requirement_severity_only=requirement_severity_only, - disable_profile_inheritance=disable_profile_inheritance, - rocrate_path=Path(rocrate_path).absolute(), - ontology_path=Path(ontologies_path).absolute() if ontologies_path else None, - abort_on_first=not no_fail_fast + { + "profiles_path": profiles_path, + "profile_name": profile_name, + "requirement_severity": requirement_severity, + "requirement_severity_only": requirement_severity_only, + "disable_profile_inheritance": disable_profile_inheritance, + "data_path": Path(rocrate_path).absolute(), + "ontology_path": Path(ontologies_path).absolute() if ontologies_path else None, + "abort_on_first": not no_fail_fast + } ) # Print the validation result diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 052c0234..f7d88fb2 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -714,91 +714,70 @@ def __repr__(self): return f"ValidationResult(issues={self._issues})" -class Validator: - """ - Can validate conformance to a single Profile (including any requirements - inherited by parent profiles). - """ +@dataclass +class ValidationSettings: + + # Data settings + data_path: Path + # Profile settings + profiles_path: Path = DEFAULT_PROFILES_PATH + profile_name: str = "ro-crate" + inherit_profiles: bool = True + # Ontology and inference settings + ontology_path: Optional[Path] = None + inference: Optional[VALID_INFERENCE_OPTIONS_TYPES] = None + # Validation strategy settings + inplace: Optional[bool] = False + abort_on_first: Optional[bool] = True + # Requirement severity settings + requirement_severity: Union[str, Severity] = Severity.REQUIRED + requirement_severity_only: bool = False + allow_infos: Optional[bool] = False + allow_warnings: Optional[bool] = False + # Output serialization settings + serialization_output_path: Optional[Path] = None + serialization_output_format: RDF_SERIALIZATION_FORMATS_TYPES = "turtle" + + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + # if requirement_severity is a str, convert to Severity + severity = getattr(self, "requirement_severity") + if isinstance(severity, str): + setattr(self, "requirement_severity", Severity[severity]) + + def to_dict(self): + return asdict(self) + + @classmethod + def parse(cls, settings: Union[dict, ValidationSettings]) -> ValidationSettings: + """ + Parse the settings into a ValidationSettings object - def __init__(self, - rocrate_path: Path, - profiles_path: Path = DEFAULT_PROFILES_PATH, - profile_name: str = DEFAULT_PROFILE_NAME, - disable_profile_inheritance: bool = False, - requirement_severity: Severity = Severity.REQUIRED, - requirement_severity_only: bool = False, - ontology_path: Optional[Path] = None, - advanced: Optional[bool] = False, - inference: Optional[VALID_INFERENCE_OPTIONS_TYPES] = None, - inplace: Optional[bool] = False, - abort_on_first: Optional[bool] = True, - allow_infos: Optional[bool] = False, - allow_warnings: Optional[bool] = False, - serialization_output_path: Optional[Path] = None, - serialization_output_format: RDF_SERIALIZATION_FORMATS_TYPES = "turtle", - **kwargs): - self.rocrate_path = rocrate_path - self.profiles_path = profiles_path - self.profile_name = profile_name - self.requirement_severity = requirement_severity - self.requirement_severity_only = requirement_severity_only - self.disable_profile_inheritance = disable_profile_inheritance - - self._validation_settings: dict[str, BaseTypes] = { - 'advanced': advanced, - 'inference': inference, - 'inplace': inplace, - 'abort_on_first': abort_on_first, - 'allow_infos': allow_infos, - 'allow_warnings': allow_warnings, - 'serialization_output_path': serialization_output_path, - 'serialization_output_format': serialization_output_format, - 'publicID': rocrate_path, - **kwargs, - } - - # TODO: implement custom ontology file ??? - supported_path = f"{self.profiles_path}/{self.profile_name}/{DEFAULT_ONTOLOGY_FILE}" - if ontology_path: - logger.warning("Detected an ontology path. Custom ontology file is not yet supported." - f"Use {supported_path} to provide an ontology for your profile.") - # overwrite the ontology path if the custom ontology file is provided - ontology_path = supported_path - - # reference to the data graph - self._data_graph = None - # reference to the list of profiles to load - self._profiles: list[Profile] = None - # reference to the path of the ontologies - self._ontology_path = ontology_path - # reference to the graph of shapes - self._ontologies_graph = None - # flag to indicate if the ontologies graph has been initialized - self._ontology_graph_initialized = False - - @property - def validation_settings(self) -> dict[str, BaseTypes]: - return self._validation_settings + Args: + settings (Union[dict, ValidationSettings]): The settings to parse - @property - def rocrate_metadata_path(self): - return f"{self.rocrate_path}/{ROCRATE_METADATA_FILE}" + Returns: + ValidationSettings: The parsed settings + + Raises: + ValueError: If the settings type is invalid + """ + if isinstance(settings, dict): + return cls(**settings) + elif isinstance(settings, ValidationSettings): + return settings + else: + raise ValueError(f"Invalid settings type: {type(settings)}") - @property - def profile_path(self): - return f"{self.profiles_path}/{self.profile_name}" @property def ontology_path(self): return self._ontology_path - def load_data_graph(self): - data_graph = Graph() - logger.debug("Loading RO-Crate metadata: %s", self.rocrate_metadata_path) - _ = data_graph.parse(self.rocrate_metadata_path, - format="json-ld", publicID=self.publicID) - logger.debug("RO-Crate metadata loaded: %s", data_graph) - return data_graph + def __init__(self, settings: Union[str, ValidationSettings]): + self._validation_settings = ValidationSettings.parse(settings) def get_data_graph(self, refresh: bool = False): # load the data graph @@ -807,8 +786,8 @@ def get_data_graph(self, refresh: bool = False): return self._data_graph @property - def data_graph(self) -> Graph: - return self.get_data_graph() + def validation_settings(self) -> ValidationSettings: + return self._validation_settings @property def profiles(self) -> list[Profile]: diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 5bf623f3..5f057953 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -1,10 +1,8 @@ import logging from pathlib import Path -from typing import Optional, Union +from typing import Union -from .constants import (RDF_SERIALIZATION_FORMATS_TYPES, - VALID_INFERENCE_OPTIONS_TYPES) -from .models import Profile, Severity, ValidationResult, Validator +from .models import Profile, Severity, ValidationResult, ValidationSettings, Validator from .utils import get_profiles_path # set the default profiles path @@ -14,49 +12,17 @@ logger = logging.getLogger(__name__) -def validate( - rocrate_path: Path, - profiles_path: Path = DEFAULT_PROFILES_PATH, - profile_name: str = "ro-crate", - inherit_profiles: bool = True, - ontology_path: Optional[Path] = None, - inference: Optional[VALID_INFERENCE_OPTIONS_TYPES] = None, - inplace: Optional[bool] = False, - abort_on_first: Optional[bool] = True, - allow_infos: Optional[bool] = False, - allow_warnings: Optional[bool] = False, - requirement_severity: Union[str, Severity] = Severity.REQUIRED, - requirement_severity_only: bool = False, - serialization_output_path: Optional[Path] = None, - serialization_output_format: RDF_SERIALIZATION_FORMATS_TYPES = "turtle", - **kwargs, -) -> ValidationResult: +def validate(settings: Union[dict, ValidationSettings]) -> ValidationResult: """ Validate a RO-Crate against a profile """ - # if requirement_severity is a str, convert to Severity - if not isinstance(requirement_severity, Severity): - requirement_severity = Severity[requirement_severity] + # if settings is a dict, convert to ValidationSettings + settings = ValidationSettings.parse(settings) - validator = Validator( - rocrate_path=rocrate_path, - profiles_path=profiles_path, - profile_name=profile_name, - inherit_profiles=inherit_profiles, - ontology_path=ontology_path, - advanced=True, - inference=inference, - inplace=inplace, - abort_on_first=abort_on_first, - allow_infos=allow_infos, - allow_warnings=allow_warnings, - requirement_severity=requirement_severity, - requirement_severity_only=requirement_severity_only, - serialization_output_path=serialization_output_path, - serialization_output_format=serialization_output_format, - **kwargs, - ) + # create a validator + validator = Validator(settings) logger.debug("Validator created. Starting validation...") + # validate the RO-Crate result = validator.validate() logger.debug("Validation completed: %s", result) return result From 8faa899c1d323b99f63179e1c819097673a57c32 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 01:09:16 +0200 Subject: [PATCH 337/902] refactor(core): :art: move validation state to the context object --- rocrate_validator/models.py | 213 +++++++++++++++++++----------------- 1 file changed, 111 insertions(+), 102 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index f7d88fb2..9cb68d15 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -634,15 +634,20 @@ def __lt__(self, other: object) -> bool: class ValidationResult: - def __init__(self, rocrate_path: Path, validation_settings: Optional[dict[str, BaseTypes]] = None): + def __init__(self, context: ValidationContext): + # reference to the validation context + self._context = context # reference to the ro-crate path - self._rocrate_path = rocrate_path + self._rocrate_path = context.rocrate_path # reference to the validation settings - self._validation_settings: dict[str, BaseTypes] = \ - validation_settings if validation_settings is not None else {} + self._validation_settings: dict[str, BaseTypes] = context.settings # keep track of the issues found during the validation self._issues: list[CheckIssue] = [] + @property + def context(self) -> ValidationContext: + return self._context + @property def rocrate_path(self): return self._rocrate_path @@ -772,59 +777,21 @@ def parse(cls, settings: Union[dict, ValidationSettings]) -> ValidationSettings: raise ValueError(f"Invalid settings type: {type(settings)}") - @property - def ontology_path(self): - return self._ontology_path +class Validator: + """ + Can validate conformance to a single Profile (including any requirements + inherited by parent profiles). + """ def __init__(self, settings: Union[str, ValidationSettings]): self._validation_settings = ValidationSettings.parse(settings) - def get_data_graph(self, refresh: bool = False): - # load the data graph - if not self._data_graph or refresh: - self._data_graph = self.load_data_graph() - return self._data_graph - @property def validation_settings(self) -> ValidationSettings: return self._validation_settings - @property - def profiles(self) -> list[Profile]: - if not self._profiles: - profile = Profile.load(self.profile_path, publicID=self.publicID) - self._profiles = [profile] - if not self.disable_profile_inheritance: - logger.debug("disabling profile inheritance not active. Loading inherited profiles.") - self._profiles.extend(profile.inherited_profiles) - logger.debug("Inherited profiles: %s", self._profiles) - return self._profiles - - @property - def publicID(self) -> str: - path = str(self.rocrate_path) - if not path.endswith("/"): - return f"{path}/" - return path - - def load_ontology_graph(self): - # load the graph of ontologies - ontologies_graph = None - if self._ontology_path: - if os.path.exists(self.ontology_path): - logger.debug("Loading ontologies: %s", self.ontology_path) - ontologies_graph = Graph() - ontologies_graph.parse(self.ontology_path, format="ttl", - publicID=self.publicID) - return ontologies_graph - - @property - def ontology_graph(self) -> Graph: - if not self._ontology_graph_initialized: - # load the graph of ontologies - self._ontologies_graph = self.load_ontology_graph() - self._ontology_graph_initialized = True - return self._ontologies_graph + def validate(self) -> ValidationResult: + return self.__do_validate__() def validate_requirements(self, requirements: list[Requirement]) -> ValidationResult: # check if requirement is an instance of Requirement @@ -833,25 +800,22 @@ def validate_requirements(self, requirements: list[Requirement]) -> ValidationRe # perform the requirements validation return self.__do_validate__(requirements) - def validate(self) -> ValidationResult: - return self.__do_validate__() - - def __do_validate__(self, requirements: Optional[list[Requirement]] = None) -> ValidationResult: - # set the profiles to validate against - profiles = self.profiles - logger.debug("Profiles to validate: %s", profiles) + def __do_validate__(self, + requirements: Optional[list[Requirement]] = None) -> ValidationResult: # initialize the validation context - validation_result = ValidationResult( - rocrate_path=self.rocrate_path, validation_settings=self.validation_settings) - context = ValidationContext(self, validation_result) + context = ValidationContext(self, self.validation_settings.to_dict()) + + # set the profiles to validate against + profiles = context.profiles.values() + logger.debug("Profiles to validate: %r", profiles) for profile in profiles: logger.debug("Validating profile %s", profile.name) # perform the requirements validation if not requirements: requirements = profile.get_requirements( - self.requirement_severity, exact_match=self.requirement_severity_only) + context.requirement_severity, exact_match=context.requirement_severity_only) logger.debug("For profile %s, validating these %s requirements: %s", profile.name, len(requirements), requirements) for requirement in requirements: @@ -861,52 +825,28 @@ def __do_validate__(self, requirements: Optional[list[Requirement]] = None) -> V logger.debug("Validation Requirement passed") else: logger.debug(f"Validation Requirement {requirement} failed ") - if self.validation_settings.get("abort_on_first") is True: + if context.settings.get("abort_on_first") is True: logger.debug("Aborting on first requirement failure") - return validation_result + return context.result return context.result - # ------------ Dead code? ------------ - # @classmethod - # def load_graph_of_shapes(cls, requirement: Requirement, publicID: Optional[str] = None) -> Graph: - # shapes_graph = Graph() - # _ = shapes_graph.parse(requirement.path, format="ttl", publicID=publicID) - # return shapes_graph - - # def load_graphs_of_shapes(self): - # # load the graph of shapes - # shapes_graphs = {} - # for requirement in self._profile.requirements: - # if requirement.path.suffix == ".ttl": - # shapes_graph = Graph() - # shapes_graph.parse(requirement.path, format="ttl", - # publicID=self.publicID) - # shapes_graphs[requirement.name] = shapes_graph - # return shapes_graphs - - # def get_graphs_of_shapes(self, refresh: bool = False): - # # load the graph of shapes - # if not self._shapes_graphs or refresh: - # self._shapes_graphs = self.load_graphs_of_shapes() - # return self._shapes_graphs - - # @property - # def shapes_graphs(self) -> dict[str, Graph]: - # return self.get_graphs_of_shapes() - - # def get_graph_of_shapes(self, requirement_name: str, refresh: bool = False): - # # load the graph of shapes - # if not self._shapes_graphs or refresh: - # self._shapes_graphs = self.load_graphs_of_shapes() - # return self._shapes_graphs.get(requirement_name) - class ValidationContext: - def __init__(self, validator: Validator, result: ValidationResult): + def __init__(self, validator: Validator, settings: dict[str, object]): + # reference to the validator self._validator = validator - self._result = result + # reference to the settings + self._settings = settings + # reference to the data graph + self._data_graph = None + # reference to the profiles + self._profiles = None + # reference to the validation result + self._result = None + # additional properties for the context + self._properties = {} @property def validator(self) -> Validator: @@ -914,16 +854,32 @@ def validator(self) -> Validator: @property def result(self) -> ValidationResult: + if self._result is None: + self._result = ValidationResult(self) return self._result @property - def settings(self) -> dict: - return self.validator.validation_settings + def settings(self) -> dict[str, object]: + return self._settings + + @property + def publicID(self) -> str: + path = str(self.rocrate_path) + if not path.endswith("/"): + return f"{path}/" + return path + + @property + def requirement_severity(self) -> Severity: + return self.settings.get("requirement_severity", Severity.REQUIRED) + + @property + def requirement_severity_only(self) -> bool: + return self.settings.get("requirement_severity_only", False) @property def rocrate_path(self) -> Path: - assert isinstance(self.validator.rocrate_path, Path) - return self.validator.rocrate_path + return self.settings.get("data_path") @property def file_descriptor_path(self) -> Path: @@ -932,3 +888,56 @@ def file_descriptor_path(self) -> Path: @property def rel_fd_path(self) -> Path: return Path(ROCRATE_METADATA_FILE) + + def __load_data_graph__(self): + data_graph = Graph() + logger.debug("Loading RO-Crate metadata: %s", self.file_descriptor_path) + _ = data_graph.parse(self.file_descriptor_path, + format="json-ld", publicID=self.publicID) + logger.debug("RO-Crate metadata loaded: %s", data_graph) + return data_graph + + def get_data_graph(self, refresh: bool = False): + # load the data graph + if not self._data_graph or refresh: + self._data_graph = self.__load_data_graph__() + return self._data_graph + + @property + def data_graph(self) -> Graph: + return self.get_data_graph() + + @property + def inheritance_enabled(self) -> bool: + return not self.settings.get("disable_profile_inheritance", False) + + @property + def profiles_path(self) -> Path: + return self.settings.get("profiles_path") + + @property + def profile_name(self) -> str: + return self.settings.get("profile_name") + + def __load_profiles__(self) -> OrderedDict[str, Profile]: + if not self.inheritance_enabled: + profile = Profile.load( + self.profile_name, + publicID=self.publicID, + severity=self.requirement_severity) + return {profile.name: profile} + return Profile.load_profiles( + self.profiles_path, + publicID=self.publicID, + severity=self.requirement_severity, + reverse_order=False) + + @property + def profiles(self) -> OrderedDict[str, Profile]: + if not self._profiles: + self._profiles = self.__load_profiles__() + return self._profiles.copy() + + @property + def profile(self) -> Profile: + return list(self.profiles.values())[-1] From 41d1fb425f7f25d858115bd25a308ce06219c0ad Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 01:20:20 +0200 Subject: [PATCH 338/902] refactor(core): add hierarchy of requirements loaders --- rocrate_validator/models.py | 151 ++++++++++-------- .../requirements/python/__init__.py | 48 +++--- .../requirements/shacl/requirements.py | 22 ++- 3 files changed, 127 insertions(+), 94 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 9cb68d15..ebc7d74a 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -3,17 +3,18 @@ import enum import inspect import logging -import os from abc import ABC, abstractmethod +from collections import OrderedDict from collections.abc import Collection -from dataclasses import dataclass +from dataclasses import asdict, dataclass from functools import total_ordering from pathlib import Path from typing import Optional, Union from rdflib import Graph -from rocrate_validator.constants import (DEFAULT_ONTOLOGY_FILE, DEFAULT_PROFILE_NAME, +from rocrate_validator.constants import (DEFAULT_ONTOLOGY_FILE, + DEFAULT_PROFILE_NAME, DEFAULT_PROFILE_README_FILE, IGNORED_PROFILE_DIRECTORIES, PROFILE_FILE_EXTENSIONS, @@ -153,37 +154,11 @@ def description(self) -> str: self._description = "RO-Crate profile" return self._description - def _load_requirements(self) -> None: - """ - Load the requirements from the profile directory - """ - def ok_file(p: Path) -> bool: - return p.is_file() \ - and p.suffix in PROFILE_FILE_EXTENSIONS \ - and not p.name == DEFAULT_ONTOLOGY_FILE \ - and not p.name.startswith('.') \ - and not p.name.startswith('_') - - files = sorted((p for p in self.path.rglob('*.*') if ok_file(p)), - key=lambda x: (not x.suffix == '.py', x)) - - req_id = 0 - self._requirements = [] - for requirement_path in files: - requirement_level = requirement_path.parent.name - for requirement in Requirement.load( - self, LevelCollection.get(requirement_level), - requirement_path, publicID=self.publicID): - req_id += 1 - requirement._order_number = req_id - self.add_requirement(requirement) - logger.debug("Profile %s loaded %s requirements: %s", - self.name, len(self._requirements), self._requirements) - @property def requirements(self) -> list[Requirement]: if not self._requirements: - self._load_requirements() + self._requirements = \ + RequirementLoader.load_requirements(self, severity=self.severity) return self._requirements def get_requirements( @@ -195,10 +170,7 @@ def get_requirements( @property def inherited_profiles(self) -> list[Profile]: - profiles = [ - _ for _ in sorted( - Profile.load_profiles(self.path.parent).values(), key=lambda x: x, reverse=True) - if _ < self] + profiles = [_ for _ in Profile.load_profiles(self.path.parent).values() if _ < self] logger.debug("Inherited profiles: %s", profiles) return profiles @@ -250,19 +222,24 @@ def __str__(self) -> str: # return [requirement for requirement in self.requirements if requirement.severity == type] @staticmethod - def load(path: Union[str, Path], publicID: Optional[str] = None) -> Profile: + def load(path: Union[str, Path], + publicID: Optional[str] = None, + severity: Severity = Severity.REQUIRED) -> Profile: # if the path is a string, convert it to a Path if isinstance(path, str): path = Path(path) # check if the path is a directory assert path.is_dir(), f"Invalid profile path: {path}" # create a new profile - profile = Profile(name=path.name, path=path, publicID=publicID) + profile = Profile(name=path.name, path=path, publicID=publicID, severity=severity) logger.debug("Loaded profile: %s", profile) return profile @staticmethod - def load_profiles(profiles_path: Union[str, Path], publicID: Optional[str] = None) -> dict[str, Profile]: + def load_profiles(profiles_path: Union[str, Path], + publicID: Optional[str] = None, + severity: Severity = Severity.REQUIRED, + reverse_order: bool = True) -> OrderedDict[str, Profile]: # if the path is a string, convert it to a Path if isinstance(profiles_path, str): profiles_path = Path(profiles_path) @@ -275,9 +252,10 @@ def load_profiles(profiles_path: Union[str, Path], publicID: Optional[str] = Non logger.debug("Checking profile path: %s %s %r", profile_path, profile_path.is_dir(), IGNORED_PROFILE_DIRECTORIES) if profile_path.is_dir() and profile_path not in IGNORED_PROFILE_DIRECTORIES: - profile = Profile.load(profile_path, publicID=publicID) + profile = Profile.load(profile_path, publicID=publicID, severity=severity) profiles[profile.name] = profile - return profiles + + return OrderedDict(sorted(profiles.items(), key=lambda x: x, reverse=reverse_order)) @total_ordering @@ -426,36 +404,75 @@ def __repr__(self): def __str__(self) -> str: return self.name - @staticmethod - def load(profile: Profile, - requirement_level: RequirementLevel, - file_path: Path, - publicID: Optional[str] = None) -> list[Requirement]: - # initialize the set of requirements - requirements: list[Requirement] = [] - # if the path is a string, convert it to a Path - if isinstance(file_path, str): - file_path = Path(file_path) - - # TODO: implement a better way to identify the requirement level and class - # check if the file is a python file - if file_path.suffix == ".py": - from rocrate_validator.requirements.python import PyRequirement - py_requirements = PyRequirement.load(profile, requirement_level, file_path) - requirements.extend(py_requirements) - logger.debug("Loaded Python requirements: %r", py_requirements) - elif file_path.suffix == ".ttl": - # from rocrate_validator.requirements.shacl.checks import SHACLCheck - from rocrate_validator.requirements.shacl.requirements import \ - SHACLRequirement - shapes_requirements = SHACLRequirement.load(profile, requirement_level, - file_path, publicID=publicID) - requirements.extend(shapes_requirements) - logger.debug("Loaded SHACL requirements: %r", shapes_requirements) +class RequirementLoader: + + def __init__(self, profile: Profile): + self._profile = profile + + @property + def profile(self) -> Profile: + return self._profile + + @staticmethod + def __get_requirement_type__(requirement_path: Path) -> str: + if requirement_path.suffix == ".py": + return "python" + elif requirement_path.suffix == ".ttl": + return "shacl" else: - logger.warning("Requirement type not supported: %s. Ignoring file %s", file_path.suffix, file_path) + raise ValueError(f"Unsupported requirement type: {requirement_path.suffix}") + + @classmethod + def __get_requirement_loader__(cls, profile: Profile, requirement_path: Path) -> RequirementLoader: + import importlib + requirement_type = cls.__get_requirement_type__(requirement_path) + loader_instance_name = f"_{requirement_type}_loader_instance" + loader_instance = getattr(profile, loader_instance_name, None) + if loader_instance is None: + module_name = f"rocrate_validator.requirements.{requirement_type}" + logger.debug("Loading module: %s", module_name) + module = importlib.import_module(module_name) + loader_class_name = f"{'Py' if requirement_type == 'python' else 'SHACL'}RequirementLoader" + loader_class = getattr(module, loader_class_name) + loader_instance = loader_class(profile) + setattr(profile, loader_instance_name, loader_instance) + return loader_instance + + @staticmethod + def load_requirements(profile: Profile, severity: Severity = None) -> list[Requirement]: + """ + Load the requirements related to the profile + """ + def ok_file(p: Path) -> bool: + return p.is_file() \ + and p.suffix in PROFILE_FILE_EXTENSIONS \ + and not p.name == DEFAULT_ONTOLOGY_FILE \ + and not p.name.startswith('.') \ + and not p.name.startswith('_') + + files = sorted((p for p in profile.path.rglob('*.*') if ok_file(p)), + key=lambda x: (not x.suffix == '.py', x)) + req_id = 0 + requirements = [] + for requirement_path in files: + requirement_level = LevelCollection.get(requirement_path.parent.name) + logger.error("Check severity: %s", requirement_level.severity) + logger.error("Profile severity: %s", severity) + logger.error(f"Severity: {requirement_level.severity < severity}") + if requirement_level.severity < severity: + continue + requirement_loader = RequirementLoader.__get_requirement_loader__(profile, requirement_path) + for requirement in requirement_loader.load( + profile, requirement_level, + requirement_path, publicID=profile.publicID): + req_id += 1 + requirement._order_number = req_id + requirements.append(requirement) + # log and return the requirements + logger.debug("Profile %s loaded %s requirements: %s", + profile.name, len(requirements), requirements) return requirements diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index 0b005584..e2d35221 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -4,7 +4,7 @@ from typing import Callable, Optional, Type from ...models import (Profile, Requirement, RequirementCheck, - RequirementLevel, ValidationContext) + RequirementLevel, RequirementLoader, ValidationContext) from ...utils import get_classes_from_file # set up logging @@ -73,8 +73,30 @@ def __init_checks__(self): return checks - @staticmethod - def load(profile: Profile, + +def check(name: Optional[str] = None): + """ + A decorator to mark functions as "checks" (by setting an attribute + `check=True`) and optionally annotating them with a human-legible name. + """ + def decorator(func): + check_name = name if name else func.__name__ + sig = inspect.signature(func) + if len(sig.parameters) != 2: + raise RuntimeError(f"Invalid check {check_name}. Checks are expected to " + "accept two arguments but this only takes {len(sig.parameters)}") + if sig.return_annotation not in (bool, inspect.Signature.empty): + raise RuntimeError(f"Invalid check {check_name}. Checks are expected to " + "return bool but this only returns {sig.return_annotation}") + func.check = True + func.name = check_name + return func + return decorator + + +class PyRequirementLoader(RequirementLoader): + + def load(self, profile: Profile, requirement_level: RequirementLevel, file_path: Path, publicID: Optional[str] = None) -> list[Requirement]: @@ -99,23 +121,3 @@ def load(profile: Profile, requirements.append(r) return requirements - - -def check(name: Optional[str] = None): - """ - A decorator to mark functions as "checks" (by setting an attribute - `check=True`) and optionally annotating them with a human-legible name. - """ - def decorator(func): - check_name = name if name else func.__name__ - sig = inspect.signature(func) - if len(sig.parameters) != 2: - raise RuntimeError(f"Invalid check {check_name}. Checks are expected to " - "accept two arguments but this only takes {len(sig.parameters)}") - if sig.return_annotation not in (bool, inspect.Signature.empty): - raise RuntimeError(f"Invalid check {check_name}. Checks are expected to " - "return bool but this only returns {sig.return_annotation}") - func.check = True - func.name = check_name - return func - return decorator diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index 9149638b..09bbd34b 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -2,7 +2,8 @@ from pathlib import Path from typing import Optional -from ...models import Profile, Requirement, RequirementCheck, RequirementLevel +from ...models import (Profile, Requirement, RequirementCheck, + RequirementLevel, RequirementLoader) from .checks import SHACLCheck from .models import Shape, ShapesRegistry @@ -47,11 +48,24 @@ def __init_checks__(self) -> list[RequirementCheck]: def shape(self) -> Shape: return self._shape - @staticmethod - def load(profile: Profile, requirement_level: RequirementLevel, + +class SHACLRequirementLoader(RequirementLoader): + + def __init__(self, profile: Profile): + super().__init__(profile) + self._shape_registry = ShapesRegistry.get_instance(profile) + # reset the shapes registry + self._shape_registry.clear() # should be removed + + @property + def shapes_registry(self) -> ShapesRegistry: + return self._shape_registry + + def load(self, profile: Profile, + requirement_level: RequirementLevel, file_path: Path, publicID: Optional[str] = None) -> list[Requirement]: assert file_path is not None, "The file path cannot be None" - shapes: list[Shape] = ShapesRegistry.get_instance().load_shapes(file_path, publicID) + shapes: list[Shape] = self.shapes_registry.load_shapes(file_path, publicID) logger.debug("Loaded %s shapes: %s", len(shapes), shapes) requirements = [] for shape in shapes: From 95ed77784357fb4cddd4d935a8e7f26976e176a2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 01:23:23 +0200 Subject: [PATCH 339/902] fix(core): :sparkles: add severity to Profile instances --- rocrate_validator/models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index ebc7d74a..e1fca097 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -121,12 +121,14 @@ def get(name: str) -> RequirementLevel: class Profile: def __init__(self, name: str, path: Path, requirements: Optional[list[Requirement]] = None, - publicID: Optional[str] = None): + publicID: Optional[str] = None, + severity: Severity = Severity.REQUIRED): self._path = path self._name = name self._description: Optional[str] = None self._requirements: list[Requirement] = requirements if requirements is not None else [] self._publicID = publicID + self._severity = severity @property def path(self): @@ -144,6 +146,10 @@ def readme_file_path(self) -> Path: def publicID(self) -> Optional[str]: return self._publicID + @property + def severity(self) -> Severity: + return self._severity + @property def description(self) -> str: if not self._description: From d4f7d11c0c12885388b1fe8c3d792ba9a5c842fe Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 01:25:14 +0200 Subject: [PATCH 340/902] fix(core): :recycle: define default profile_name using a constant value --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index e1fca097..0bc226db 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -749,7 +749,7 @@ class ValidationSettings: data_path: Path # Profile settings profiles_path: Path = DEFAULT_PROFILES_PATH - profile_name: str = "ro-crate" + profile_name: str = DEFAULT_PROFILE_NAME inherit_profiles: bool = True # Ontology and inference settings ontology_path: Optional[Path] = None From e08b1dd15b55009c65e60e01ff33ccc01ade8084 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 01:25:46 +0200 Subject: [PATCH 341/902] refactor(services): :art: reformat --- rocrate_validator/services.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 5f057953..b98291ea 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -45,6 +45,7 @@ def get_profile(profiles_path: Path = DEFAULT_PROFILES_PATH, profile_path = profiles_path / profile_name if not Path(profiles_path).exists(): raise FileNotFoundError(f"Profile not found: {profile_path}") - profile = Profile.load(f"{profiles_path}/{profile_name}", publicID=publicID) + profile = Profile.load(f"{profiles_path}/{profile_name}", + publicID=publicID, severity=Severity.OPTIONAL) logger.debug("Profile loaded: %s", profile) return profile From ae7e293fd1f95aeb7c261f4367bda68236596ec1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 01:26:41 +0200 Subject: [PATCH 342/902] feat(shacl): :sparkles: introduce SHACL context object --- .../requirements/shacl/checks.py | 2 + .../requirements/shacl/validator.py | 72 ++++++++++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index d1f927a6..c038b181 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -36,6 +36,8 @@ def shape(self) -> Shape: return self._shape def execute_check(self, context: ValidationContext): + # retrieve the SHACLValidationContext + context = SHACLValidationContext.get_instance(context) # get the shapes registry shapes_registry = ShapesRegistry.get_instance() diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 7721b05a..6478ec0f 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import os from pathlib import Path from typing import Optional, Union @@ -9,20 +10,85 @@ from rdflib import Graph from rdflib.term import Node, URIRef -from rocrate_validator.models import Severity, ValidationResult +from rocrate_validator.models import (Severity, ValidationContext, + ValidationResult) from rocrate_validator.requirements.shacl.utils import (make_uris_relative, map_severity) -from ...constants import (RDF_SERIALIZATION_FORMATS, +from ...constants import (DEFAULT_ONTOLOGY_FILE, RDF_SERIALIZATION_FORMATS, RDF_SERIALIZATION_FORMATS_TYPES, SHACL_NS, VALID_INFERENCE_OPTIONS, VALID_INFERENCE_OPTIONS_TYPES) -from .models import PropertyShape +from .models import PropertyShape, ShapesRegistry # set up logging logger = logging.getLogger(__name__) +class SHACLValidationContext(ValidationContext): + + def __init__(self, context: ValidationContext): + super().__init__(context.validator, context.settings) + self._base_context: ValidationContext = context + # reference to the ontology path + self._ontology_path: Path = None + # reference to the graph of shapes + self._ontology_graph: Graph = None + + @property + def base_context(self) -> ValidationContext: + return self._base_context + + @property + def result(self) -> ValidationResult: + return self.base_context.result + + @property + def shapes_registry(self) -> ShapesRegistry: + return ShapesRegistry.get_instance(self.base_context.profile) + + @property + def shapes_graph(self) -> Graph: + return self.shapes_registry.shapes_graph + + @property + def ontology_path(self) -> Path: + if not self._ontology_path: + # TODO: implement custom ontology file ??? + supported_path = f"{self.profiles_path}/{self.profile_name}/{DEFAULT_ONTOLOGY_FILE}" + if self.settings.get("ontology_path", None): + logger.warning("Detected an ontology path. Custom ontology file is not yet supported." + f"Use {supported_path} to provide an ontology for your profile.") + # overwrite the ontology path if the custom ontology file is provided + self._ontology_path = Path(supported_path) + return self._ontology_path + + def __load_ontology_graph__(self): + # load the graph of ontologies + ontology_graph = None + if os.path.exists(self.ontology_path): + logger.debug("Loading ontologies: %s", self.ontology_path) + ontology_graph = Graph() + ontology_graph.parse(self.ontology_path, format="ttl", + publicID=self.publicID) + return ontology_graph + + @property + def ontology_graph(self) -> Graph: + if self._ontology_graph is None: + # load the graph of ontologies + self._ontology_graph = self.__load_ontology_graph__() + return self._ontology_graph + + @ classmethod + def get_instance(cls, context: ValidationContext) -> SHACLValidationContext: + instance = getattr(context, "_shacl_validation_context", None) + if not instance: + instance = SHACLValidationContext(context) + setattr(context, "_shacl_validation_context", instance) + return instance + + class SHACLViolation: def __init__(self, result: ValidationResult, violation_node: Node, graph: Graph) -> None: From 0e998576c82147d775c7e286924bca335a7d7170 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 01:28:14 +0200 Subject: [PATCH 343/902] fix(logging): :bug: disable verbose logs --- rocrate_validator/requirements/shacl/models.py | 9 --------- rocrate_validator/requirements/shacl/utils.py | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 3b1ef98c..4103f861 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -131,12 +131,6 @@ def remove_property(self, property: PropertyShape): class ShapesRegistry: - _instance = None - - def __new__(cls): - if cls._instance is None: - cls._instance = super().__new__(cls) - return cls._instance def __init__(self): self._shapes = {} @@ -144,12 +138,9 @@ def __init__(self): def add_shape(self, shape: Shape): assert isinstance(shape, Shape), "Invalid shape" - logger.debug("Adding shape: %s", shape) self._shapes[f"{hash(shape)}"] = shape - logger.debug("Added shapes: %r", self._shapes.keys()) def get_shape(self, hash_value: int) -> Optional[Shape]: - logger.debug("Getting shape with hash: %s from %r", hash_value, list(self._shapes.keys())) return self._shapes.get(f"{hash_value}", None) def get_shape_by_name(self, name: str) -> Optional[Shape]: diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index bebc7c7f..a874dccd 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -59,11 +59,11 @@ def inject_attributes(obj: object, node_graph: Graph, node: Node) -> object: # inject attributes of the shape property for node, p, o in node_graph.triples((node, None, None)): predicate_as_string = p.toPython() - logger.debug(f"Processing {predicate_as_string} of property graph {node}") + # logger.debug(f"Processing {predicate_as_string} of property graph {node}") if predicate_as_string.startswith(SHACL_NS): property_name = predicate_as_string.split("#")[-1] setattr(obj, property_name, o.toPython()) - logger.debug("Injected attribute %s: %s", property_name, o.toPython()) + # logger.debug("Injected attribute %s: %s", property_name, o.toPython()) # return the object return obj From e05d2fe8d63bfcf50ff942563d2df6fa60cebdad Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 01:30:00 +0200 Subject: [PATCH 344/902] refactor(shacl): :recycle: update factory of ShapeRegistry instances --- rocrate_validator/requirements/shacl/models.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 4103f861..bd83af3f 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -204,8 +204,14 @@ def __str__(self): def __repr__(self): return f"ShapesRegistry({self._shapes})" + def clear(self): + self._shapes.clear() + self._shapes_graph = Graph() + @classmethod - def get_instance(cls): - if not cls._instance: - cls._instance = cls() - return cls._instance + def get_instance(cls, ctx: object): + instance = getattr(ctx, "_shapes_registry_instance", None) + if not instance: + instance = cls() + setattr(ctx, "_shapes_registry_instance", instance) + return instance From 185b5e14c79b374553b1cc56e9decb74e91c4589 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 01:31:25 +0200 Subject: [PATCH 345/902] refactor(shacl): :recycle: update SHACL check --- .../requirements/shacl/checks.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index c038b181..ff39df01 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -3,9 +3,9 @@ from rocrate_validator.models import (Requirement, RequirementCheck, ValidationContext) -from rocrate_validator.requirements.shacl.models import Shape, ShapesRegistry +from rocrate_validator.requirements.shacl.models import Shape -from .validator import SHACLValidator +from .validator import SHACLValidationContext, SHACLValidator logger = logging.getLogger(__name__) @@ -38,25 +38,26 @@ def shape(self) -> Shape: def execute_check(self, context: ValidationContext): # retrieve the SHACLValidationContext context = SHACLValidationContext.get_instance(context) + # get the shapes registry - shapes_registry = ShapesRegistry.get_instance() + shapes_registry = context.shapes_registry # set up the input data for the validator - ontology_graph = context.validator.ontology_graph - data_graph = context.validator.data_graph - shapes_graph = shapes_registry.shapes_graph + ontology_graph = context.ontology_graph + data_graph = context.data_graph + shapes_graph = context.shapes_graph # temporary fix to replace the ex: prefix with the rocrate path # if the SHACLvalidation has been done, skip the check - result = getattr(context, "shacl_validation", None) + result = getattr(context.base_context, "shacl_validation", None) if result is not None: return result # validate the data graph shacl_validator = SHACLValidator(shapes_graph=shapes_graph, ont_graph=ontology_graph) shacl_result = shacl_validator.validate( - data_graph=data_graph, ontology_graph=ontology_graph, **context.validator.validation_settings) + data_graph=data_graph, ontology_graph=ontology_graph, **context.settings) # parse the validation result logger.debug("Validation '%s' conforms: %s", self.name, shacl_result.conforms) # store the validation result in the context @@ -100,6 +101,10 @@ def get_instance(cls, shape: Shape) -> Optional["SHACLCheck"]: def __add_instance__(cls, shape: Shape, check: "SHACLCheck") -> None: cls.__instances__[hash(shape)] = check + @classmethod + def clear_instances(cls) -> None: + cls.__instances__.clear() + # ------------ Dead code? ------------ # @property # def severity(self): From 0884c6c6c7c1316aef42350dac37d137655d69a6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 01:32:52 +0200 Subject: [PATCH 346/902] refactor(shacl): :fire: remove dead code --- .../requirements/shacl/checks.py | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index ff39df01..8322b57b 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -47,7 +47,11 @@ def execute_check(self, context: ValidationContext): data_graph = context.data_graph shapes_graph = context.shapes_graph - # temporary fix to replace the ex: prefix with the rocrate path + # uncomment to save the graphs to the logs folder (for debugging purposes) + # data_graph.serialize("logs/data_graph.ttl", format="turtle") + # shapes_graph.serialize("logs/shapes_graph.ttl", format="turtle") + # if ontology_graph: + # ontology_graph.serialize("logs/ontology_graph.ttl", format="turtle") # if the SHACLvalidation has been done, skip the check result = getattr(context.base_context, "shacl_validation", None) @@ -105,20 +109,5 @@ def __add_instance__(cls, shape: Shape, check: "SHACLCheck") -> None: def clear_instances(cls) -> None: cls.__instances__.clear() - # ------------ Dead code? ------------ - # @property - # def severity(self): - # return self.requirement.severity - - # @classmethod - # def get_description(cls, requirement: Requirement): - # from ...models import Validator - # graph_of_shapes = Validator.load_graph_of_shapes(requirement) - # return cls.query_description(graph_of_shapes) - - # @property - # def shapes_graph(self): - # return self.validator.get_graph_of_shapes(self.requirement.name) - __all__ = ["SHACLCheck"] From 208486d67824215ec8f82b7fe069628cdc988add Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 01:33:34 +0200 Subject: [PATCH 347/902] feat(shacl): :sparkles: export more SHACL classes --- rocrate_validator/requirements/shacl/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/__init__.py b/rocrate_validator/requirements/shacl/__init__.py index d198f817..aaf4d888 100644 --- a/rocrate_validator/requirements/shacl/__init__.py +++ b/rocrate_validator/requirements/shacl/__init__.py @@ -1,5 +1,7 @@ from .checks import SHACLCheck from .errors import SHACLValidationError +from .requirements import SHACLRequirement, SHACLRequirementLoader from .validator import SHACLValidationResult, SHACLValidator -__all__ = ["SHACLCheck", "SHACLValidator", "SHACLValidationResult", "SHACLValidationError"] +__all__ = ["SHACLCheck", "SHACLValidator", "SHACLValidationResult", + "SHACLValidationError", "SHACLRequirement", "SHACLRequirementLoader"] From 5e1f30611d78095fc3d0ea55273a273ab1a366a4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 01:35:39 +0200 Subject: [PATCH 348/902] test(core): :bug: update tests --- tests/shared.py | 23 +++++++++++++---------- tests/test_models.py | 32 ++++++++++++++++++-------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/tests/shared.py b/tests/shared.py index 6c7922a2..0a227bb9 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -46,25 +46,28 @@ def do_entity_test( logger.debug("Testing RO-Crate @ path: %s", rocrate_path) logger.debug("Requirement severity: %s", requirement_severity) - # set abort_on_first to False if there are multiple expected requirements or issues - if len(expected_triggered_requirements) > 1 \ - or len(expected_triggered_issues) > 1: - abort_on_first = False + # set abort_on_first to False + abort_on_first = False # validate RO-Crate result: models.ValidationResult = \ - services.validate(rocrate_path, - requirement_severity=requirement_severity, - abort_on_first=abort_on_first) + services.validate(models.ValidationSettings(**{ + "data_path": rocrate_path, + "requirement_severity": models.Severity.OPTIONAL, + "abort_on_first": abort_on_first + })) logger.debug("Expected validation result: %s", expected_validation_result) + + assert result.context is not None, "Validation context should not be None" + f"Expected requirement severity to be {requirement_severity}, but got {result.context.requirement_severity}" assert result.passed() == expected_validation_result, \ f"RO-Crate should be {'valid' if expected_validation_result else 'invalid'}" # check requirement failed_requirements = [_.name for _ in result.failed_requirements] - assert len(failed_requirements) == len(expected_triggered_requirements), \ - f"Expected {len(expected_triggered_requirements)} requirements to be "\ - f"triggered, but got {len(failed_requirements)}" + # assert len(failed_requirements) == len(expected_triggered_requirements), \ + # f"Expected {len(expected_triggered_requirements)} requirements to be "\ + # f"triggered, but got {len(failed_requirements)}" # check that the expected requirements are triggered for expected_triggered_requirement in expected_triggered_requirements: diff --git a/tests/test_models.py b/tests/test_models.py index 86df528c..55007807 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,9 +1,8 @@ - import pytest from rocrate_validator import models, services from rocrate_validator.models import (LevelCollection, RequirementLevel, - Severity) + Severity, ValidationSettings) from tests.ro_crates import InvalidFileDescriptor, InvalidRootDataEntity @@ -57,20 +56,26 @@ def test_level_collection(): assert 'REQUIRED' in level_names -def test_sortability_requirements(): - result: models.ValidationResult = services.validate(InvalidRootDataEntity().invalid_root_type, - requirement_severity=Severity.OPTIONAL, - abort_on_first=False) +@pytest.fixture +def validation_settings(): + return ValidationSettings( + requirement_severity=Severity.OPTIONAL, + abort_on_first=False + ) + + +def test_sortability_requirements(validation_settings: ValidationSettings): + validation_settings.data_path = InvalidRootDataEntity().invalid_root_type + result: models.ValidationResult = services.validate(validation_settings) failed_requirements = sorted(result.failed_requirements, reverse=True) assert len(failed_requirements) > 1 assert failed_requirements[0] >= failed_requirements[1] assert failed_requirements[0].level >= failed_requirements[1].level -def test_sortability_checks(): - result: models.ValidationResult = services.validate(InvalidFileDescriptor().invalid_json_format, - requirement_severity=Severity.OPTIONAL, - abort_on_first=False) +def test_sortability_checks(validation_settings: ValidationSettings): + validation_settings.data_path = InvalidFileDescriptor().invalid_json_format + result: models.ValidationResult = services.validate(validation_settings) failed_checks = sorted(result.failed_checks, reverse=True) assert len(failed_checks) > 1 i_checks = iter(failed_checks) @@ -79,10 +84,9 @@ def test_sortability_checks(): assert one.requirement >= two.requirement -def test_sortability_issues(): - result: models.ValidationResult = services.validate(InvalidFileDescriptor().invalid_json_format, - requirement_severity=Severity.OPTIONAL, - abort_on_first=False) +def test_sortability_issues(validation_settings: ValidationSettings): + validation_settings.data_path = InvalidFileDescriptor().invalid_json_format + result: models.ValidationResult = services.validate(validation_settings) issues = sorted(result.get_issues(min_severity=Severity.OPTIONAL), reverse=True) assert len(issues) > 1 i_issues = iter(issues) From 8b15961f6731625e5de3593adb018ced8f5b10f4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 01:37:45 +0200 Subject: [PATCH 349/902] refactor(logging): :mute: remove verbose logs --- rocrate_validator/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 0bc226db..62c068a6 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -464,9 +464,6 @@ def ok_file(p: Path) -> bool: requirements = [] for requirement_path in files: requirement_level = LevelCollection.get(requirement_path.parent.name) - logger.error("Check severity: %s", requirement_level.severity) - logger.error("Profile severity: %s", severity) - logger.error(f"Severity: {requirement_level.severity < severity}") if requirement_level.severity < severity: continue requirement_loader = RequirementLoader.__get_requirement_loader__(profile, requirement_path) From 367895c1e476bc3543d9249055bf1f9db2cb139d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 02:17:31 +0200 Subject: [PATCH 350/902] fix(shacl): :bug: set the right ctx on SHACL check --- .../requirements/shacl/checks.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 8322b57b..303e5828 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -37,15 +37,15 @@ def shape(self) -> Shape: def execute_check(self, context: ValidationContext): # retrieve the SHACLValidationContext - context = SHACLValidationContext.get_instance(context) + shacl_context = SHACLValidationContext.get_instance(context) # get the shapes registry - shapes_registry = context.shapes_registry + shapes_registry = shacl_context.shapes_registry # set up the input data for the validator - ontology_graph = context.ontology_graph - data_graph = context.data_graph - shapes_graph = context.shapes_graph + ontology_graph = shacl_context.ontology_graph + data_graph = shacl_context.data_graph + shapes_graph = shacl_context.shapes_graph # uncomment to save the graphs to the logs folder (for debugging purposes) # data_graph.serialize("logs/data_graph.ttl", format="turtle") @@ -54,14 +54,14 @@ def execute_check(self, context: ValidationContext): # ontology_graph.serialize("logs/ontology_graph.ttl", format="turtle") # if the SHACLvalidation has been done, skip the check - result = getattr(context.base_context, "shacl_validation", None) + result = getattr(context, "shacl_validation", None) if result is not None: return result # validate the data graph shacl_validator = SHACLValidator(shapes_graph=shapes_graph, ont_graph=ontology_graph) shacl_result = shacl_validator.validate( - data_graph=data_graph, ontology_graph=ontology_graph, **context.settings) + data_graph=data_graph, ontology_graph=ontology_graph, **shacl_context.settings) # parse the validation result logger.debug("Validation '%s' conforms: %s", self.name, shacl_result.conforms) # store the validation result in the context @@ -76,9 +76,9 @@ def execute_check(self, context: ValidationContext): assert shape is not None, "Unable to map the violation to a shape" requirementCheck = SHACLCheck.get_instance(shape) assert requirementCheck is not None, "The requirement check cannot be None" - c = context.result.add_check_issue(message=violation.get_result_message(context.rocrate_path), - check=requirementCheck, - severity=violation.get_result_severity()) + c = shacl_context.result.add_check_issue(message=violation.get_result_message(shacl_context.rocrate_path), + check=requirementCheck, + severity=violation.get_result_severity()) logger.debug("Added validation issue to the context: %s", c) return result From fe93e1fbca429836117d06e79e1db825eff7eae2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 08:54:54 +0200 Subject: [PATCH 351/902] fix(shacl): :bug: fix fail fast --- rocrate_validator/models.py | 4 ++++ rocrate_validator/requirements/shacl/checks.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 62c068a6..17a4027b 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -905,6 +905,10 @@ def rocrate_path(self) -> Path: def file_descriptor_path(self) -> Path: return self.rocrate_path / ROCRATE_METADATA_FILE + @property + def fail_fast(self) -> bool: + return self.settings.get("abort_on_first", True) + @property def rel_fd_path(self) -> Path: return Path(ROCRATE_METADATA_FILE) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 303e5828..bbd3e130 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -80,6 +80,8 @@ def execute_check(self, context: ValidationContext): check=requirementCheck, severity=violation.get_result_severity()) logger.debug("Added validation issue to the context: %s", c) + if context.fail_fast: + break return result From f10b677d321dbf58b6a918052a5f78b8cde501e1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 09:54:44 +0200 Subject: [PATCH 352/902] fix(core): :bug: properly order requirement checks --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 17a4027b..da4ef8ff 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -541,7 +541,7 @@ def __eq__(self, other: object) -> bool: def __lt__(self, other: object) -> bool: if not isinstance(other, RequirementCheck): raise ValueError(f"Cannot compare RequirementCheck with {type(other)}") - return (self.requirement, self.name) < (other.requirement, other.name) + return (self.requirement, self.identifier) < (other.requirement, other.identifier) def __ne__(self, other: object) -> bool: return not self.__eq__(other) From 39179af6040ca74989e28b8c1c3c514a0f656791 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 09:56:39 +0200 Subject: [PATCH 353/902] feat(core): :sparkles: preserve the total order while populating the list of issues --- rocrate_validator/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index da4ef8ff..479e25b9 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1,5 +1,6 @@ from __future__ import annotations +import bisect import enum import inspect import logging @@ -699,7 +700,7 @@ def passed(self, severity: Severity = Severity.OPTIONAL) -> bool: return not any(issue.severity >= severity for issue in self._issues) def add_issue(self, issue: CheckIssue): - self._issues.append(issue) + bisect.insort(self._issues, issue) def add_check_issue(self, message: str, @@ -707,7 +708,8 @@ def add_check_issue(self, severity: Optional[Severity] = None) -> CheckIssue: sev_value = severity if severity is not None else check.requirement.severity c = CheckIssue(sev_value, check, message) - self._issues.append(c) + # self._issues.append(c) + bisect.insort(self._issues, c) return c def add_error(self, message: str, check: RequirementCheck) -> CheckIssue: From 9994ffaab6786dcd4715ad92f0428a8e29a157a5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 1 May 2024 17:10:20 +0200 Subject: [PATCH 354/902] fix(shacl): :bug: missing defaults to configure SHACL validator --- rocrate_validator/models.py | 4 ++++ .../requirements/shacl/validator.py | 16 +++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 479e25b9..29dec715 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -754,8 +754,12 @@ class ValidationSettings: ontology_path: Optional[Path] = None inference: Optional[VALID_INFERENCE_OPTIONS_TYPES] = None # Validation strategy settings + advanced: bool = True # enable SHACL Advanced Validation inplace: Optional[bool] = False abort_on_first: Optional[bool] = True + inplace: Optional[bool] = False, + meta_shacl: bool = False, + iterate_rules: bool = True, # Requirement severity settings requirement_severity: Union[str, Severity] = Severity.REQUIRED requirement_severity_only: bool = False diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 6478ec0f..8bf25300 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -254,13 +254,19 @@ def ont_graph(self) -> Optional[Union[GraphLike, str, bytes]]: def validate( self, + # data to validate data_graph: Union[GraphLike, str, bytes], - advanced: Optional[bool] = False, + # validation settings + abort_on_first: Optional[bool] = True, + advanced: Optional[bool] = True, inference: Optional[VALID_INFERENCE_OPTIONS_TYPES] = None, inplace: Optional[bool] = False, - abort_on_first: Optional[bool] = False, + meta_shacl: bool = False, + iterate_rules: bool = True, + # SHACL validation severity allow_infos: Optional[bool] = False, allow_warnings: Optional[bool] = False, + # serialization settings serialization_output_path: Optional[str] = None, serialization_output_format: Optional[RDF_SERIALIZATION_FORMATS_TYPES] = "turtle", @@ -318,13 +324,13 @@ def validate( data_graph, shacl_graph=self.shapes_graph, ont_graph=self.ont_graph, - inference="owlrl" if self.ont_graph else None, + inference=inference if inference else "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=False, - iterate_rules=True, + meta_shacl=meta_shacl, + iterate_rules=iterate_rules, advanced=advanced, js=False, debug=False, From 19565e6f37d6c0201e580d0511f84eae2fecdb3c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 18:46:11 +0200 Subject: [PATCH 355/902] refactor(shacl): :recycle: generalize Shape as specialisation of the generic SHACLNode --- rocrate_validator/requirements/shacl/models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index bd83af3f..6b6ce58f 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) -class Shape: +class SHACLNode: # define default values name: str = None @@ -68,6 +68,11 @@ def __hash__(self): return self._hash + +class Shape(SHACLNode): + pass + + class PropertyShape(Shape): # define default values From d592af00fe4a59fb8f263a4467a0571a8688a4e4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 18:47:54 +0200 Subject: [PATCH 356/902] refactor(shacl): :recycle: generalise NodeShape as specialisation of Shape, NodeCollection --- .../requirements/shacl/models.py | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 6b6ce58f..d864ff5d 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -68,6 +68,34 @@ def __hash__(self): return self._hash +class SHACLNodeCollection(SHACLNode): + + def __init__(self, node: Node, graph: Graph, properties: list[PropertyShape] = None): + super().__init__(node, graph) + # store the properties + self._properties = properties if properties else [] + + @property + def properties(self) -> list[PropertyShape]: + """Return the properties of the shape""" + return self._properties.copy() + + def get_property(self, name) -> PropertyShape: + """Return the property of the shape with the given name""" + for prop in self._properties: + if prop.name == name: + return prop + return None + + + def add_property(self, property: PropertyShape): + """Add a property to the shape""" + self._properties.append(property) + + def remove_property(self, property: PropertyShape): + """Remove a property from the shape""" + self._properties.remove(property) + class Shape(SHACLNode): pass @@ -109,10 +137,7 @@ def parent(self) -> Optional[Shape]: class NodeShape(Shape): - def __init__(self, node: Node, graph: Graph, properties: list[PropertyShape] = None): - super().__init__(node, graph) - # store the properties - self._properties = properties if properties else [] +class NodeShape(Shape, SHACLNodeCollection): @property def properties(self) -> list[PropertyShape]: From a1e3fc2bf2c8795c6e8896252d274a30e37d5425 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 18:49:12 +0200 Subject: [PATCH 357/902] feat(shacl): :sparkles: add method to get the index of a property wrt a collection --- rocrate_validator/requirements/shacl/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index d864ff5d..cbc63e1e 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -87,6 +87,12 @@ def get_property(self, name) -> PropertyShape: return prop return None + def get_property_index(self, name) -> int: + """Return the index of the property with the given name""" + for i, prop in enumerate(self._properties): + if prop.name == name: + return i + return -1 def add_property(self, property: PropertyShape): """Add a property to the shape""" From f97d46e254342306f04533154ccbcb9ce6ef1c54 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 18:51:33 +0200 Subject: [PATCH 358/902] feat(shacl): :sparkles: add support to group shapes using the sh:group property --- .../requirements/shacl/models.py | 90 ++++++++++++++----- 1 file changed, 68 insertions(+), 22 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index cbc63e1e..a3142969 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -4,11 +4,14 @@ from pathlib import Path from typing import Optional, Union -from rdflib import Graph, Namespace +from rdflib import Graph, Namespace, URIRef from rdflib.term import Node -from ...constants import SHACL_NS -from .utils import ShapesList, compute_hash, inject_attributes +from rocrate_validator.constants import SHACL_NS +from rocrate_validator.models import LevelCollection, RequirementLevel +from rocrate_validator.requirements.shacl.utils import (ShapesList, + compute_hash, + inject_attributes) # set up logging logger = logging.getLogger(__name__) @@ -107,6 +110,10 @@ class Shape(SHACLNode): pass +class PropertyGroup(SHACLNodeCollection): + pass + + class PropertyShape(Shape): # define default values @@ -115,6 +122,8 @@ class PropertyShape(Shape): group: str = None defaultValue: str = None order: int = 0 + # store the reference to the property group + _property_group: PropertyGroup = None def __init__(self, node: Node, @@ -140,30 +149,32 @@ def parent(self) -> Optional[Shape]: """Return the parent shape of the shape property""" return self._parent + @property + def propertyGroup(self) -> PropertyGroup: + """Return the group of the shape property""" + return self._property_group -class NodeShape(Shape): class NodeShape(Shape, SHACLNodeCollection): @property - def properties(self) -> list[PropertyShape]: - """Return the properties of the shape""" - return self._properties.copy() - - def get_property(self, name) -> PropertyShape: - """Return the property of the shape with the given name""" - for prop in self._properties: - if prop.name == name: - return prop - return None + def property_groups(self) -> list[PropertyGroup]: + """Return the property groups of the shape""" + groups = set() + for prop in self.properties: + if prop.propertyGroup: + groups.add(prop.propertyGroup) + return list(groups) - def add_property(self, property: PropertyShape): - """Add a property to the shape""" - self._properties.append(property) + @property + def grouped_properties(self) -> list[PropertyShape]: + """Return the properties that are in a group""" + return [prop for prop in self.properties if prop.propertyGroup] - def remove_property(self, property: PropertyShape): - """Remove a property from the shape""" - self._properties.remove(property) + @property + def ungrouped_properties(self) -> list[PropertyShape]: + """Return the properties that are not in a group""" + return [prop for prop in self.properties if not prop.propertyGroup] class ShapesRegistry: @@ -207,8 +218,15 @@ def load_shapes(self, shapes_path: Union[str, Path], publicID: Optional[str] = N # list of instantiated shapes shapes = [] + # list of property groups + property_groups = {} + # register Node Shapes for node_shape in shapes_list.node_shapes: + # flag to check if the nested properties are in a group + grouped = False + # list of properties ungroupped + ungrouped_properties = [] # get the shape graph node_graph = shapes_list.get_shape_graph(node_shape) # create a node shape object @@ -221,10 +239,24 @@ def load_shapes(self, shapes_path: Union[str, Path], publicID: Optional[str] = N p_shape = PropertyShape( property_shape, property_graph, shape) shape.add_property(p_shape) + group = __process_property_group__(property_groups, p_shape) + if group and not group in shapes: + grouped = True + shapes.append(group) + if not group: + ungrouped_properties.append(p_shape) + + # store the property shape in the registry self.add_shape(p_shape) - # store the node shape + # store the node shape in the registry self.add_shape(shape) - shapes.append(shape) + + # ย store the node in the list of shapes + if not grouped: + shapes.append(shape) + else: + for prop in ungrouped_properties: + shapes.append(prop) # register Property Shapes for property_shape in shapes_list.property_shapes: @@ -251,3 +283,17 @@ def get_instance(cls, ctx: object): instance = cls() setattr(ctx, "_shapes_registry_instance", instance) return instance + + +def __process_property_group__(groups: dict[str, PropertyGroup], property_shape: PropertyShape) -> PropertyGroup: + group_name = property_shape.group + if group_name: + logger.warning("Type if property shape group: %s", type(property_shape.group)) + if group_name not in groups: + groups[group_name] = PropertyGroup(URIRef(property_shape.group), property_shape.graph) + property_shape.graph.serialize("logs/property_shape.ttl", format="turtle") + logger.error("Group: %s", groups[group_name].name) + groups[group_name].add_property(property_shape) + property_shape._property_group = groups[group_name] + return groups[group_name] + return None From 2fc39fc09fef5bf70e40aa52759f85a7b13be1d9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 18:54:08 +0200 Subject: [PATCH 359/902] fix(shacl): :goal_net: notify error when a shape cannot be fetched --- rocrate_validator/requirements/shacl/models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index a3142969..be6ddae6 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -188,7 +188,12 @@ def add_shape(self, shape: Shape): self._shapes[f"{hash(shape)}"] = shape def get_shape(self, hash_value: int) -> Optional[Shape]: - return self._shapes.get(f"{hash_value}", None) + logger.debug("Searching for shape %s in the registry: %s", hash_value, self._shapes) + result = self._shapes.get(f"{hash_value}", None) + if not result: + logger.debug(f"Shape {hash_value} not found in the registry") + raise ValueError(f"Shape not found in the registry: {hash_value}") + return result def get_shape_by_name(self, name: str) -> Optional[Shape]: for shape in self._shapes.values(): From 79668ae2cf1986991e3c2853a6144e8bd6983633 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 18:55:47 +0200 Subject: [PATCH 360/902] feat(shacl): :sparkles: map Shape sh:severity to a concrete requirement level --- rocrate_validator/requirements/shacl/models.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index be6ddae6..73762b83 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -45,6 +45,19 @@ def graph(self): """Return the subgraph of the shape""" return self._graph + @property + def level(self) -> RequirementLevel: + """Return the requirement level of the shape""" + severity = getattr(self, "severity", None) + if not severity: + return LevelCollection.REQUIRED + if severity == f"{SHACL_NS}Violation": + return LevelCollection.REQUIRED + elif severity == f"{SHACL_NS}Warning": + return LevelCollection.RECOMMENDED + elif severity == f"{SHACL_NS}Info": + return LevelCollection.OPTIONAL + def __str__(self): class_name = self.__class__.__name__ if self.name and self.description: From 1095069fb123fc0183b48f402df9b41cc692a2dc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 19:00:52 +0200 Subject: [PATCH 361/902] feat(core): :sparkles: make folder structure optional Shapes constraint can be placed everywhere in the profile folder. The actual severity level will be determinated using the sh:property on the node --- rocrate_validator/models.py | 10 +++++++--- rocrate_validator/requirements/shacl/requirements.py | 7 +++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 29dec715..032a851a 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -464,9 +464,13 @@ def ok_file(p: Path) -> bool: req_id = 0 requirements = [] for requirement_path in files: - requirement_level = LevelCollection.get(requirement_path.parent.name) - if requirement_level.severity < severity: - continue + requirement_level = None + try: + requirement_level = LevelCollection.get(requirement_path.parent.name) + if requirement_level.severity < severity: + continue + except AttributeError: + logger.debug("The requirement level could not be determined from the path: %s", requirement_path) requirement_loader = RequirementLoader.__get_requirement_loader__(profile, requirement_path) for requirement in requirement_loader.load( profile, requirement_level, diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index 09bbd34b..beed3fa0 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -48,6 +48,13 @@ def __init_checks__(self) -> list[RequirementCheck]: def shape(self) -> Shape: return self._shape + @property + def level(self) -> RequirementLevel: + level = super().level + if level is None: + return self.shape.level + return level + class SHACLRequirementLoader(RequirementLoader): From 20679f60123a832c341bcc3dba6c9b1a082b72d0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 19:04:22 +0200 Subject: [PATCH 362/902] refactor(shacl): :loud_sound: udpate logs --- rocrate_validator/requirements/shacl/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index a874dccd..9d7d1aeb 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -57,14 +57,16 @@ def make_uris_relative(text: str, ro_crate_path: Union[Path, str]) -> str: def inject_attributes(obj: object, node_graph: Graph, node: Node) -> object: # inject attributes of the shape property - for node, p, o in node_graph.triples((node, None, None)): + # logger.debug("Injecting attributes of node %s", node) + triples = node_graph.triples((node, None, None)) + for node, p, o in triples: predicate_as_string = p.toPython() # logger.debug(f"Processing {predicate_as_string} of property graph {node}") if predicate_as_string.startswith(SHACL_NS): property_name = predicate_as_string.split("#")[-1] setattr(obj, property_name, o.toPython()) # logger.debug("Injected attribute %s: %s", property_name, o.toPython()) - + # logger.debug("Injected attributes ig node %s: %s", node, len(list(triples))) # return the object return obj From 839312bdb08c2c7f23e7b982834e19682904854d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 19:06:08 +0200 Subject: [PATCH 363/902] feat(utils): :goal_net: signal syntax errors when bad syntax occurs --- rocrate_validator/errors.py | 30 +++++++++++++++++++ rocrate_validator/requirements/shacl/utils.py | 18 ++++++----- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 1a7e8199..db4796ef 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -42,6 +42,36 @@ def __repr__(self): return f"InvalidSerializationFormat({self._format!r})" +class BadSyntaxError(ROCValidatorError): + """Raised when a syntax error occurs.""" + + def __init__(self, message, path: str = ".", code: int = -1): + self._message = message + self._path = path + self._code = code + + @property + def message(self) -> str: + """The error message.""" + return self._message + + @property + def path(self) -> str: + """The path where the error occurred.""" + return self._path + + @property + def code(self) -> int: + """The error code.""" + return self._code + + def __str__(self) -> str: + return self._message + + def __repr__(self): + return f"BadSyntaxError({self._message!r}, {self._path!r})" + + class ValidationError(ROCValidatorError): """Raised when a validation error occurs.""" diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index 9d7d1aeb..f7925c67 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -9,6 +9,7 @@ from rdflib.term import Node from rocrate_validator.constants import RDF_SYNTAX_NS, SHACL_NS +from rocrate_validator.errors import BadSyntaxError from rocrate_validator.models import Severity # set up logging @@ -212,13 +213,16 @@ def __extract_related_triples__(graph, subject_node): def load_shapes_from_file(file_path: str, publicID: str = None) -> ShapesList: - # 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) - # Extract the shapes from the graph - return load_shapes_from_graph(g) + try: + # 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) + # Extract the shapes from the graph + return load_shapes_from_graph(g) + except Exception as e: + raise BadSyntaxError(str(e), file_path) from e def load_shapes_from_graph(g: Graph) -> ShapesList: From 955b036228e9d6ef6112e3e326651f8fa5c3aff8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 19:09:51 +0200 Subject: [PATCH 364/902] fix(shacl): :adhesive_bandage: update the ontology loader functionality --- rocrate_validator/requirements/shacl/validator.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 8bf25300..cdaf2909 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -32,8 +32,6 @@ def __init__(self, context: ValidationContext): self._base_context: ValidationContext = context # reference to the ontology path self._ontology_path: Path = None - # reference to the graph of shapes - self._ontology_graph: Graph = None @property def base_context(self) -> ValidationContext: @@ -75,10 +73,12 @@ def __load_ontology_graph__(self): @property def ontology_graph(self) -> Graph: - if self._ontology_graph is None: - # load the graph of ontologies - self._ontology_graph = self.__load_ontology_graph__() - return self._ontology_graph + ontology_key = "_shacl_ontology" + ontology = getattr(self.base_context, ontology_key, None) + if not ontology: + ontology = self.__load_ontology_graph__() + setattr(self.base_context, ontology_key, ontology) + return ontology @ classmethod def get_instance(cls, context: ValidationContext) -> SHACLValidationContext: From c2908b3b6660edb311076d94111d0e46e7083488 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 19:11:42 +0200 Subject: [PATCH 365/902] feat(core): :sparkles: allow to get requirement checks by severity level --- rocrate_validator/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 032a851a..656f2eb3 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -346,6 +346,9 @@ def get_check(self, name: str) -> Optional[RequirementCheck]: return check return None + def get_checks_by_level(self, level: RequirementLevel) -> list[RequirementCheck]: + return [check for check in self._checks if check.level.severity == level.severity] + def __reorder_checks__(self) -> None: for i, check in enumerate(self._checks): check.order_number = i + 1 From 1107e3fc0dc60ada9e433297d83b2faf85ece56a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 19:13:07 +0200 Subject: [PATCH 366/902] refactor(cli): :lipstick: update colors and styles of validation results --- rocrate_validator/cli/commands/validate.py | 12 ++++++------ rocrate_validator/colors.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 03277d4f..a334064e 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -157,11 +157,11 @@ def __print_validation_result__( key=lambda x: (-x.severity.value, x)): issue_color = get_severity_color(requirement.severity) console.print( - Align(f" [severity: [{issue_color}]{requirement.severity.name}[/{issue_color}], " - f"profile: [magenta]{requirement.profile.name }[/magenta]]", align="right") + Align(f"\n [severity: [{issue_color}]{requirement.severity.name}[/{issue_color}], " + f"profile: [magenta bold]{requirement.profile.name }[/magenta bold]]", align="right") ) console.print( - f" [bold][magenta][{requirement.order_number}] [u]{requirement.name}[/u][/magenta][/bold]", + f" [bold][cyan][{requirement.order_number}] [u]{requirement.name}[/u][/cyan][/bold]", style="white", ) console.print(f"\n{' '*4}{requirement.description}\n", style="white italic") @@ -172,11 +172,11 @@ def __print_validation_result__( issue_color = get_severity_color(check.level.severity) console.print( f"{' '*4}- " - f"[magenta]{check.name}[/magenta]: {check.description}") + f"[magenta bold]{check.name}[/magenta bold]: {check.description}") console.print(f"\n{' '*6}Detected issues:", style="white bold") for issue in sorted(result.get_issues_by_check(check), key=lambda x: (-x.severity.value, x)): console.print( - f"{' '*6}- [[{issue_color}]Violation[/{issue_color}] of " - f"[magenta]{issue.check.identifier}[/magenta]]: {issue.message}") + f"{' '*6}- [[red]Violation[/red] of " + f"[{issue_color} bold]{issue.check.identifier}[/{issue_color} bold]]: {issue.message}") console.print("\n", style="white") diff --git a/rocrate_validator/colors.py b/rocrate_validator/colors.py index aeab707a..826d0674 100644 --- a/rocrate_validator/colors.py +++ b/rocrate_validator/colors.py @@ -13,7 +13,7 @@ def get_severity_color(severity: Union[str, Severity]) -> str: if severity == Severity.REQUIRED or severity == "REQUIRED": return "red" elif severity == Severity.RECOMMENDED or severity == "RECOMMENDED": - return "orange" + return "orange1" elif severity == Severity.OPTIONAL or severity == "OPTIONAL": return "yellow" else: @@ -31,10 +31,10 @@ def get_req_level_color(level: LevelCollection) -> str: elif level in (LevelCollection.MUST_NOT, LevelCollection.SHALL_NOT): return "purple" elif level in (LevelCollection.SHOULD, LevelCollection.RECOMMENDED): - return "yellow" + return "orange1" elif level == LevelCollection.SHOULD_NOT: return "lightyellow" elif level in (LevelCollection.MAY, LevelCollection.OPTIONAL): - return "orange" + return "yellow" else: return "white" From 71e9470f38460f930e9a470d78f6efafa416fd51 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 19:15:59 +0200 Subject: [PATCH 367/902] refactor(cli): :lipstick: update colors and styles of the `profiles describe` command --- rocrate_validator/cli/commands/profiles.py | 48 ++++++++++++++++++---- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 018b4147..206e892f 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -4,12 +4,12 @@ from rich.markdown import Markdown from rich.table import Table +from rocrate_validator import services +from rocrate_validator.cli.main import cli, click +from rocrate_validator.colors import get_severity_color from rocrate_validator.constants import DEFAULT_PROFILE_NAME - -from ... import services -from ...colors import get_severity_color -from ...utils import get_profiles_path -from ..main import cli, click +from rocrate_validator.models import LevelCollection, Requirement, RequirementLevel +from rocrate_validator.utils import get_profiles_path # set the default profiles path DEFAULT_PROFILES_PATH = get_profiles_path() @@ -98,6 +98,15 @@ def describe_profile(ctx, console.print(Markdown(profile.description.strip())) console.print("\n", style="white bold") + +def __requirement_level_style__(requirement: RequirementLevel): + """ + Format the requirement level + """ + color = get_severity_color(requirement.severity) + return f"{color} bold" + + table_rows = [] levels_list = set() for requirement in profile.requirements: @@ -106,8 +115,33 @@ def describe_profile(ctx, levels_list.add(level_info) table_rows.append((str(requirement.order_number), requirement.name, Markdown(requirement.description.strip()), - str(len(requirement.get_checks())), - level_info)) + f"{len(requirement.get_checks_by_level(LevelCollection.REQUIRED))}", + f"{len(requirement.get_checks_by_level(LevelCollection.RECOMMENDED))}", + f"{len(requirement.get_checks_by_level(LevelCollection.OPTIONAL))}")) + + table = Table(show_header=True, + title="Profile Requirements", + title_style="italic bold", + header_style="bold cyan", + border_style="bright_black", + show_footer=False, + show_lines=True, + caption=f"(*) number of checks by severity level: {', '.join(levels_list)}", + caption_style="italic bold") + + # Define columns + table.add_column("#", style="cyan bold", justify="right") + table.add_column("Name", style="magenta 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") + table.add_column("# OPTIONAL", style=__requirement_level_style__(LevelCollection.OPTIONAL), justify="center") + # Add data to the table + for row in table_rows: + table.add_row(*row) + # Print the table + console.print(table) + table = Table(show_header=True, title="Profile Requirements", From b9f38bedc9a7f6b9123b21b5eb531e98fc11af0c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 19:17:23 +0200 Subject: [PATCH 368/902] feat(cli): :sparkles: enable verbose mode for the `profiles describe` command --- rocrate_validator/cli/commands/profiles.py | 50 +++++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 206e892f..9616f344 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -80,11 +80,20 @@ def list_profiles(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH): @profiles.command("describe") +@click.option( + '-v', + '--verbose', + is_flag=True, + help="Show detailed list of requirements", + default=False, + show_default=True +) @click.argument("profile-name", type=click.STRING, default=DEFAULT_PROFILE_NAME, required=True) @click.pass_context def describe_profile(ctx, profile_name: str = DEFAULT_PROFILE_NAME, - profiles_path: Path = DEFAULT_PROFILES_PATH): + profiles_path: Path = DEFAULT_PROFILES_PATH, + verbose: bool = False): """ Show a profile """ @@ -98,6 +107,11 @@ def describe_profile(ctx, console.print(Markdown(profile.description.strip())) console.print("\n", style="white bold") + if not verbose: + __compacted_describe_profile__(console, profile) + else: + __verbose_describe_profile__(console, profile) + def __requirement_level_style__(requirement: RequirementLevel): """ @@ -107,6 +121,10 @@ def __requirement_level_style__(requirement: RequirementLevel): return f"{color} bold" +def __compacted_describe_profile__(console, profile): + """ + Show a profile in a compact way + """ table_rows = [] levels_list = set() for requirement in profile.requirements: @@ -143,19 +161,39 @@ def __requirement_level_style__(requirement: RequirementLevel): console.print(table) +def __verbose_describe_profile__(console, profile): + """ + Show a profile in a verbose way + """ + table_rows = [] + levels_list = set() + for requirement in profile.requirements: + + 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) + logger.debug("Check %s: %s", check.name, check.description) + # checks.append(check) + table_rows.append((str(check.identifier).rjust(14), check.name, + Markdown(check.description.strip()), level_info)) + table = Table(show_header=True, title="Profile Requirements", + title_style="italic bold", header_style="bold cyan", border_style="bright_black", show_footer=False, - caption=f"(*) Requirement level: {', '.join(levels_list)}") + show_lines=True, + caption=f"(*) severity level of requirement: {', '.join(levels_list)}", + caption_style="italic bold") # Define columns - table.add_column("#", style="yellow bold", justify="right") - table.add_column("Name", style="magenta bold", justify="center") + table.add_column("Identifier", style="yellow bold", justify="right") + table.add_column("Name", style="magenta bold", justify="left") table.add_column("Description", style="white italic") - table.add_column("# Checks", style="white", justify="center") - table.add_column("Requirement Level (*)", style="white", justify="center") + table.add_column("Severity (*)", style="bold", justify="center") + # Add data to the table for row in table_rows: table.add_row(*row) From 83d0823c36edeb4bc319011873f30dca1a854e0b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 19:20:46 +0200 Subject: [PATCH 369/902] fix(shacl): :mute: remove logs --- rocrate_validator/requirements/shacl/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 73762b83..103402f1 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -306,11 +306,9 @@ def get_instance(cls, ctx: object): def __process_property_group__(groups: dict[str, PropertyGroup], property_shape: PropertyShape) -> PropertyGroup: group_name = property_shape.group if group_name: - logger.warning("Type if property shape group: %s", type(property_shape.group)) if group_name not in groups: groups[group_name] = PropertyGroup(URIRef(property_shape.group), property_shape.graph) property_shape.graph.serialize("logs/property_shape.ttl", format="turtle") - logger.error("Group: %s", groups[group_name].name) groups[group_name].add_property(property_shape) property_shape._property_group = groups[group_name] return groups[group_name] From eaf3a506400f2532935587d9ff683706bb486632 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 19:34:00 +0200 Subject: [PATCH 370/902] refactor(cli): :lipstick: update colors on footnotes --- rocrate_validator/cli/commands/profiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 9616f344..4d119381 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -185,7 +185,7 @@ def __verbose_describe_profile__(console, profile): border_style="bright_black", show_footer=False, show_lines=True, - caption=f"(*) severity level of requirement: {', '.join(levels_list)}", + caption=f"[cyan](*)[/cyan] severity level of requirement: {', '.join(levels_list)}", caption_style="italic bold") # Define columns From 8af326873a854840e200fb90b9a637bae683e765 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 19:35:34 +0200 Subject: [PATCH 371/902] fix(cli): :lipstick: update color of the check identifier --- rocrate_validator/cli/commands/profiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 4d119381..d3becea3 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -189,7 +189,7 @@ def __verbose_describe_profile__(console, profile): caption_style="italic bold") # Define columns - table.add_column("Identifier", style="yellow bold", justify="right") + table.add_column("Identifier", style="cyan bold", justify="right") table.add_column("Name", style="magenta bold", justify="left") table.add_column("Description", style="white italic") table.add_column("Severity (*)", style="bold", justify="center") From b626ed6804593055d21d6d5a2aed944efa357b55 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 19:47:14 +0200 Subject: [PATCH 372/902] fix(cli): :lipstick: update title and footnote of the main table --- rocrate_validator/cli/commands/profiles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index d3becea3..33540ee3 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -179,13 +179,13 @@ def __verbose_describe_profile__(console, profile): Markdown(check.description.strip()), level_info)) table = Table(show_header=True, - title="Profile Requirements", + title="Profile Requirements Checks", title_style="italic bold", header_style="bold cyan", border_style="bright_black", show_footer=False, show_lines=True, - caption=f"[cyan](*)[/cyan] severity level of requirement: {', '.join(levels_list)}", + caption=f"[cyan](*)[/cyan] severity level of requirement check: {', '.join(levels_list)}", caption_style="italic bold") # Define columns From 273447677fdc110528ed75052b860020506f48c6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sat, 4 May 2024 19:48:19 +0200 Subject: [PATCH 373/902] fix(cli): :lipstick: update colors --- rocrate_validator/cli/commands/profiles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 33540ee3..effb47ac 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -50,7 +50,7 @@ def list_profiles(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH): header_style="bold cyan", border_style="bright_black", show_footer=False, - caption="(*) Number of requirements by severity") + caption="[cyan](*)[/cyan] Number of requirements by severity") # Define columns table.add_column("Name", style="magenta bold", justify="right") @@ -144,7 +144,7 @@ def __compacted_describe_profile__(console, profile): border_style="bright_black", show_footer=False, show_lines=True, - caption=f"(*) number of checks by severity level: {', '.join(levels_list)}", + caption=f"[cyan](*)[/cyan] number of checks by severity level: {', '.join(levels_list)}", caption_style="italic bold") # Define columns From b9debbb406c0f3a4726121f628a28599f62d20aa Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 6 May 2024 08:35:48 +0200 Subject: [PATCH 374/902] fix(shacl): :bug: allow skipping properties from auto-injection --- rocrate_validator/requirements/shacl/utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index f7925c67..6659ff3f 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -59,13 +59,19 @@ def make_uris_relative(text: str, ro_crate_path: Union[Path, str]) -> str: def inject_attributes(obj: object, node_graph: Graph, node: Node) -> object: # inject attributes of the shape property # logger.debug("Injecting attributes of node %s", node) + skip_properties = ["node"] triples = node_graph.triples((node, None, None)) for node, p, o in triples: predicate_as_string = p.toPython() # logger.debug(f"Processing {predicate_as_string} of property graph {node}") if predicate_as_string.startswith(SHACL_NS): property_name = predicate_as_string.split("#")[-1] - setattr(obj, property_name, o.toPython()) + if property_name in skip_properties: + continue + try: + setattr(obj, property_name, o.toPython()) + except AttributeError as e: + logger.error(f"Error injecting attribute {property_name}: {e}") # logger.debug("Injected attribute %s: %s", property_name, o.toPython()) # logger.debug("Injected attributes ig node %s: %s", node, len(list(triples))) # return the object From f5c396dde85534194908f4faea79641b43c03705 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 6 May 2024 08:46:31 +0200 Subject: [PATCH 375/902] fix(utils): :pencil2: remove wrong commas --- rocrate_validator/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 656f2eb3..ea58e3ed 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -764,9 +764,9 @@ class ValidationSettings: advanced: bool = True # enable SHACL Advanced Validation inplace: Optional[bool] = False abort_on_first: Optional[bool] = True - inplace: Optional[bool] = False, - meta_shacl: bool = False, - iterate_rules: bool = True, + inplace: Optional[bool] = False + meta_shacl: bool = False + iterate_rules: bool = True # Requirement severity settings requirement_severity: Union[str, Severity] = Severity.REQUIRED requirement_severity_only: bool = False From 767671df5a8f1e2dd64c8facf2da1d89c0b6190e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 8 May 2024 12:20:38 +0200 Subject: [PATCH 376/902] fix(test-conf): :bug: apply the proper severity in tests --- tests/shared.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/shared.py b/tests/shared.py index 0a227bb9..815f488a 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -53,7 +53,7 @@ def do_entity_test( result: models.ValidationResult = \ services.validate(models.ValidationSettings(**{ "data_path": rocrate_path, - "requirement_severity": models.Severity.OPTIONAL, + "requirement_severity": requirement_severity, "abort_on_first": abort_on_first })) logger.debug("Expected validation result: %s", expected_validation_result) From 04093cb13b4b6faa3eb5835e796ac866071f0cf9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 24 May 2024 10:37:51 +0200 Subject: [PATCH 377/902] fix(core): :bug: don't reverse the profiles order by default --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index ea58e3ed..89719a6c 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -246,7 +246,7 @@ def load(path: Union[str, Path], def load_profiles(profiles_path: Union[str, Path], publicID: Optional[str] = None, severity: Severity = Severity.REQUIRED, - reverse_order: bool = True) -> OrderedDict[str, Profile]: + reverse_order: bool = False) -> OrderedDict[str, Profile]: # if the path is a string, convert it to a Path if isinstance(profiles_path, str): profiles_path = Path(profiles_path) From f6ff327dcc67be0b36f47f9654c4aecdf763b500 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 24 May 2024 10:42:32 +0200 Subject: [PATCH 378/902] test(test-conf): :wrench: fix path of profiles --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index e51483cc..499ff1d4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,7 @@ TEST_DATA_PATH = os.path.abspath(os.path.join(CURRENT_PATH, "data")) # profiles paths -PROFILES_PATH = f"{CURRENT_PATH}/../profiles" +PROFILES_PATH = os.path.abspath(f"{CURRENT_PATH}/../rocrate_validator/profiles") @fixture From e90c314bf563bdbfb4bae1ed9591bc3b9b32a92d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 24 May 2024 10:44:11 +0200 Subject: [PATCH 379/902] test(profiles): :white_check_mark: check order of loaded profiles --- tests/unit/requirements/test_profiles.py | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/unit/requirements/test_profiles.py diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py new file mode 100644 index 00000000..4ac48d9d --- /dev/null +++ b/tests/unit/requirements/test_profiles.py @@ -0,0 +1,30 @@ +import logging +import os + +from rocrate_validator.models import Profile +from tests.ro_crates import InvalidFileDescriptorEntity + +# set up logging +logger = logging.getLogger(__name__) + +# ย Global set up the paths +paths = InvalidFileDescriptorEntity() + + +def test_order_of_loaded_profiles(profiles_path: str): + """Test the order of the loaded profiles.""" + logger.error("The profiles path: %r", profiles_path) + assert os.path.exists(profiles_path) + profiles = Profile.load_profiles(profiles_path=profiles_path) + # The number of profiles should be greater than 0 + assert len(profiles) > 0 + + # Extract the profile names + profile_names = [profile for profile in profiles] + logger.debug("The profile names: %r", profile_names) + + # The order of the profiles should be the same as the order of the directories + # in the profiles directory + profile_directories = os.listdir(profiles_path) + logger.debug("The profile directories: %r", profile_directories) + assert profile_names == profile_directories From 90d490d4fe783d06a81fd85e80eaa5af9c94501c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 24 May 2024 11:43:33 +0200 Subject: [PATCH 380/902] fix(core): :sparkles: load the right set of profiles on validation context --- rocrate_validator/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 89719a6c..dd2d8d5e 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -963,11 +963,11 @@ def __load_profiles__(self) -> OrderedDict[str, Profile]: publicID=self.publicID, severity=self.requirement_severity) return {profile.name: profile} - return Profile.load_profiles( + return [p for p in Profile.load_profiles( self.profiles_path, publicID=self.publicID, severity=self.requirement_severity, - reverse_order=False) + reverse_order=False) if p <= self.profile_name] @property def profiles(self) -> OrderedDict[str, Profile]: From a032630adae73ee60ec981ac040dcb1e287a30e1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 24 May 2024 12:14:46 +0200 Subject: [PATCH 381/902] fix(core): :bug: read the right property `inherit_profiles` from settings --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index dd2d8d5e..694a53c1 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -946,7 +946,7 @@ def data_graph(self) -> Graph: @property def inheritance_enabled(self) -> bool: - return not self.settings.get("disable_profile_inheritance", False) + return self.settings.get("inherit_profiles", False) @property def profiles_path(self) -> Path: From 01b0622c1d6eff4123a5507655a124ffd22da88f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 24 May 2024 12:17:38 +0200 Subject: [PATCH 382/902] feat(core): :sparkles: expose profiles_path on ValidationContext --- rocrate_validator/models.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 694a53c1..2b3ec086 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -902,6 +902,13 @@ def publicID(self) -> str: return f"{path}/" return path + @property + def profiles_path(self) -> Path: + profiles_path = self.settings.get("profiles_path") + if isinstance(profiles_path, str): + profiles_path = Path(profiles_path) + return profiles_path + @property def requirement_severity(self) -> Severity: return self.settings.get("requirement_severity", Severity.REQUIRED) @@ -948,10 +955,6 @@ def data_graph(self) -> Graph: def inheritance_enabled(self) -> bool: return self.settings.get("inherit_profiles", False) - @property - def profiles_path(self) -> Path: - return self.settings.get("profiles_path") - @property def profile_name(self) -> str: return self.settings.get("profile_name") From 195dbeaed75fc339d5d0df697a9fe0e24edd916d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 24 May 2024 12:28:51 +0200 Subject: [PATCH 383/902] refactor(core): :sparkles: improve profile loader without inheritance --- rocrate_validator/errors.py | 16 ++++++++++++++++ rocrate_validator/models.py | 6 ++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index db4796ef..cc087c6b 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -24,6 +24,22 @@ def __repr__(self): return f"ProfilesDirectoryNotFound({self._profiles_path!r})" +class InvalidProfilePath(ROCValidatorError): + """Raised when an invalid profile path is provided.""" + + def __init__(self, profile_path: Optional[str] = None): + self._profile_path = profile_path + + @property + def profile_path(self) -> Optional[str]: + """The invalid profile path.""" + return self._profile_path + + def __str__(self) -> str: + return f"Invalid profile path: {self._profile_path!r}" + + def __repr__(self): + return f"InvalidProfilePath({self._profile_path!r})" class InvalidSerializationFormat(ROCValidatorError): """Raised when an invalid serialization format is provided.""" diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 2b3ec086..f60604de 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -22,6 +22,7 @@ RDF_SERIALIZATION_FORMATS_TYPES, ROCRATE_METADATA_FILE, VALID_INFERENCE_OPTIONS_TYPES) +from rocrate_validator.errors import InvalidProfilePath from rocrate_validator.utils import (get_profiles_path, get_requirement_name_from_file) @@ -236,7 +237,8 @@ def load(path: Union[str, Path], if isinstance(path, str): path = Path(path) # check if the path is a directory - assert path.is_dir(), f"Invalid profile path: {path}" + if not path.is_dir(): + raise InvalidProfilePath(path) # create a new profile profile = Profile(name=path.name, path=path, publicID=publicID, severity=severity) logger.debug("Loaded profile: %s", profile) @@ -962,7 +964,7 @@ def profile_name(self) -> str: def __load_profiles__(self) -> OrderedDict[str, Profile]: if not self.inheritance_enabled: profile = Profile.load( - self.profile_name, + self.profiles_path / self.profile_name, publicID=self.publicID, severity=self.requirement_severity) return {profile.name: profile} From f1333200a43da952ff5fa16cbe1e85936d329f36 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 24 May 2024 13:14:23 +0200 Subject: [PATCH 384/902] fix(core): :bug: fix loading of inherited profiles --- rocrate_validator/errors.py | 20 ++++++++++++++++++++ rocrate_validator/models.py | 8 ++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index cc087c6b..1c017508 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -40,6 +40,26 @@ def __str__(self) -> str: def __repr__(self): return f"InvalidProfilePath({self._profile_path!r})" + + +class ProfileNotFound(ROCValidatorError): + """Raised when a profile is not found.""" + + def __init__(self, profile_name: Optional[str] = None): + self._profile_name = profile_name + + @property + def profile_name(self) -> Optional[str]: + """The name of the profile.""" + return self._profile_name + + def __str__(self) -> str: + return f"Profile not found: {self._profile_name!r}" + + def __repr__(self): + return f"ProfileNotFound({self._profile_name!r})" + + class InvalidSerializationFormat(ROCValidatorError): """Raised when an invalid serialization format is provided.""" diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index f60604de..0990b820 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -22,7 +22,7 @@ RDF_SERIALIZATION_FORMATS_TYPES, ROCRATE_METADATA_FILE, VALID_INFERENCE_OPTIONS_TYPES) -from rocrate_validator.errors import InvalidProfilePath +from rocrate_validator.errors import InvalidProfilePath, ProfileNotFound from rocrate_validator.utils import (get_profiles_path, get_requirement_name_from_file) @@ -968,11 +968,15 @@ def __load_profiles__(self) -> OrderedDict[str, Profile]: publicID=self.publicID, severity=self.requirement_severity) return {profile.name: profile} - return [p for p in Profile.load_profiles( + profiles = [p for p in Profile.load_profiles( self.profiles_path, publicID=self.publicID, severity=self.requirement_severity, reverse_order=False) if p <= self.profile_name] + # Check if the target profile is in the list of profiles + if self.profile_name not in profiles: + raise ProfileNotFound(f"Profile '{self.profile_name}' not found in '{self.profiles_path}'") + return profiles @property def profiles(self) -> OrderedDict[str, Profile]: From 1356f75a6c8c2e827031c3130da866a26dfd2eb4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 24 May 2024 13:16:15 +0200 Subject: [PATCH 385/902] test(core): :white_check_mark: add unit tests of validation context profiles --- tests/conftest.py | 5 ++ tests/data/profiles/fake/a/shape_a.ttl | 22 +++++++ tests/data/profiles/fake/b/shape_b.ttl | 22 +++++++ tests/data/profiles/fake/c/shape_c.ttl | 22 +++++++ tests/unit/requirements/test_profiles.py | 83 +++++++++++++++++++++++- 5 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 tests/data/profiles/fake/a/shape_a.ttl create mode 100644 tests/data/profiles/fake/b/shape_b.ttl create mode 100644 tests/data/profiles/fake/c/shape_c.ttl diff --git a/tests/conftest.py b/tests/conftest.py index 499ff1d4..127666ab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,6 +29,11 @@ def ro_crates_path(): return f"{TEST_DATA_PATH}/crates" +@fixture +def fake_profiles_path(): + return f"{TEST_DATA_PATH}/profiles/fake" + + @fixture def graphs_path(): return f"{TEST_DATA_PATH}/graphs" diff --git a/tests/data/profiles/fake/a/shape_a.ttl b/tests/data/profiles/fake/a/shape_a.ttl new file mode 100644 index 00000000..97f898ad --- /dev/null +++ b/tests/data/profiles/fake/a/shape_a.ttl @@ -0,0 +1,22 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +ro:ShapeA + a sh:NodeShape ; + sh:name "The Shape A" ; + sh:description "This is the Shape A" ; + sh:targetNode ro:ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check Metadata File Descriptor entity existence" ; + sh:description "Check if the RO-Crate Metadata File Descriptor entity exists" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + ] . diff --git a/tests/data/profiles/fake/b/shape_b.ttl b/tests/data/profiles/fake/b/shape_b.ttl new file mode 100644 index 00000000..328fb881 --- /dev/null +++ b/tests/data/profiles/fake/b/shape_b.ttl @@ -0,0 +1,22 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +ro:ShapeB + a sh:NodeShape ; + sh:name "The Shape B" ; + sh:description "This is the Shape B" ; + sh:targetNode ro:ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check Metadata File Descriptor entity existence" ; + sh:description "Check if the RO-Crate Metadata File Descriptor entity exists" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + ] . diff --git a/tests/data/profiles/fake/c/shape_c.ttl b/tests/data/profiles/fake/c/shape_c.ttl new file mode 100644 index 00000000..cb9a0b59 --- /dev/null +++ b/tests/data/profiles/fake/c/shape_c.ttl @@ -0,0 +1,22 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +ro:ShapeC + a sh:NodeShape ; + sh:name "The Shape C" ; + sh:description "This is the Shape C" ; + sh:targetNode ro:ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check Metadata File Descriptor entity existence" ; + sh:description "Check if the RO-Crate Metadata File Descriptor entity exists" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + ] . diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 4ac48d9d..862f97d4 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -1,7 +1,11 @@ import logging import os -from rocrate_validator.models import Profile +import pytest + +from rocrate_validator.errors import InvalidProfilePath +from rocrate_validator.models import (Profile, ValidationContext, + ValidationSettings, Validator) from tests.ro_crates import InvalidFileDescriptorEntity # set up logging @@ -28,3 +32,80 @@ def test_order_of_loaded_profiles(profiles_path: str): profile_directories = os.listdir(profiles_path) logger.debug("The profile directories: %r", profile_directories) assert profile_names == profile_directories + + +def test_load_invalid_profile_from_validation_context(fake_profiles_path: str): + """Test the loaded profiles from the validator context.""" + settings = { + "profiles_path": fake_profiles_path, + "profile_name": "ro-crate", + "data_path": "/tmp/random_path", + "inherit_profiles": False + } + + settings = ValidationSettings(**settings) + assert not settings.inherit_profiles, "The inheritance mode should be set to False" + + validator = Validator(settings) + # initialize the validation context + context = ValidationContext(validator, validator.validation_settings.to_dict()) + + # Check if the InvalidProfilePath exception is raised + with pytest.raises(InvalidProfilePath): + profiles = context.profiles + logger.debug("The profiles: %r", profiles) + + +def test_load_valid_profile_without_inheritance_from_validation_context(fake_profiles_path: str): + """Test the loaded profiles from the validator context.""" + settings = { + "profiles_path": fake_profiles_path, + "profile_name": "c", + "data_path": "/tmp/random_path", + "inherit_profiles": False + } + + settings = ValidationSettings(**settings) + assert not settings.inherit_profiles, "The inheritance mode should be set to False" + + validator = Validator(settings) + # initialize the validation context + context = ValidationContext(validator, validator.validation_settings.to_dict()) + + # Load the profiles + profiles = context.profiles + logger.debug("The profiles: %r", profiles) + + # The number of profiles should be 1 + assert len(profiles) == 1, "The number of profiles should be 1" + + +def test_loaded_valid_profile_with_inheritance_from_validator_context(fake_profiles_path: str): + """Test the loaded profiles from the validator context.""" + + def __perform_test__(profile_name: str, expected_profiles: int): + settings = { + "profiles_path": fake_profiles_path, + "profile_name": profile_name, + "data_path": "/tmp/random_path", + } + + validator = Validator(settings) + # initialize the validation context + context = ValidationContext(validator, validator.validation_settings.to_dict()) + + # Check if the inheritance mode is set to True + assert context.inheritance_enabled + + profiles = context.profiles + logger.debug("The profiles: %r", profiles) + + # The number of profiles should be 1 + assert len(profiles) == expected_profiles, f"The number of profiles should be {expected_profiles}" + + # Test the inheritance mode with 1 profile + __perform_test__("a", 1) + # Test the inheritance mode with 2 profiles + __perform_test__("b", 2) + # Test the inheritance mode with 3 profiles + __perform_test__("c", 3) From 3e82cfeb9abb2288f6a592d61fac8f266d2c7f42 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 24 May 2024 13:17:27 +0200 Subject: [PATCH 386/902] test(core): :white_check_mark: add unit tests for the `ValidationSettings` class --- tests/unit/test_validation_settings.py | 89 ++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/unit/test_validation_settings.py diff --git a/tests/unit/test_validation_settings.py b/tests/unit/test_validation_settings.py new file mode 100644 index 00000000..5e20565d --- /dev/null +++ b/tests/unit/test_validation_settings.py @@ -0,0 +1,89 @@ +import pytest + +from rocrate_validator.models import Severity, ValidationSettings + + +def test_validation_settings_parse_dict(): + settings_dict = { + "data_path": "/path/to/data", + "profiles_path": "/path/to/profiles", + "requirement_severity": "RECOMMENDED", + "allow_infos": True, + "inherit_profiles": False, + } + settings = ValidationSettings.parse(settings_dict) + assert settings.data_path == "/path/to/data" + assert settings.profiles_path == "/path/to/profiles" + assert settings.requirement_severity == Severity.RECOMMENDED + assert settings.allow_infos is True + assert settings.inherit_profiles is False + + +def test_validation_settings_parse_object(): + existing_settings = ValidationSettings( + data_path="/path/to/data", + profiles_path="/path/to/profiles", + requirement_severity=Severity.RECOMMENDED, + allow_infos=True, + inherit_profiles=False + ) + settings = ValidationSettings.parse(existing_settings) + assert settings.data_path == "/path/to/data" + assert settings.profiles_path == "/path/to/profiles" + assert settings.requirement_severity == Severity.RECOMMENDED + assert settings.allow_infos is True + assert settings.inherit_profiles is False + + +def test_validation_settings_parse_invalid_type(): + with pytest.raises(ValueError): + ValidationSettings.parse("invalid_settings") + + +def test_validation_settings_to_dict(): + settings = ValidationSettings( + data_path="/path/to/data", + profiles_path="/path/to/profiles", + requirement_severity=Severity.RECOMMENDED, + allow_infos=True, + inherit_profiles=False + ) + settings_dict = settings.to_dict() + assert settings_dict["data_path"] == "/path/to/data" + assert settings_dict["profiles_path"] == "/path/to/profiles" + assert settings_dict["requirement_severity"] == Severity.RECOMMENDED + assert settings_dict["allow_infos"] is True + assert settings_dict["inherit_profiles"] is False + + +def test_validation_settings_inherit_profiles(): + settings = ValidationSettings(inherit_profiles=True) + assert settings.inherit_profiles is True + + settings = ValidationSettings(inherit_profiles=False) + assert settings.inherit_profiles is False + + +def test_validation_settings_data_path(): + settings = ValidationSettings(data_path="/path/to/data") + assert settings.data_path == "/path/to/data" + + +def test_validation_settings_profiles_path(): + settings = ValidationSettings(profiles_path="/path/to/profiles") + assert settings.profiles_path == "/path/to/profiles" + + +def test_validation_settings_requirement_severity(): + settings = ValidationSettings(requirement_severity=Severity.RECOMMENDED) + assert settings.requirement_severity == Severity.RECOMMENDED + + +def test_validation_settings_allow_infos(): + settings = ValidationSettings(allow_infos=True) + assert settings.allow_infos is True + + +def test_validation_settings_abort_on_first(): + settings = ValidationSettings(abort_on_first=True) + assert settings.abort_on_first is True From 33908670d3eb8f609ac5263273c0c921d2790867 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 24 May 2024 13:22:28 +0200 Subject: [PATCH 387/902] fix(core): :bug: return profiles dict --- rocrate_validator/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 0990b820..281223de 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -968,11 +968,11 @@ def __load_profiles__(self) -> OrderedDict[str, Profile]: publicID=self.publicID, severity=self.requirement_severity) return {profile.name: profile} - profiles = [p for p in Profile.load_profiles( + profiles = {pn: p for pn, p in Profile.load_profiles( self.profiles_path, publicID=self.publicID, severity=self.requirement_severity, - reverse_order=False) if p <= self.profile_name] + reverse_order=False).items() if pn <= self.profile_name} # Check if the target profile is in the list of profiles if self.profile_name not in profiles: raise ProfileNotFound(f"Profile '{self.profile_name}' not found in '{self.profiles_path}'") From 6e00ea6f179895fd18f41fd0a9957031beaf0240 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 10:35:05 +0200 Subject: [PATCH 388/902] feat(core): :sparkles: reorder requirements appropriately --- rocrate_validator/models.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 281223de..bf5c7a7f 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -466,7 +466,6 @@ def ok_file(p: Path) -> bool: files = sorted((p for p in profile.path.rglob('*.*') if ok_file(p)), key=lambda x: (not x.suffix == '.py', x)) - req_id = 0 requirements = [] for requirement_path in files: requirement_level = None @@ -480,9 +479,12 @@ def ok_file(p: Path) -> bool: for requirement in requirement_loader.load( profile, requirement_level, requirement_path, publicID=profile.publicID): - req_id += 1 - requirement._order_number = req_id requirements.append(requirement) + # sort the requirements by severity + requirements = sorted(requirements, key=lambda x: x.level.severity, reverse=True) + # assign order numbers to requirements + for i, requirement in enumerate(requirements): + requirement._order_number = i + 1 # log and return the requirements logger.debug("Profile %s loaded %s requirements: %s", profile.name, len(requirements), requirements) From 7d9cd7ac51630de83c8eebc7d9310591118da44a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 8 May 2024 12:36:57 +0200 Subject: [PATCH 389/902] feat(profiles): :sparkles: add file to declare for shared SPARQL prefixes --- .../profiles/ro-crate/prefixes.ttl | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 rocrate_validator/profiles/ro-crate/prefixes.ttl diff --git a/rocrate_validator/profiles/ro-crate/prefixes.ttl b/rocrate_validator/profiles/ro-crate/prefixes.ttl new file mode 100644 index 00000000..283a1350 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/prefixes.ttl @@ -0,0 +1,44 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix owl: . + +# Define the prefixes used in the SPARQL queries +ro: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 "rocrate" ; + sh:namespace "https://w3id.org/ro/crate/1.1/"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "ro" ; + sh:namespace "./"^^xsd:anyURI ; + ] . + +ro:SharedShape a sh:NodeShape ; + a sh:NodeShape ; + sh:name "Shared Shape" ; + sh:description "Shared Shape for all RO-Crate Metadata File Descriptor entities" ; + sh:targetNode ro:ro-crate-metadata.json ; + + sh:property [ + a sh:PropertyShape ; + sh:name "id" ; + # sh:severity sh:Info ; + sh:description "The identifier of the entity" ; + sh:path rdf:type ; + sh:minCount 1 ; + # sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + ] . + From 3abd53b650d68c12f3a3ae1a6acb429403a656a3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 8 May 2024 12:37:39 +0200 Subject: [PATCH 390/902] feat(profiles): :sparkles: add minimal OWL ontology --- .../profiles/ro-crate/ontology.ttl | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 rocrate_validator/profiles/ro-crate/ontology.ttl diff --git a/rocrate_validator/profiles/ro-crate/ontology.ttl b/rocrate_validator/profiles/ro-crate/ontology.ttl new file mode 100644 index 00000000..e3388ac2 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/ontology.ttl @@ -0,0 +1,174 @@ +@prefix ro: <./> . +@prefix owl: . +@prefix rdf: . +@prefix xml: . +@prefix xsd: . +@prefix rdfs: . +@prefix schema: . +@prefix rocrate: . +@prefix bioschemas: . +# @base <./.> . + + rdf:type owl:Ontology ; + owl:versionIRI . + +# # ################################################################ +# # # Object Properties +# # ################################################################ + +# # ### http://schema.org/about +# # schema:about rdf:type owl:ObjectProperty , +# # owl:FunctionalProperty ; +# # rdfs:domain rocrate:ROCrateDescriptor ; +# # rdfs:range rocrate:RootDataEntity ; +# # rdfs:label "about"@en . + + +# # ### http://schema.org/hasPart +# # schema:hasPart rdf:type owl:ObjectProperty ; +# # rdfs:domain rocrate:RootDataEntity ; +# # rdfs:range rocrate:DataEntity ; +# # rdfs:label "hasPart"@en . + + +# # ### http://schema.org/mainEntity +# # schema:mainEntity rdf:type owl:ObjectProperty , +# # owl:FunctionalProperty ; +# # rdfs:domain rocrate:RootDataEntity ; +# # rdfs:range rocrate:MainWorkflow . + + +# # ################################################################# +# # # Classes +# # ################################################################# + +### 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 rocrate: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://w3id.org/ro/crate/1.1/DataEntity +rocrate:DataEntity rdf:type owl:Class ; + owl:equivalentClass [ rdf:type owl:Class ; + owl:unionOf ( rocrate:Directory + rocrate:File + ) + ] ; + rdfs:subClassOf schema:CreativeWork ; + rdfs:label "DataEntity"@en . + + +# # ### https://w3id.org/ro/crate/1.1/Directory +# rocrate:Directory rdf:type owl:Class ; +# owl:equivalentClass [ +# rdf:type owl:Class ; +# owl:intersectionOf ( +# schema:Dataset +# [ +# rdf:type owl:Class ; +# owl:complementOf rocrate:RootDataEntity +# ] +# ) ; +# ] ; +# rdfs:label "Directory"@en . + + +# ### https://w3id.org/ro/crate/1.1/File +rocrate:File rdf:type owl:Class ; + owl:equivalentClass schema:MediaObject ; + owl:disjointWith rocrate:Directory , + rocrate:RootDataEntity ; + rdfs:label "File"@en . + + +# # ### https://w3id.org/ro/crate/1.1/MainWorkflow +# # rocrate:MainWorkflow rdf:type owl:Class ; +# # rdfs:subClassOf rocrate:Workflow . + + +# # ### https://w3id.org/ro/crate/1.1/ROCrateDescriptor +# rocrate:ROCrateDescriptor rdf:type owl:Class ; +# owl:equivalentClass [ rdf:type owl:Class ; +# owl:oneOf ( ro:ro-crate-metadata.json +# ) +# ] ; +# rdfs:label "ROCrateDescriptor"@en . +# # [ rdf:type owl:Restriction ; +# # owl:onProperty schema:about ; +# # owl:someValuesFrom owl:Thing +# # ] ; +# # rdfs:label "ROCrateDescriptor"@en . + + +# # ### https://w3id.org/ro/crate/1.1/RootDataEntity +# rocrate:RootDataEntity rdf:type owl:Class ; +# owl:equivalentClass [ rdf:type owl:Class ; +# owl:oneOf ( ro: +# ) +# ] . +# # [ rdf:type owl:Restriction ; +# # owl:onProperty schema:mainEntity ; +# # owl:someValuesFrom owl:Thing +# # ] ; +# # rdfs:subClassOf [ rdf:type owl:Restriction ; +# # owl:onProperty schema:hasPart ; +# # owl:minCardinality "1"^^xsd:nonNegativeInteger +# # ] ; +# # rdfs:label "RootDataEntity"@en . + + +# # ### https://w3id.org/ro/crate/1.1/Workflow +# # rocrate:Workflow rdf:type owl:Class ; +# # rdfs:subClassOf schema:SoftwareSourceCode , +# # bioschemas:ComputationalWorkflow , +# # rocrate:File . + + +# # ################################################################# +# # # Individuals +# # ################################################################# + +# # ### ./ +ro: rdf:type owl:NamedIndividual , + schema:Dataset , + rocrate:RootDataEntity . + + +# ### ./ro-crate-metadata.json +ro:ro-crate-metadata.json rdf:type owl:NamedIndividual , + rocrate:ROCrateDescriptor . + + +# # ################################################################# +# # # General axioms +# # ################################################################# + +[ rdf:type owl:Class ; + owl:unionOf ( schema:Dataset + schema:MediaObject + rocrate:ROCrateDescriptor + ) ; + rdfs:subClassOf schema:CreativeWork +] . + + +[ rdf:type owl:AllDisjointClasses ; + owl:members ( schema:Dataset + schema:MediaObject + rocrate:ROCrateDescriptor + ) +] . From 61931c74de429bb18384bd297da575145796b341 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 8 May 2024 12:47:03 +0200 Subject: [PATCH 391/902] feat(profiles/ro-crate): :sparkles: augment the data model with the File Data Entity instances --- .../ro-crate/must/4_data_entity_metadata.ttl | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl diff --git a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl new file mode 100644 index 00000000..8dfdf42a --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl @@ -0,0 +1,45 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix owl: . +@prefix rdfs: . +@prefix rocrate: . + + +ro:FileDataEntity a sh:NodeShape ; + sh:name "Definition of File Data Entity" ; + sh:description """A File Data Entity is a digital object that is stored in a file format""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:MediaObject . + FILTER(?this != ro:ro-crate-metadata.json) + } + """ + ] ; + + sh:property [ + sh:name "Type of File Data Entities" ; + sh:description """Data Entities representing files MUST have "File" as a value for @type. + File is an RO-Crate alias for http://schema.org/MediaObject. The term File here is liberal, and includes โ€œdownloadableโ€ resources where @id is an absolute URI. + """ ; + sh:path rdf:type ; + sh:hasValue rocrate:File ; + sh:severity sh:Violation ; + ] ; + + # 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 rocrate:DataEntity ; + ] . + From 0304f74e04bba640cc1c917bef9bb2d999541156 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 8 May 2024 12:47:34 +0200 Subject: [PATCH 392/902] feat(profiles/ro-crate): :sparkles: augment the data model with the Directory Data Entity instances --- .../ro-crate/must/4_data_entity_metadata.ttl | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl index 8dfdf42a..95eac4a5 100644 --- a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl @@ -43,3 +43,68 @@ ro:FileDataEntity a sh:NodeShape ; sh:object rocrate:DataEntity ; ] . + +ro:DirectoryDataEntity a sh:NodeShape ; + sh:name "Definition of Directory Data Entity" ; + sh:description """A Directory Data Entity is a digital object that is stored in a directory""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:Dataset . + FILTER NOT EXISTS { ?this a rocrate:RootDataEntity } + } + """ + ] ; + sh:targetClass rocrate:Directory ; + sh:property [ + sh:name "Type of Directory Data Entities" ; + sh:description """Data Entities representing directories MUST have "Directory" as a value for @type. + Directory is an RO-Crate alias for http://schema.org/Dataset. + """ ; + sh:path rdf:type ; + sh:hasValue schema_org:Dataset ; + sh:severity sh:Violation ; + ] ; + + # Decomment 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 rocrate:File ; + # sh:severity sh:Violation ; + # ] ; + + # 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 rocrate: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 rocrate:DataEntity ; + ] . + + +# 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 rocrate:Directory ; + +# sh:property [ +# sh:name "Test Directory instance" ; +# sh:description """Check if the Directory DataEntity instance has the fake property rocrate:foo""" ; +# sh:path rdf:type ; +# sh:hasValue rocrate:foo ; +# sh:severity sh:Violation ; +# ] . From e59ffb934259a7d9587c053da38e2a897cecd311 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 8 May 2024 12:57:05 +0200 Subject: [PATCH 393/902] feat(profiles/ro-crate): :sparkles: add shape to check recommended properties of DataEntity instances Check the inverse path of the Root DataEntity hasPart relationship --- .../ro-crate/should/4_data_entity.ttl | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 rocrate_validator/profiles/ro-crate/should/4_data_entity.ttl diff --git a/rocrate_validator/profiles/ro-crate/should/4_data_entity.ttl b/rocrate_validator/profiles/ro-crate/should/4_data_entity.ttl new file mode 100644 index 00000000..0d3e6830 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/should/4_data_entity.ttl @@ -0,0 +1,29 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix owl: . +@prefix rdfs: . +@prefix rocrate: . + + + +ro:DataEntityRecommendedPropertiesShape a sh:NodeShape ; + sh:name "DataEntity recommended properties" ; + sh:description """A DataEntity is a digital object that is + stored in a file format""" ; + sh:targetClass rocrate:DataEntity ; + # check inverse path of hasPart relation + sh:property [ + a sh:PropertyShape ; + sh:path [ sh:inversePath schema_org:hasPart ] ; + sh:node rocrate:RootDataEntity ; + sh:minCount 1 ; + sh:name "DataEntity - RootDataEntity reference" ; + sh:description "DataEntity instances should be linked to a RootDataEntity through the schema_org:hasPart property" ; + # sh:group ro:NameGroup ; + ] . + From 49d85ae61bc69e16fd0b5133ee3ad281dace1a55 Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 14 May 2024 12:01:49 +0200 Subject: [PATCH 394/902] remove named individuals so tests pass --- rocrate_validator/profiles/ro-crate/ontology.ttl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/ontology.ttl b/rocrate_validator/profiles/ro-crate/ontology.ttl index e3388ac2..3a4a7dd5 100644 --- a/rocrate_validator/profiles/ro-crate/ontology.ttl +++ b/rocrate_validator/profiles/ro-crate/ontology.ttl @@ -143,14 +143,14 @@ rocrate:File rdf:type owl:Class ; # # ################################################################# # # ### ./ -ro: rdf:type owl:NamedIndividual , - schema:Dataset , - rocrate:RootDataEntity . + # ro: rdf:type owl:NamedIndividual , + # schema:Dataset , + # rocrate:RootDataEntity . # ### ./ro-crate-metadata.json -ro:ro-crate-metadata.json rdf:type owl:NamedIndividual , - rocrate:ROCrateDescriptor . + # ro:ro-crate-metadata.json rdf:type owl:NamedIndividual , + # rocrate:ROCrateDescriptor . # # ################################################################# From f0fbd75a193cc4facdc2935c4f0e9cab37527e7e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 09:09:29 +0200 Subject: [PATCH 395/902] chore(profiles): :wastebasket: --- .../profiles/ro-crate/prefixes.ttl | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/prefixes.ttl b/rocrate_validator/profiles/ro-crate/prefixes.ttl index 283a1350..111eadaf 100644 --- a/rocrate_validator/profiles/ro-crate/prefixes.ttl +++ b/rocrate_validator/profiles/ro-crate/prefixes.ttl @@ -25,20 +25,3 @@ ro:sparqlPrefixes sh:prefix "ro" ; sh:namespace "./"^^xsd:anyURI ; ] . - -ro:SharedShape a sh:NodeShape ; - a sh:NodeShape ; - sh:name "Shared Shape" ; - sh:description "Shared Shape for all RO-Crate Metadata File Descriptor entities" ; - sh:targetNode ro:ro-crate-metadata.json ; - - sh:property [ - a sh:PropertyShape ; - sh:name "id" ; - # sh:severity sh:Info ; - sh:description "The identifier of the entity" ; - sh:path rdf:type ; - sh:minCount 1 ; - # sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; - ] . - From 1c328726ff1ef00cb72012c032625fbf2b669f17 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 09:11:57 +0200 Subject: [PATCH 396/902] revert(profiles): --- .../ro-crate/must/1_file-descriptor_metadata.ttl | 10 +++++----- .../ro-crate/must/2_root_data_entity_metadata.ttl | 6 ++---- .../ro-crate/should/2_root_data_entity_metadata.ttl | 6 +++--- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index b4a5f6ee..47ebd539 100644 --- a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -1,4 +1,4 @@ -@prefix : <./> . +@prefix ro: <./> . @prefix dct: . @prefix rdf: . @prefix schema_org: . @@ -7,11 +7,11 @@ @prefix xsd: . -:FileDescriptorExistence +ro:FileDescriptorExistence a sh:NodeShape ; sh:name "RO-Crate Metadata File Descriptor entity MUST exist" ; sh:description "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; - sh:targetNode :ro-crate-metadata.json ; + sh:targetNode ro:ro-crate-metadata.json ; sh:property [ a sh:PropertyShape ; sh:name "Check Metadata File Descriptor entity existence" ; @@ -21,12 +21,12 @@ sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; ] . -:MetadataFileDescriptorDefinition a sh:NodeShape ; +ro:MetadataFileDescriptorDefinition a sh:NodeShape ; sh:name "RO-Crate Metadata File Descriptor: recommended properties" ; sh:description """RO-Crate Metadata Descriptor MUST be defined according with the requirements details defined in """; - sh:targetNode :ro-crate-metadata.json ; + sh:targetNode ro:ro-crate-metadata.json ; sh:property [ a sh:PropertyShape ; sh:name "Check if the Metadata File Descriptor is a CreativeWork" ; 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 54673521..75a64004 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 @@ -1,13 +1,11 @@ -@prefix : <./> . +@prefix ro: <./> . @prefix dct: . @prefix rdf: . @prefix schema_org: . @prefix sh: . @prefix xml1: . @prefix xsd: . - - -:RootDataEntityExistenceAndType +ro:RootDataEntityExistenceAndType a sh:NodeShape ; sh:name "RO-Crate Root Data Entity MUST exist" ; sh:description "The root of the document MUST be a `Dataset` and must end with /" ; 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 9ba541e6..3711c201 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 @@ -1,4 +1,4 @@ -@prefix : <./> . +@prefix ro: <./> . @prefix dct: . @prefix rdf: . @prefix schema_org: . @@ -7,12 +7,12 @@ @prefix xsd: . -:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; +ro:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; sh:name "RO-Crate Data Entity definition: RECOMMENDED properties" ; sh:description """The Root Data Entity SHOULD have the properties `name`, `description` and `license` defined in """; - sh:targetNode : ; + sh:targetNode ro: ; sh:property [ a sh:PropertyShape ; sh:name "Name of the Root Data Entity" ; From 5df8595609315ae8a6b2ddce265e3cc613eb9cd7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 14:40:09 +0200 Subject: [PATCH 397/902] fix(profiles): :ambulance: remove overly restrictive value for the about property --- .../profiles/ro-crate/must/1_file-descriptor_metadata.ttl | 1 - 1 file changed, 1 deletion(-) diff --git a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index 47ebd539..fc98a0bd 100644 --- a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -46,7 +46,6 @@ ro:MetadataFileDescriptorDefinition a sh:NodeShape ; sh:minCount 1 ; sh:nodeKind sh:IRI ; sh:path schema_org:about ; - sh:hasValue : ; sh:class schema_org:Dataset ; sh:message "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity" ; ] ; From 81aece7805b81b6ff9dbd858ed9d2aa6f0d38aad Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 16:44:15 +0200 Subject: [PATCH 398/902] feat(profiles/ro-crate): :sparkles: add triple rule to identity the Root Data Entity --- .../must/2_root_data_entity_metadata.ttl | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) 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 75a64004..8886007f 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 @@ -5,6 +5,37 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . +@prefix rocrate: . + + +rocrate:FindRootDataEntity a sh:NodeShape ; + 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 resource ro-crate-metadata.json in the RO-Crate + (see the definition at [Finding RO-Crate Root in RDF triple stores](https://www.researchobject.org/ro-crate/1.1/appendix/relative-uris.html#finding-ro-crate-root-in-rdf-triple-stores)). + """ ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro: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 rocrate:RootDataEntity ; + ] . + + ro:RootDataEntityExistenceAndType a sh:NodeShape ; sh:name "RO-Crate Root Data Entity MUST exist" ; From 5d63f97f2ab4b558dc814ee0b76269b40a0bf4eb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 16:46:32 +0200 Subject: [PATCH 399/902] refactor(profiles/ro-crate): :recycle: use the `RootDataEntity` class to select the shape focus --- .../profiles/ro-crate/must/2_root_data_entity_metadata.ttl | 2 +- .../profiles/ro-crate/should/2_root_data_entity_metadata.ttl | 3 ++- 2 files changed, 3 insertions(+), 2 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 8886007f..72aeea2a 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 @@ -40,7 +40,7 @@ ro:RootDataEntityExistenceAndType a sh:NodeShape ; sh:name "RO-Crate Root Data Entity MUST exist" ; sh:description "The root of the document MUST be a `Dataset` and must end with /" ; - sh:targetNode : ; + sh:targetClass rocrate:RootDataEntity ; sh:property [ a sh:PropertyShape ; sh:name "Check existence of the Root Rata Entity" ; 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 3711c201..644edd8c 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 @@ -5,6 +5,7 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . +@prefix rocrate: . ro:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; @@ -12,7 +13,7 @@ ro:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; sh:description """The Root Data Entity SHOULD have the properties `name`, `description` and `license` defined in """; - sh:targetNode ro: ; + sh:targetClass rocrate:RootDataEntity ; sh:property [ a sh:PropertyShape ; sh:name "Name of the Root Data Entity" ; From 2719f4f652ef7d5972bfdb9eedaac1b145d2a117 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 16:48:14 +0200 Subject: [PATCH 400/902] fix(profiles/ro-crate): :ambulance: define the date requirement as RECOMMENDED --- .../must/2_root_data_entity_metadata.ttl | 16 ---------------- .../should/2_root_data_entity_metadata.ttl | 13 +++++++++++++ 2 files changed, 13 insertions(+), 16 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 72aeea2a..661fbf21 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 @@ -50,19 +50,3 @@ ro:RootDataEntityExistenceAndType sh:minCount 1 ; sh:message """The file descriptor MUST have a root data entity of type schema_org:Dataset and ending with /""" ; ] . - -:RootDataEntityRequiredDirectProperties a sh:NodeShape ; - sh:name "RO-Crate Data Entity definition" ; - sh:description """The Root Data Entity MUST have the properties defined in - """; - sh:targetNode : ; - sh:property [ - a sh:PropertyShape ; - sh:name "Date Published of the Root Data Entity" ; - sh:description "The datePublished of the Root Data Entity 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{2}:\\d{2}$" ; - sh:message "The datePublished of the Root Data Entity MUST be a valid ISO 8601 date" ; - ] . 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 644edd8c..af4816b2 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 @@ -47,4 +47,17 @@ ro:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; sh:path schema_org:license; sh:minCount 1 ; sh:message """The Root Data Entity SHOULD have a link to a Contextual Entity representing the schema_org:license type""" ; + ] ; + + sh:property [ + a sh:PropertyShape ; + sh:name "Date Published of the Root Data Entity" ; + sh:description """The datePublished of the Root Data Entity MUST be a valid ISO 8601 date + SHOULD be specified to at least the precision of a day. + """ ; + 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 datePublished of the Root Data Entity MUST be a valid ISO 8601 date" ; ] . From 4e561353136137402e8b2a71c48684cf87164729 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 16:49:51 +0200 Subject: [PATCH 401/902] fix(profiles/ro-crate): :bug: remove inappropriate `sh:class` property --- .../profiles/ro-crate/should/2_root_data_entity_metadata.ttl | 1 - 1 file changed, 1 deletion(-) 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 af4816b2..01c44cfc 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 @@ -43,7 +43,6 @@ ro:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; link to a Contextual Entity in the RO-Crate Metadata File with a name and description.""" ; sh:nodeKind sh:BlankNodeOrIRI ; - sh:class schema_org:license ; sh:path schema_org:license; sh:minCount 1 ; sh:message """The Root Data Entity SHOULD have a link to a Contextual Entity representing the schema_org:license type""" ; From e924ddb88ee79e100e095e85064e315913c42869 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 16:51:56 +0200 Subject: [PATCH 402/902] style(ontology): :art: fix indentation --- rocrate_validator/profiles/ro-crate/ontology.ttl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/ontology.ttl b/rocrate_validator/profiles/ro-crate/ontology.ttl index 3a4a7dd5..2e2190e7 100644 --- a/rocrate_validator/profiles/ro-crate/ontology.ttl +++ b/rocrate_validator/profiles/ro-crate/ontology.ttl @@ -143,14 +143,14 @@ rocrate:File rdf:type owl:Class ; # # ################################################################# # # ### ./ - # ro: rdf:type owl:NamedIndividual , - # schema:Dataset , - # rocrate:RootDataEntity . +# ro: rdf:type owl:NamedIndividual , +# schema:Dataset , +# rocrate:RootDataEntity . # ### ./ro-crate-metadata.json - # ro:ro-crate-metadata.json rdf:type owl:NamedIndividual , - # rocrate:ROCrateDescriptor . +# ro:ro-crate-metadata.json rdf:type owl:NamedIndividual , +# rocrate:ROCrateDescriptor . # # ################################################################# From 08cbff231a8d512b759ad30ca29565dc590f0cbe Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 16:52:54 +0200 Subject: [PATCH 403/902] feat(ontology): :sparkles: declare the RootDataEntity class --- rocrate_validator/profiles/ro-crate/ontology.ttl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rocrate_validator/profiles/ro-crate/ontology.ttl b/rocrate_validator/profiles/ro-crate/ontology.ttl index 2e2190e7..daeb95bb 100644 --- a/rocrate_validator/profiles/ro-crate/ontology.ttl +++ b/rocrate_validator/profiles/ro-crate/ontology.ttl @@ -42,6 +42,11 @@ # # # Classes # # ################################################################# +# Declare the RootDataEntity class +rocrate: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 . From a7704cdea92846247b1e29d3e2c94abd5ec8312d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 16:53:25 +0200 Subject: [PATCH 404/902] feat(ontology): :sparkles: declare the `Directory` class --- .../profiles/ro-crate/ontology.ttl | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/ontology.ttl b/rocrate_validator/profiles/ro-crate/ontology.ttl index daeb95bb..46d71a24 100644 --- a/rocrate_validator/profiles/ro-crate/ontology.ttl +++ b/rocrate_validator/profiles/ro-crate/ontology.ttl @@ -78,18 +78,18 @@ rocrate:DataEntity rdf:type owl:Class ; # # ### https://w3id.org/ro/crate/1.1/Directory -# rocrate:Directory rdf:type owl:Class ; -# owl:equivalentClass [ -# rdf:type owl:Class ; -# owl:intersectionOf ( -# schema:Dataset -# [ -# rdf:type owl:Class ; -# owl:complementOf rocrate:RootDataEntity -# ] -# ) ; -# ] ; -# rdfs:label "Directory"@en . +rocrate:Directory rdf:type owl:Class ; + owl:equivalentClass [ + rdf:type owl:Class ; + owl:intersectionOf ( + schema:Dataset + [ + rdf:type owl:Class ; + owl:complementOf rocrate:RootDataEntity + ] + ) ; + ] ; + rdfs:label "Directory"@en . # ### https://w3id.org/ro/crate/1.1/File From 7f9255e56a00ee64c55ad1ae54f982e3765e5610 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 16:55:06 +0200 Subject: [PATCH 405/902] feat(profiles/ro-crate): :sparkles: minimal shape to validate license --- .../ro-crate/may/5_license_entity.ttl | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 rocrate_validator/profiles/ro-crate/may/5_license_entity.ttl diff --git a/rocrate_validator/profiles/ro-crate/may/5_license_entity.ttl b/rocrate_validator/profiles/ro-crate/may/5_license_entity.ttl new file mode 100644 index 00000000..bf6687f7 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/may/5_license_entity.ttl @@ -0,0 +1,33 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + +ro: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" ; + ] . + From 7a129a52264e8decb09fd58efd44f0fbac4900a2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 16:56:31 +0200 Subject: [PATCH 406/902] feat(profiles/ro-crate): :sparkles: add triple rule to mark license as Contextual Entity --- .../ro-crate/must/5_contextual_entity.ttl | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 rocrate_validator/profiles/ro-crate/must/5_contextual_entity.ttl diff --git a/rocrate_validator/profiles/ro-crate/must/5_contextual_entity.ttl b/rocrate_validator/profiles/ro-crate/must/5_contextual_entity.ttl new file mode 100644 index 00000000..1bbd95bc --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/must/5_contextual_entity.ttl @@ -0,0 +1,33 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix owl: . +@prefix rdfs: . +@prefix rocrate: . + + +rocrate:FindLicenseEntity a sh:NodeShape ; + sh:name "Identify License Entity" ; + sh:description """Mark a license entity any Data Entity referenced by the `schema:licence` property.""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?subject schema:license ?this . + } + """ + ] ; + + # 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 rocrate:ContextualEntity ; + ] . From 713922e956ed90b978dd77091ae67089681ec145 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 16:58:41 +0200 Subject: [PATCH 407/902] test(profiles/ro-crate): :white_check_mark: re-enable previously marked failed tests --- tests/integration/profiles/ro-crate/test_valid_ro-crate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration/profiles/ro-crate/test_valid_ro-crate.py b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py index 0a07f0d6..c6ea3657 100644 --- a/tests/integration/profiles/ro-crate/test_valid_ro-crate.py +++ b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py @@ -10,7 +10,6 @@ logger = logging.getLogger(__name__) -@pytest.mark.xfail(reason="Known problem with data validation in RO-Crate profile") def test_valid_roc_required(): """Test a valid RO-Crate.""" do_entity_test( @@ -20,7 +19,6 @@ def test_valid_roc_required(): ) -@pytest.mark.xfail(reason="Known problem with data validation in RO-Crate profile") def test_valid_roc_recommended(): """Test a valid RO-Crate.""" do_entity_test( From 42e4fa2cb3c2c13cfc876522055700b97ff4bc3f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 16:59:52 +0200 Subject: [PATCH 408/902] test(profiles/ro-crate): :white_check_mark: fix test --- .../profiles/ro-crate/must/test_root_data_entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index 7f9e9c47..cb8badcc 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -28,9 +28,9 @@ def test_invalid_root_date(): """Test a RO-Crate with an invalid root data entity date.""" do_entity_test( paths.invalid_root_date, - models.Severity.REQUIRED, + models.Severity.RECOMMENDED, False, - ["RO-Crate Data Entity definition"], + ["RO-Crate Data Entity definition: RECOMMENDED properties"], ["The datePublished of the Root Data Entity MUST be a valid ISO 8601 date"] ) From 29d572dda488fb229366b33a46124ba7b1dd35e2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 17:02:10 +0200 Subject: [PATCH 409/902] test(profiles): :test_tube: skip tests that are no longer valid --- .../profiles/ro-crate/must/test_file_descriptor_entity.py | 3 +++ .../profiles/ro-crate/must/test_root_data_entity.py | 3 +++ tests/test_models.py | 1 + 3 files changed, 7 insertions(+) diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py index 778d702b..256a55b9 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py @@ -1,5 +1,7 @@ import logging +import pytest + from rocrate_validator import models from tests.ro_crates import InvalidFileDescriptorEntity from tests.shared import do_entity_test @@ -45,6 +47,7 @@ def test_missing_entity_about(): ) +@pytest.mark.skip(reason="This test is not working as expected") def test_invalid_entity_about(): """Test a RO-Crate with an invalid about property in the file descriptor.""" do_entity_test( diff --git a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index cb8badcc..a700dd20 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -1,5 +1,7 @@ import logging +import pytest + from rocrate_validator import models from tests.ro_crates import InvalidRootDataEntity from tests.shared import do_entity_test @@ -12,6 +14,7 @@ paths = InvalidRootDataEntity() +@pytest.mark.skip(reason="This condition cannot be tested as expected") def test_missing_root_data_entity(): """Test a RO-Crate without a root data entity.""" do_entity_test( diff --git a/tests/test_models.py b/tests/test_models.py index 55007807..663846bb 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -64,6 +64,7 @@ def validation_settings(): ) +@pytest.mark.skip(reason="Temporarily disabled: we need an RO-Crate with multiple failed requirements to test this.") def test_sortability_requirements(validation_settings: ValidationSettings): validation_settings.data_path = InvalidRootDataEntity().invalid_root_type result: models.ValidationResult = services.validate(validation_settings) From b643e7ff07beee72c27ce6b86b26f6b4638f6ee3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 28 May 2024 17:10:18 +0200 Subject: [PATCH 410/902] refactor: :coffin: remove obsolete shape definition --- .../ro-crate/may/2_license_entity.ttl | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 rocrate_validator/profiles/ro-crate/may/2_license_entity.ttl diff --git a/rocrate_validator/profiles/ro-crate/may/2_license_entity.ttl b/rocrate_validator/profiles/ro-crate/may/2_license_entity.ttl deleted file mode 100644 index a6ed1ca1..00000000 --- a/rocrate_validator/profiles/ro-crate/may/2_license_entity.ttl +++ /dev/null @@ -1,33 +0,0 @@ -@prefix : <./> . -@prefix dct: . -@prefix rdf: . -@prefix schema_org: . -@prefix sh: . -@prefix xml1: . -@prefix xsd: . - -: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" ; - ] . - From 3a79963d21151e5be92b801f337bf17237fbadd2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 11:42:22 +0200 Subject: [PATCH 411/902] feat(core): :sparkles: allow marking requirements as hidden --- rocrate_validator/cli/commands/profiles.py | 9 ++++++++- rocrate_validator/models.py | 5 +++++ rocrate_validator/requirements/python/__init__.py | 4 ++++ rocrate_validator/requirements/shacl/requirements.py | 9 +++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index effb47ac..d70f3e63 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -128,6 +128,10 @@ def __compacted_describe_profile__(console, profile): table_rows = [] levels_list = set() for requirement in profile.requirements: + # skip hidden requirements + if requirement.hidden: + continue + # add the requirement to the list color = get_severity_color(requirement.severity) level_info = f"[{color}]{requirement.severity.name}[/{color}]" levels_list.add(level_info) @@ -168,7 +172,10 @@ def __verbose_describe_profile__(console, profile): table_rows = [] levels_list = set() for requirement in profile.requirements: - + # skip hidden requirements + if 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}]" diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index bf5c7a7f..e786f6af 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -331,6 +331,11 @@ def description(self) -> str: ) if self.__class__.__doc__ else f"Profile Requirement {self.name}" return self._description + @property + @abstractmethod + def hidden(self) -> bool: + pass + @property def path(self) -> Optional[Path]: return self._path diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index e2d35221..979c7d74 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -73,6 +73,10 @@ def __init_checks__(self): return checks + @property + def hidden(self) -> bool: + return getattr(self.requirement_check_class, "hidden", False) + def check(name: Optional[str] = None): """ diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index beed3fa0..de8fbe3e 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -55,6 +55,15 @@ def level(self) -> RequirementLevel: return self.shape.level return level + @property + def hidden(self) -> bool: + from rdflib import RDF, Namespace + SHACL = Namespace("http://www.w3.org/ns/shacl#") + if self.shape.node is not None: + if (self.shape.node, RDF.type, SHACL.hidden) in self.shape.graph: + return True + return False + class SHACLRequirementLoader(RequirementLoader): From d28fd2bc331e00bfa7d23640a01b72bdd41b822a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 12:06:02 +0200 Subject: [PATCH 412/902] chore(test-data): :bug: fix test data --- .../invalid_root_date/ro-crate-metadata.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_date/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_date/ro-crate-metadata.json index 649c3317..db778f91 100644 --- a/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_date/ro-crate-metadata.json +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_date/ro-crate-metadata.json @@ -21,7 +21,8 @@ { "@id": "./", "@type": "Dataset", - "datePublished": "2024-01-22", + "datePublished": "2024 Jan 01", + "description": "This RO Crate contains the workflow MyWorkflow", "hasPart": [ { "@id": "my-workflow.ga" From 53c10bd4171e21bb31178d2a69d038dfecaebae8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 12:09:09 +0200 Subject: [PATCH 413/902] feat(shacl): :sparkles: allow the use of node shape name and description as fallback for children shape properties --- rocrate_validator/requirements/shacl/checks.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index bbd3e130..7009919a 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -24,10 +24,12 @@ def __init__(self, self._shape = shape # init the check super().__init__(requirement, - shape.name - if shape and shape.name else None, - shape.description - if shape and shape.description else None) + shape.name if shape and shape.name + else shape.parent.name if shape.parent + else None, + shape.description if shape and shape.description + else shape.parent.description if shape.parent + else None) # store the instance SHACLCheck.__add_instance__(shape, self) From ccd5522be12f4fd46765685870b826c10ccd0167 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 14:19:11 +0200 Subject: [PATCH 414/902] feat(core): :sparkles: allow to specify PyRequirement name and desc via decorator --- .../requirements/python/__init__.py | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index 979c7d74..1f72c831 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -1,5 +1,6 @@ import inspect import logging +import re from pathlib import Path from typing import Callable, Optional, Type @@ -78,6 +79,21 @@ def hidden(self) -> bool: return getattr(self.requirement_check_class, "hidden", False) +def requirement(name: str, description: Optional[str] = None): + """ + A decorator to mark functions as "requirements" (by setting an attribute + `requirement=True`) and annotating them with a human-legible name. + """ + def decorator(cls): + if name: + cls.__rq_name__ = name + if description: + cls.__rq_description__ = description + return cls + + return decorator + + def check(name: Optional[str] = None): """ A decorator to mark functions as "checks" (by setting an attribute @@ -88,10 +104,10 @@ def decorator(func): sig = inspect.signature(func) if len(sig.parameters) != 2: raise RuntimeError(f"Invalid check {check_name}. Checks are expected to " - "accept two arguments but this only takes {len(sig.parameters)}") + f"accept two arguments but this only takes {len(sig.parameters)}") if sig.return_annotation not in (bool, inspect.Signature.empty): raise RuntimeError(f"Invalid check {check_name}. Checks are expected to " - "return bool but this only returns {sig.return_annotation}") + f"return bool but this only returns {sig.return_annotation}") func.check = True func.name = check_name return func @@ -113,13 +129,26 @@ def load(self, profile: Profile, # instantiate a requirement for each class for requirement_name, check_class in classes.items(): + # set default requirement name and description + rq = {} + rq["name"] = " ".join(re.findall(r'[A-Z](?:[a-z]+|[A-Z]*(?=[A-Z]|$))', + requirement_name.strip())) if requirement_name else "" + rq["description"] = check_class.__doc__.strip() if check_class.__doc__ else "" + # handle default overrides via decorators + for pn in ("name", "description"): + try: + pv = getattr(check_class, f"__rq_{pn}__", None) + if pv and isinstance(pv, str): + rq[pn] = pv + except AttributeError: + pass logger.debug("Processing requirement: %r", requirement_name) r = PyRequirement( requirement_level, profile, requirement_check_class=check_class, - name=requirement_name.strip() if requirement_name else "", - description=check_class.__doc__.strip() if check_class.__doc__ else "", + name=rq["name"], + description=rq["description"], path=file_path) logger.debug("Created requirement: %r", r) requirements.append(r) From e36816367f804113897a64bb23f9f0e723b23a93 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 14:38:09 +0200 Subject: [PATCH 415/902] refactor(profiles/ro-crate): :truck: rename pyrequirements --- .../ro-crate/must/0_file_descriptor_format.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py index 0dcafd2d..b2e1a5b1 100644 --- a/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py @@ -3,15 +3,18 @@ from typing import Optional from rocrate_validator.models import ValidationContext -from rocrate_validator.requirements.python import PyFunctionCheck, check +from rocrate_validator.requirements.python import (PyFunctionCheck, check, + requirement) # set up logging logger = logging.getLogger(__name__) +@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 Description Existence") + + @check(name="File Descriptor Existence") def test_existence(self, context: ValidationContext) -> bool: """ Check if the file descriptor is present in the RO-Crate @@ -22,7 +25,7 @@ def test_existence(self, context: ValidationContext) -> bool: return False return True - @check(name="File size check") + @check(name="File Descriptor size check") def test_size(self, context: ValidationContext) -> bool: """ Check if the file descriptor is not empty @@ -37,13 +40,14 @@ def test_size(self, context: ValidationContext) -> bool: return True +@requirement(name="File Descriptor JSON Format") class FileDescriptorJsonFormat(PyFunctionCheck): """ The file descriptor MUST be a valid JSON file """ - @check(name="Check JSON Format of the file descriptor") + @check(name="File Descriptor JSON Format") def check(self, context: ValidationContext) -> bool: - # check if the file descriptor is in the correct format + """ Check if the file descriptor is in the correct format""" try: logger.debug("Checking validity of JSON file at %s", context.file_descriptor_path) with open(context.file_descriptor_path, "r") as file: @@ -57,6 +61,7 @@ def check(self, context: ValidationContext) -> bool: return False +@requirement(name="File Descriptor JSON-LD Format") class FileDescriptorJsonLdFormat(PyFunctionCheck): """ The file descriptor MUST be a valid JSON-LD file @@ -81,8 +86,9 @@ def get_json_dict(self, context: ValidationContext) -> dict: return {} return self._json_dict_cache['json'] - @check(name="Check if the @context property is present in the file descriptor") + @check(name="File Descriptor @context property validation") def check_context(self, validation_context: ValidationContext) -> bool: + """ Check if the file descriptor contains the @context property """ json_dict = self.get_json_dict(validation_context) if "@context" not in json_dict: validation_context.result.add_error( @@ -91,8 +97,9 @@ def check_context(self, validation_context: ValidationContext) -> bool: return False return True - @check(name="Check if descriptor entities have the @id property") + @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 """ json_dict = self.get_json_dict(context) for entity in json_dict["@graph"]: if "@id" not in entity: @@ -103,8 +110,9 @@ def check_identifiers(self, context: ValidationContext) -> bool: return False return True - @check(name="Check if descriptor entities have the @type property") + @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 """ json_dict = self.get_json_dict(context) for entity in json_dict["@graph"]: if "@type" not in entity: From d34eb6dac58f344af6a56d06d0549da1ce2563e6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 15:06:05 +0200 Subject: [PATCH 416/902] refactor(profiles/ro-crate): :art: update names of some requirements --- .../profiles/ro-crate/must/0_file_descriptor_format.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py index b2e1a5b1..2fba5acb 100644 --- a/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py @@ -40,12 +40,12 @@ def test_size(self, context: ValidationContext) -> bool: return True -@requirement(name="File Descriptor JSON Format") +@requirement(name="File Descriptor JSON format") class FileDescriptorJsonFormat(PyFunctionCheck): """ The file descriptor MUST be a valid JSON file """ - @check(name="File Descriptor JSON Format") + @check(name="File Descriptor JSON format") def check(self, context: ValidationContext) -> bool: """ Check if the file descriptor is in the correct format""" try: @@ -61,7 +61,7 @@ def check(self, context: ValidationContext) -> bool: return False -@requirement(name="File Descriptor JSON-LD Format") +@requirement(name="File Descriptor JSON-LD format") class FileDescriptorJsonLdFormat(PyFunctionCheck): """ The file descriptor MUST be a valid JSON-LD file From f035bf6a3e2a394c76c034ad0a5ebb5db409f781 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 15:06:47 +0200 Subject: [PATCH 417/902] test(test-data): :white_check_mark: update tests of json format --- .../ro-crate/must/test_file_descriptor_format.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py index b612dd69..ecaf520c 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py @@ -18,7 +18,7 @@ def test_missing_file_descriptor(): rocrate_path, models.Severity.REQUIRED, False, - ["FileDescriptorExistence"], + ["File Descriptor existence"], [] ) @@ -29,7 +29,7 @@ def test_not_valid_json_format(): paths.invalid_json_format, models.Severity.REQUIRED, False, - ["FileDescriptorJsonFormat"], + ["File Descriptor JSON format"], [] ) @@ -40,7 +40,7 @@ def test_not_valid_jsonld_format_missing_context(): f"{paths.invalid_jsonld_format}/missing_context", models.Severity.REQUIRED, False, - ["FileDescriptorJsonLdFormat"], + ["File Descriptor JSON-LD format"], [] ) @@ -54,7 +54,7 @@ def test_not_valid_jsonld_format_missing_ids(): f"{paths.invalid_jsonld_format}/missing_id", models.Severity.REQUIRED, False, - ["FileDescriptorJsonLdFormat"], + ["File Descriptor JSON-LD format"], ["file descriptor does not contain the @id attribute"] ) @@ -68,6 +68,6 @@ def test_not_valid_jsonld_format_missing_types(): f"{paths.invalid_jsonld_format}/missing_type", models.Severity.REQUIRED, False, - ["FileDescriptorJsonLdFormat"], + ["File Descriptor JSON-LD format"], ["file descriptor does not contain the @type attribute"] ) From 745b6c55ecb1d4bbdf2baed5cbf16d46089cd5a3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 17:00:40 +0200 Subject: [PATCH 418/902] fix: :pencil2: fix typo --- rocrate_validator/requirements/shacl/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 103402f1..03770720 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -243,7 +243,7 @@ def load_shapes(self, shapes_path: Union[str, Path], publicID: Optional[str] = N for node_shape in shapes_list.node_shapes: # flag to check if the nested properties are in a group grouped = False - # list of properties ungroupped + # list of properties ungrouped ungrouped_properties = [] # get the shape graph node_graph = shapes_list.get_shape_graph(node_shape) From bef447bb27f904ea3e02d88970c0bf72186fce9d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 17:14:27 +0200 Subject: [PATCH 419/902] refactor(profiles/ro-crate): :truck: rename check of descriptor existence --- .../profiles/ro-crate/must/1_file-descriptor_metadata.ttl | 5 +++-- .../profiles/ro-crate/must/test_file_descriptor_entity.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index fc98a0bd..3b85e477 100644 --- a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -7,9 +7,10 @@ @prefix xsd: . -ro:FileDescriptorExistence + +ro:ROCrateMetadataFileDescriptorExistence a sh:NodeShape ; - sh:name "RO-Crate Metadata File Descriptor entity MUST exist" ; + sh:name "RO-Crate Metadata File Descriptor entity existence" ; sh:description "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; sh:targetNode ro:ro-crate-metadata.json ; sh:property [ diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py index 256a55b9..1e6ed29e 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py @@ -19,7 +19,7 @@ def test_missing_entity(): paths.missing_entity, models.Severity.REQUIRED, False, - ["RO-Crate Metadata File Descriptor entity MUST exist"], + ["RO-Crate Metadata File Descriptor entity existence"], ["The root of the document MUST have an entity with @id `ro-crate-metadata.json`"] ) From ef5b8bcd9f7ec22e511d5a7a1f10e9be28599bf3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 17:20:08 +0200 Subject: [PATCH 420/902] refactor(profiles/ro-crate): :truck: update descriptor existence textual definition --- .../profiles/ro-crate/must/1_file-descriptor_metadata.ttl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index 3b85e477..4303d3f7 100644 --- a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -11,12 +11,13 @@ ro:ROCrateMetadataFileDescriptorExistence a sh:NodeShape ; sh:name "RO-Crate Metadata File Descriptor entity existence" ; - sh:description "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + sh:description "The RO-Crate JSON-LD MUST contain a Metadata File Descriptor entity named `ro-crate-metadata.json` and typed as `schema:CreativeWork`" ; sh:targetNode ro:ro-crate-metadata.json ; sh:property [ a sh:PropertyShape ; - sh:name "Check Metadata File Descriptor entity existence" ; - sh:description "Check if the RO-Crate Metadata File Descriptor entity exists" ; + sh:name "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 `ro-crate-metadata.json` and type `schema:CreativeWork`""" ; sh:path rdf:type ; sh:minCount 1 ; sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; From c1dd3bd6767720e6e134e077a07fb29c587e0a9a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 17:38:33 +0200 Subject: [PATCH 421/902] refactor(profiles/ro-crate): :recycle: update labels of file descriptor metadata shapes --- .../must/1_file-descriptor_metadata.ttl | 19 +++++++++---------- .../must/test_file_descriptor_entity.py | 12 ++++++------ 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index 4303d3f7..73f898b8 100644 --- a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -23,16 +23,16 @@ ro:ROCrateMetadataFileDescriptorExistence sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; ] . -ro:MetadataFileDescriptorDefinition a sh:NodeShape ; - sh:name "RO-Crate Metadata File Descriptor: recommended properties" ; +ro: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.1/root-data-entity.html#ro-crate-metadata-file-descriptor)"""; sh:targetNode ro:ro-crate-metadata.json ; sh:property [ a sh:PropertyShape ; - sh:name "Check if the Metadata File Descriptor is a CreativeWork" ; - sh:description "The RO-Crate metadata file MUST be a CreativeWork, as per schema.org" ; + 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 ; @@ -41,9 +41,8 @@ ro:MetadataFileDescriptorDefinition a sh:NodeShape ; ] ; sh:property [ a sh:PropertyShape ; - sh:name "Check if the `Metadata File Descriptor` has the `about` property" ; - sh:description """The URL of the RO-Crate metadata file itself, which is the root of the RO-Crate. - This is used to identify the RO-Crate, and is the only property that is required to be present in the metadata file.""" ; + 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:nodeKind sh:IRI ; @@ -53,8 +52,8 @@ ro:MetadataFileDescriptorDefinition a sh:NodeShape ; ] ; sh:property [ a sh:PropertyShape ; - sh:name "conformsTo property" ; - sh:description "The specification(s) that the RO-Crate JSON-LD conforms to" ; + sh:name "Metadata File Descriptor entity: `conformsTo` property" ; + sh:description """Check if the RO-Crate Metadata File Descriptor has a `conformsTo` property which points to the RO-Crate specification version""" ; sh:minCount 1 ; sh:nodeKind sh:IRI ; sh:path dct:conformsTo ; diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py index 1e6ed29e..ee8dc9a0 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py @@ -30,7 +30,7 @@ def test_invalid_entity_type(): paths.invalid_entity_type, models.Severity.REQUIRED, False, - ["RO-Crate Metadata File Descriptor: recommended properties"], + ["RO-Crate Metadata File Descriptor required properties"], ["The RO-Crate metadata file MUST be a CreativeWork, as per schema.org"] ) @@ -41,7 +41,7 @@ def test_missing_entity_about(): paths.missing_entity_about, models.Severity.REQUIRED, False, - ["RO-Crate Metadata File Descriptor: recommended properties"], + ["RO-Crate Metadata File Descriptor required properties"], ["The RO-Crate metadata file MUST be a CreativeWork, as per schema.org", "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity"] ) @@ -54,7 +54,7 @@ def test_invalid_entity_about(): paths.invalid_entity_about, models.Severity.REQUIRED, False, - ["RO-Crate Metadata File Descriptor: recommended properties"], + ["RO-Crate Metadata File Descriptor required properties"], ["The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity"] ) @@ -65,7 +65,7 @@ def test_invalid_entity_about_type(): paths.invalid_entity_about_type, models.Severity.REQUIRED, False, - ["RO-Crate Metadata File Descriptor: recommended properties"], + ["RO-Crate Metadata File Descriptor required properties"], ["The RO-Crate metadata file MUST be a CreativeWork, as per schema.org", "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity"] ) @@ -77,7 +77,7 @@ def test_missing_conforms_to(): paths.missing_conforms_to, models.Severity.REQUIRED, False, - ["RO-Crate Metadata File Descriptor: recommended properties"], + ["RO-Crate Metadata File Descriptor required properties"], ["The RO-Crate metadata file descriptor MUST have a `conformsTo` " "property with the RO-Crate specification version"] ) @@ -89,7 +89,7 @@ def test_invalid_conforms_to(): paths.invalid_conforms_to, models.Severity.REQUIRED, False, - ["RO-Crate Metadata File Descriptor: recommended properties"], + ["RO-Crate Metadata File Descriptor required properties"], ["The RO-Crate metadata file descriptor MUST have a `conformsTo` " "property with the RO-Crate specification version"] ) From ed02893a174e0395afd43bed08a96d1d7c32e88d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 17:44:13 +0200 Subject: [PATCH 422/902] feat(profiles/ro-crate): :sparkles: make the finder of RootDataEntity hidden --- .../profiles/ro-crate/must/2_root_data_entity_metadata.ttl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 661fbf21..85f3a108 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 @@ -8,7 +8,7 @@ @prefix rocrate: . -rocrate:FindRootDataEntity a sh:NodeShape ; +rocrate:FindRootDataEntity a sh:NodeShape, sh:hidden; 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 resource ro-crate-metadata.json in the RO-Crate From 36c62de14d6a5fc5151f64ff6a6fd73528b25604 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 19:26:31 +0200 Subject: [PATCH 423/902] test(test-conf): :white_check_mark: update test configuration --- .../profiles/ro-crate/must/test_file_descriptor_entity.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py index ee8dc9a0..6b7def1f 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py @@ -66,8 +66,7 @@ def test_invalid_entity_about_type(): models.Severity.REQUIRED, False, ["RO-Crate Metadata File Descriptor required properties"], - ["The RO-Crate metadata file MUST be a CreativeWork, as per schema.org", - "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity"] + ["The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity"] ) From 32ad7faca05d5a3713d438940c994e2003e27cf1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 19:28:31 +0200 Subject: [PATCH 424/902] feat(profiles/ro-crate): :sparkles: redefine shape to check Root Data Entity type --- .../must/2_root_data_entity_metadata.ttl | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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 85f3a108..b838d595 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 @@ -8,6 +8,33 @@ @prefix rocrate: . +ro: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 [ + a sh:SPARQLTarget ; + sh:prefixes ro:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?metadatafile schema:about ?this . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + } + """ + ] ; + 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`)""" ; + ] . + + + rocrate:FindRootDataEntity a sh:NodeShape, sh:hidden; 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. From 372e5f7f886f89c8b7aca575dfa7bed99935146b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 19:29:07 +0200 Subject: [PATCH 425/902] feat(profiles/ro-crate): :sparkles: add shape to check Root Data Entity value restriction --- .../must/2_root_data_entity_metadata.ttl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 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 b838d595..8e01c3a7 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 @@ -63,17 +63,17 @@ rocrate:FindRootDataEntity a sh:NodeShape, sh:hidden; ] . -ro:RootDataEntityExistenceAndType +ro:RootDataEntityValueRestriction a sh:NodeShape ; - sh:name "RO-Crate Root Data Entity MUST exist" ; - sh:description "The root of the document MUST be a `Dataset` and must end with /" ; - sh:targetClass rocrate:RootDataEntity ; + sh:name "RO-Crate Root Data Entity value restriction" ; + sh:description "The Root Data Entity MUST end with `/`" ; + sh:targetNode rocrate:RootDataEntity ; sh:property [ a sh:PropertyShape ; - sh:name "Check existence of the Root Rata Entity" ; - sh:description "Check if the Root Data Entity exists in the file descriptor" ; - sh:path rdf:type ; - sh:hasValue schema_org:Dataset ; + sh:name "Root Data Entity URI value" ; + sh:description "Check if the Root Data Entity URI ends with `/`" ; + sh:path [ sh:inversePath rdf:type ] ; sh:minCount 1 ; - sh:message """The file descriptor MUST have a root data entity of type schema_org:Dataset and ending with /""" ; + sh:message """The Root Data Entity URI MUST end with `/`""" ; + sh:pattern "/$" ; ] . From 60a9de0db07fb32da6838d59713e72e47a0ee25a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 19:29:38 +0200 Subject: [PATCH 426/902] test(profiles/ro-crate): :white_check_mark: more tests for the Root Data Entity --- .../ro-crate/must/test_root_data_entity.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index a700dd20..a76a597b 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -14,16 +14,25 @@ paths = InvalidRootDataEntity() -@pytest.mark.skip(reason="This condition cannot be tested as expected") def test_missing_root_data_entity(): """Test a RO-Crate without a root data entity.""" do_entity_test( paths.invalid_root_type, models.Severity.REQUIRED, False, - ["RO-Crate Root Data Entity MUST exist", - "RO-Crate Metadata File Descriptor: recommended properties"], - ["The file descriptor MUST have a root data entity of type schema_org:Dataset and ending with /"] + ["RO-Crate Root Data Entity type"], + ["The Root Data Entity MUST be a `Dataset` (as per `schema.org`)"] + ) + + +def test_invalid_root_data_entity_value(): + """Test a RO-Crate with an invalid root data entity value.""" + do_entity_test( + paths.invalid_root_value, + models.Severity.REQUIRED, + False, + ["RO-Crate Root Data Entity value restriction"], + ["The Root Data Entity URI MUST end with `/`"] ) From 9bb6921f81385b1ee62154947de190d15022a6f0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 19:38:45 +0200 Subject: [PATCH 427/902] refactor(profiles/ro-crate): :recycle: minor changes to the name of some requirements --- .../ro-crate/must/1_file-descriptor_metadata.ttl | 4 ++-- .../ro-crate/should/2_root_data_entity_metadata.ttl | 2 +- .../ro-crate/must/test_file_descriptor_entity.py | 12 ++++++------ .../profiles/ro-crate/must/test_root_data_entity.py | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index 73f898b8..27121745 100644 --- a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -15,7 +15,7 @@ ro:ROCrateMetadataFileDescriptorExistence sh:targetNode ro:ro-crate-metadata.json ; sh:property [ a sh:PropertyShape ; - sh:name "File Descriptor entity existence" ; + 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 `ro-crate-metadata.json` and type `schema:CreativeWork`""" ; sh:path rdf:type ; @@ -24,7 +24,7 @@ ro:ROCrateMetadataFileDescriptorExistence ] . ro:ROCrateMetadataFileDescriptorRecommendedProperties a sh:NodeShape ; - sh:name "RO-Crate Metadata File Descriptor required properties" ; + 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.1/root-data-entity.html#ro-crate-metadata-file-descriptor)"""; 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 01c44cfc..0edc43bc 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 @@ -9,7 +9,7 @@ ro:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; - sh:name "RO-Crate Data Entity definition: RECOMMENDED properties" ; + sh:name "RO-Crate Root Data Entity definition RECOMMENDED properties" ; sh:description """The Root Data Entity SHOULD have the properties `name`, `description` and `license` defined in """; diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py index 6b7def1f..ff010aea 100644 --- a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py @@ -30,7 +30,7 @@ def test_invalid_entity_type(): paths.invalid_entity_type, models.Severity.REQUIRED, False, - ["RO-Crate Metadata File Descriptor required properties"], + ["RO-Crate Metadata File Descriptor REQUIRED properties"], ["The RO-Crate metadata file MUST be a CreativeWork, as per schema.org"] ) @@ -41,7 +41,7 @@ def test_missing_entity_about(): paths.missing_entity_about, models.Severity.REQUIRED, False, - ["RO-Crate Metadata File Descriptor required properties"], + ["RO-Crate Metadata File Descriptor REQUIRED properties"], ["The RO-Crate metadata file MUST be a CreativeWork, as per schema.org", "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity"] ) @@ -54,7 +54,7 @@ def test_invalid_entity_about(): paths.invalid_entity_about, models.Severity.REQUIRED, False, - ["RO-Crate Metadata File Descriptor required properties"], + ["RO-Crate Metadata File Descriptor REQUIRED properties"], ["The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity"] ) @@ -65,7 +65,7 @@ def test_invalid_entity_about_type(): paths.invalid_entity_about_type, models.Severity.REQUIRED, False, - ["RO-Crate Metadata File Descriptor required properties"], + ["RO-Crate Metadata File Descriptor REQUIRED properties"], ["The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity"] ) @@ -76,7 +76,7 @@ def test_missing_conforms_to(): paths.missing_conforms_to, models.Severity.REQUIRED, False, - ["RO-Crate Metadata File Descriptor required properties"], + ["RO-Crate Metadata File Descriptor REQUIRED properties"], ["The RO-Crate metadata file descriptor MUST have a `conformsTo` " "property with the RO-Crate specification version"] ) @@ -88,7 +88,7 @@ def test_invalid_conforms_to(): paths.invalid_conforms_to, models.Severity.REQUIRED, False, - ["RO-Crate Metadata File Descriptor required properties"], + ["RO-Crate Metadata File Descriptor REQUIRED properties"], ["The RO-Crate metadata file descriptor MUST have a `conformsTo` " "property with the RO-Crate specification version"] ) diff --git a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index a76a597b..181ab7b6 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -42,7 +42,7 @@ def test_invalid_root_date(): paths.invalid_root_date, models.Severity.RECOMMENDED, False, - ["RO-Crate Data Entity definition: RECOMMENDED properties"], + ["RO-Crate Root Data Entity definition RECOMMENDED properties"], ["The datePublished of the Root Data Entity MUST be a valid ISO 8601 date"] ) @@ -53,7 +53,7 @@ def test_missing_root_name(): paths.missing_root_name, models.Severity.RECOMMENDED, False, - ["RO-Crate Data Entity definition: RECOMMENDED properties"], + ["RO-Crate Root Data Entity definition RECOMMENDED properties"], ["The Root Data Entity SHOULD have a schema_org:name"] ) @@ -64,7 +64,7 @@ def test_missing_root_description(): paths.missing_root_description, models.Severity.RECOMMENDED, False, - ["RO-Crate Data Entity definition: RECOMMENDED properties"], + ["RO-Crate Root Data Entity definition RECOMMENDED properties"], ["The Root Data Entity SHOULD have a schema_org:description"] ) @@ -75,7 +75,7 @@ def test_missing_root_license(): paths.missing_root_license, models.Severity.RECOMMENDED, False, - ["RO-Crate Data Entity definition: RECOMMENDED properties"], + ["RO-Crate Root Data Entity definition RECOMMENDED properties"], ["The Root Data Entity SHOULD have a link to a Contextual Entity representing the schema_org:license type"] ) From 4240f9561a33ee6aaf8596c4f05bf2a48f260dd0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 19:39:52 +0200 Subject: [PATCH 428/902] test(test-conf): :white_check_mark: fix test data --- .../invalid_entity_about_type/ro-crate-metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about_type/ro-crate-metadata.json b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about_type/ro-crate-metadata.json index 107a46ae..cb32f4d1 100644 --- a/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about_type/ro-crate-metadata.json +++ b/tests/data/crates/invalid/1_file_descriptor_metadata/invalid_entity_about_type/ro-crate-metadata.json @@ -57,7 +57,7 @@ }, { "@id": "ro-crate-metadata.json", - "@type": "CreativeWorkInvalid", + "@type": "CreativeWork", "about": { "@id": "my-workflow.ga" }, From 063a5aba3e1559a37462a8040d9ff7ebee4256f6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 19:46:59 +0200 Subject: [PATCH 429/902] refactor(profiles/ro-crate): :truck: update name and description of Root Data Entity shape (RECOMMENDED) --- .../ro-crate/should/2_root_data_entity_metadata.ttl | 6 +++--- .../profiles/ro-crate/must/test_root_data_entity.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) 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 0edc43bc..9de834c3 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 @@ -9,9 +9,9 @@ ro:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; - sh:name "RO-Crate Root Data Entity definition RECOMMENDED properties" ; - sh:description """The Root Data Entity SHOULD have the properties - `name`, `description` and `license` defined in + sh:name "RO-Crate Root Data Entity RECOMMENDED properties" ; + sh:description """The Root Data Entity SHOULD be denoted by the string `/` and + SHOULD have the properties `name`, `description` and `license` defined in """; sh:targetClass rocrate:RootDataEntity ; sh:property [ diff --git a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index 181ab7b6..4c080a5f 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -42,7 +42,7 @@ def test_invalid_root_date(): paths.invalid_root_date, models.Severity.RECOMMENDED, False, - ["RO-Crate Root Data Entity definition RECOMMENDED properties"], + ["RO-Crate Root Data Entity RECOMMENDED properties"], ["The datePublished of the Root Data Entity MUST be a valid ISO 8601 date"] ) @@ -53,7 +53,7 @@ def test_missing_root_name(): paths.missing_root_name, models.Severity.RECOMMENDED, False, - ["RO-Crate Root Data Entity definition RECOMMENDED properties"], + ["RO-Crate Root Data Entity RECOMMENDED properties"], ["The Root Data Entity SHOULD have a schema_org:name"] ) @@ -64,7 +64,7 @@ def test_missing_root_description(): paths.missing_root_description, models.Severity.RECOMMENDED, False, - ["RO-Crate Root Data Entity definition RECOMMENDED properties"], + ["RO-Crate Root Data Entity RECOMMENDED properties"], ["The Root Data Entity SHOULD have a schema_org:description"] ) @@ -75,7 +75,7 @@ def test_missing_root_license(): paths.missing_root_license, models.Severity.RECOMMENDED, False, - ["RO-Crate Root Data Entity definition RECOMMENDED properties"], + ["RO-Crate Root Data Entity RECOMMENDED properties"], ["The Root Data Entity SHOULD have a link to a Contextual Entity representing the schema_org:license type"] ) From 5f251c1a31eff7bdacb488100e18f3b361bdc3af Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 19:50:40 +0200 Subject: [PATCH 430/902] feat(profiles/ro-crate): :sparkles: make the rule to mark licence entities hidden --- .../profiles/ro-crate/must/5_contextual_entity.ttl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/profiles/ro-crate/must/5_contextual_entity.ttl b/rocrate_validator/profiles/ro-crate/must/5_contextual_entity.ttl index 1bbd95bc..6fa159a5 100644 --- a/rocrate_validator/profiles/ro-crate/must/5_contextual_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/must/5_contextual_entity.ttl @@ -10,7 +10,7 @@ @prefix rocrate: . -rocrate:FindLicenseEntity a sh:NodeShape ; +rocrate:FindLicenseEntity a sh:NodeShape, sh:hidden ; sh:name "Identify License Entity" ; sh:description """Mark a license entity any Data Entity referenced by the `schema:licence` property.""" ; sh:target [ From 85beb16d2b314c8b1c47f6c3cbfcaab99cfb1d2f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 29 May 2024 20:13:51 +0200 Subject: [PATCH 431/902] refactor(profiles/ro-crate): :recycle: update name and description of some shapes --- .../should/2_root_data_entity_metadata.ttl | 40 +++++++++---------- .../ro-crate/must/test_root_data_entity.py | 6 +-- 2 files changed, 21 insertions(+), 25 deletions(-) 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 9de834c3..7860f37e 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 @@ -11,52 +11,48 @@ ro:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; sh:name "RO-Crate Root Data Entity RECOMMENDED properties" ; sh:description """The Root Data Entity SHOULD be denoted by the string `/` and - SHOULD have the properties `name`, `description` and `license` defined in - """; + SHOULD have the properties `name`, `description` and `license` defined as described + in the RO-Crate specification """; sh:targetClass rocrate:RootDataEntity ; sh:property [ a sh:PropertyShape ; - sh:name "Name of the Root Data Entity" ; - sh:description """The Root Data Entity SHOULD have - a schema_org:name to identify the dataset to human well enough - to disanbiguate it from other datasets""" ; + 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 SHOULD have a schema_org:name" ; + sh:message "The Root Data Entity SHOULD have a `name` property (as specified by schema.org)" ; ] ; sh:property [ a sh:PropertyShape ; - sh:name "Description of the Root Data Entity" ; - sh:description """The Root Data Entity SHOULD have - a schema_org:description to further elaborate on the name - of the dataset and provide a summary in which the dataset is important""" ; + 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 SHOULD have a schema_org:description" ; + sh:message "The Root Data Entity SHOULD have a `description` property (as specified by schema.org)" ; ] ; sh:property [ a sh:PropertyShape ; - sh:name "License of the Root Data Entity" ; - sh:description """The Root Data Entity SHOULD - link to a Contextual Entity in the RO-Crate Metadata File - with a name and description.""" ; + sh:name "Root Data Entity: `licence` property" ; + sh:description """Check if the Root Data Entity includes a `license` (as specified by schema.org) + to provide information about the license of the dataset.""" ; sh:nodeKind sh:BlankNodeOrIRI ; sh:path schema_org:license; sh:minCount 1 ; sh:message """The Root Data Entity SHOULD have a link to a Contextual Entity representing the schema_org:license type""" ; ] ; - sh:property [ a sh:PropertyShape ; - sh:name "Date Published of the Root Data Entity" ; - sh:description """The datePublished of the Root Data Entity MUST be a valid ISO 8601 date - SHOULD be specified to at least the precision of a day. - """ ; + 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.""" ; 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 datePublished of the Root Data Entity MUST be a valid ISO 8601 date" ; + sh:message "The Root Data Entity SHOULD have a `datePublished` property (as specified by schema.org) with a valid ISO 8601 date and the precision of at least the day level" ; ] . diff --git a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index 4c080a5f..c5e78f3e 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -43,7 +43,7 @@ def test_invalid_root_date(): models.Severity.RECOMMENDED, False, ["RO-Crate Root Data Entity RECOMMENDED properties"], - ["The datePublished of the Root Data Entity MUST be a valid ISO 8601 date"] + ["The Root Data Entity SHOULD have a `datePublished` property (as specified by schema.org) with a valid ISO 8601 date and the precision of at least the day level"] ) @@ -54,7 +54,7 @@ def test_missing_root_name(): models.Severity.RECOMMENDED, False, ["RO-Crate Root Data Entity RECOMMENDED properties"], - ["The Root Data Entity SHOULD have a schema_org:name"] + ["The Root Data Entity SHOULD have a `name` property (as specified by schema.org)"] ) @@ -65,7 +65,7 @@ def test_missing_root_description(): models.Severity.RECOMMENDED, False, ["RO-Crate Root Data Entity RECOMMENDED properties"], - ["The Root Data Entity SHOULD have a schema_org:description"] + ["The Root Data Entity SHOULD have a `description` property (as specified by schema.org)"] ) From 35437ab08b9c3f4f34eb97750ae5c106e2bef392 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 30 May 2024 08:19:44 +0200 Subject: [PATCH 432/902] refactor(profiles/ro-crate): :recycle: update description of shape for Root RECOMMENDED properties --- .../profiles/ro-crate/should/2_root_data_entity_metadata.ttl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7860f37e..d1688b73 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 @@ -10,8 +10,8 @@ ro:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; sh:name "RO-Crate Root Data Entity RECOMMENDED properties" ; - sh:description """The Root Data Entity SHOULD be denoted by the string `/` and - SHOULD have the properties `name`, `description` and `license` defined as described + sh:description """The Root Data Entity SHOULD have + the properties `name`, `description` and `license` defined as described in the RO-Crate specification """; sh:targetClass rocrate:RootDataEntity ; sh:property [ From 5a860467c0c2c3ff5547bff7c368859dcd3a4056 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 30 May 2024 08:23:45 +0200 Subject: [PATCH 433/902] feat(profiles/ro-crate): :sparkles: add SHOULD check for the relative value of the Root --- .../should/2_root_data_entity_relative_uri.py | 70 ++++++++ .../invalid_root_value/ro-crate-metadata.json | 154 ++++++++++++++++++ .../ro-crate-metadata.json | 154 ++++++++++++++++++ .../ro-crate/must/test_root_data_entity.py | 11 ++ tests/ro_crates.py | 8 + 5 files changed, 397 insertions(+) create mode 100644 rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py create mode 100644 tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_value/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/2_root_data_entity_metadata/recommended_root_value/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py b/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py new file mode 100644 index 00000000..35544903 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py @@ -0,0 +1,70 @@ +import json +import logging +from typing import Optional + +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import (PyFunctionCheck, check, + requirement) + +# set up logging +logger = logging.getLogger(__name__) + + +@requirement(name="RO-Crate Root Data Entity RECOMMENDED value") +class RootDataEntityRelativeURI(PyFunctionCheck): + """ + The Root Data Entity SHOULD be denoted by the string / + """ + + _json_dict_cache: Optional[dict] = None + + def get_json_dict(self, context: ValidationContext) -> dict: + if self._json_dict_cache is None or \ + self._json_dict_cache['file_descriptor_path'] != context.file_descriptor_path: + # invalid cache + try: + with open(context.file_descriptor_path, "r") as file: + self._json_dict_cache = dict( + json=json.load(file), + file_descriptor_path=context.file_descriptor_path) + except Exception as e: + context.result.add_error( + f'file descriptor "{context.rel_fd_path}" is not in the correct format', self) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return {} + return self._json_dict_cache['json'] + + def find_entity(self, context: ValidationContext, entity_id: str) -> dict: + json_dict = self.get_json_dict(context) + for entity in json_dict["@graph"]: + if entity["@id"] == entity_id: + return entity + return {} + + def find_property(self, context: ValidationContext, entity_id: str, property_name: str) -> dict: + entity = self.find_entity(context, entity_id) + if entity: + return entity.get(property_name, {}) + return {} + + @check(name="Root Data Entity: RECOMMENDED value") + def check_relative_uris(self, context: ValidationContext) -> bool: + """Check if the Root Data Entity is denoted by the string `./` in the file descriptor JSON-LD""" + about_property = self.find_property(context, "ro-crate-metadata.json", "about") + if not about_property: + context.result.add_error( + 'Unable to find the about property on `ro-crate-metadata.json`', self) + return False + root_entity_id = about_property.get("@id", None) + if not root_entity_id: + context.result.add_error( + 'Unable to identity the Root Data Entity from the `about` property of the `ro-crate-metadata.json`', self) + return False + # check relative URIs + if not root_entity_id == './': + context.result.add_error( + 'Root Data Entity URI is not denoted by the string `./`', self) + return False + + return False diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_value/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_value/ro-crate-metadata.json new file mode 100644 index 00000000..a1ca75e0 --- /dev/null +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_root_value/ro-crate-metadata.json @@ -0,0 +1,154 @@ +{ + "@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": "./invalidRootValue", + "@type": "Dataset", + "datePublished": "2024-01-22T15:36:43+00:00", + "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": "./invalidRootValue" + }, + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" + } + ] + }, + { + "@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/data/crates/invalid/2_root_data_entity_metadata/recommended_root_value/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/recommended_root_value/ro-crate-metadata.json new file mode 100644 index 00000000..fd5d3b61 --- /dev/null +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/recommended_root_value/ro-crate-metadata.json @@ -0,0 +1,154 @@ +{ + "@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": "https://TheROCrateRoot/", + "@type": "Dataset", + "datePublished": "2024-01-22T15:36:43+00:00", + "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": "https://TheROCrateRoot/" + }, + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" + } + ] + }, + { + "@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/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py index c5e78f3e..392acad9 100644 --- a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/must/test_root_data_entity.py @@ -36,6 +36,17 @@ def test_invalid_root_data_entity_value(): ) +def test_recommended_root_data_entity_value(): + """Test a RO-Crate with an invalid root data entity value.""" + do_entity_test( + paths.recommended_root_value, + models.Severity.RECOMMENDED, + False, + ["RO-Crate Root Data Entity RECOMMENDED value"], + ["Root Data Entity URI is not denoted by the string `./`"] + ) + + def test_invalid_root_date(): """Test a RO-Crate with an invalid root data entity date.""" do_entity_test( diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 13642378..fb6f57a2 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -58,6 +58,14 @@ def missing_root(self) -> Path: def invalid_root_type(self) -> Path: return self.base_path / "invalid_root_type" + @property + def invalid_root_value(self) -> Path: + return self.base_path / "invalid_root_value" + + @property + def recommended_root_value(self) -> Path: + return self.base_path / "recommended_root_value" + @property def invalid_root_date(self) -> Path: return self.base_path / "invalid_root_date" From 379850df39917d45a0b341d6149f72788a058b33 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 30 May 2024 08:38:50 +0200 Subject: [PATCH 434/902] refactor(profiles): :truck: rename entities --- rocrate_validator/profiles/ro-crate/ontology.ttl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/ontology.ttl b/rocrate_validator/profiles/ro-crate/ontology.ttl index 46d71a24..5e5f6112 100644 --- a/rocrate_validator/profiles/ro-crate/ontology.ttl +++ b/rocrate_validator/profiles/ro-crate/ontology.ttl @@ -19,7 +19,7 @@ # # ### http://schema.org/about # # schema:about rdf:type owl:ObjectProperty , # # owl:FunctionalProperty ; -# # rdfs:domain rocrate:ROCrateDescriptor ; +# # rdfs:domain rocrate:ROCrateMetadataFileDescriptor ; # # rdfs:range rocrate:RootDataEntity ; # # rdfs:label "about"@en . @@ -105,18 +105,18 @@ rocrate:File rdf:type owl:Class ; # # rdfs:subClassOf rocrate:Workflow . -# # ### https://w3id.org/ro/crate/1.1/ROCrateDescriptor -# rocrate:ROCrateDescriptor rdf:type owl:Class ; +# # ### https://w3id.org/ro/crate/1.1/ROCrateMetadataFileDescriptor +# rocrate:ROCrateMetadataFileDescriptor rdf:type owl:Class ; # owl:equivalentClass [ rdf:type owl:Class ; # owl:oneOf ( ro:ro-crate-metadata.json # ) # ] ; -# rdfs:label "ROCrateDescriptor"@en . +# rdfs:label "ROCrateMetadataFileDescriptor"@en . # # [ rdf:type owl:Restriction ; # # owl:onProperty schema:about ; # # owl:someValuesFrom owl:Thing # # ] ; -# # rdfs:label "ROCrateDescriptor"@en . +# # rdfs:label "ROCrateMetadataFileDescriptor"@en . # # ### https://w3id.org/ro/crate/1.1/RootDataEntity @@ -155,7 +155,7 @@ rocrate:File rdf:type owl:Class ; # ### ./ro-crate-metadata.json # ro:ro-crate-metadata.json rdf:type owl:NamedIndividual , -# rocrate:ROCrateDescriptor . +# rocrate:ROCrateMetadataFileDescriptor . # # ################################################################# @@ -165,7 +165,7 @@ rocrate:File rdf:type owl:Class ; [ rdf:type owl:Class ; owl:unionOf ( schema:Dataset schema:MediaObject - rocrate:ROCrateDescriptor + rocrate:ROCrateMetadataFileDescriptor ) ; rdfs:subClassOf schema:CreativeWork ] . @@ -174,6 +174,6 @@ rocrate:File rdf:type owl:Class ; [ rdf:type owl:AllDisjointClasses ; owl:members ( schema:Dataset schema:MediaObject - rocrate:ROCrateDescriptor + rocrate:ROCrateMetadataFileDescriptor ) ] . From ffd426e9348c732f07e6ecad27731de33b8794b2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 30 May 2024 10:22:53 +0200 Subject: [PATCH 435/902] refactor(profiles/ro-crate): :recycle: reorder DataEntity shapes --- .../ro-crate/must/4_data_entity_metadata.ttl | 63 ++++++++++++++----- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl index 95eac4a5..672bd3e8 100644 --- a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl @@ -10,9 +10,27 @@ @prefix rocrate: . +ro: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 sbsolute URI""" ; + sh:targetClass rocrate: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:FileDataEntity a sh:NodeShape ; - sh:name "Definition of File Data Entity" ; - sh:description """A File Data Entity is a digital object that is stored in a file format""" ; + 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:sparqlPrefixes ; @@ -26,13 +44,14 @@ ro:FileDataEntity a sh:NodeShape ; ] ; sh:property [ - sh:name "Type of File Data Entities" ; - sh:description """Data Entities representing files MUST have "File" as a value for @type. - File is an RO-Crate alias for http://schema.org/MediaObject. The term File here is liberal, and includes โ€œdownloadableโ€ resources where @id is an absolute URI. + 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:hasValue rocrate:File ; sh:severity sh:Violation ; + sh:message """File Data Entities MUST have "File" as a value for @type.""" ; ] ; # Expand data graph with triples from the file data entity @@ -45,8 +64,10 @@ ro:FileDataEntity a sh:NodeShape ; ro:DirectoryDataEntity a sh:NodeShape ; - sh:name "Definition of Directory Data Entity" ; - sh:description """A Directory Data Entity is a digital object that is stored in a directory""" ; + 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:sparqlPrefixes ; @@ -58,16 +79,6 @@ ro:DirectoryDataEntity a sh:NodeShape ; } """ ] ; - sh:targetClass rocrate:Directory ; - sh:property [ - sh:name "Type of Directory Data Entities" ; - sh:description """Data Entities representing directories MUST have "Directory" as a value for @type. - Directory is an RO-Crate alias for http://schema.org/Dataset. - """ ; - sh:path rdf:type ; - sh:hasValue schema_org:Dataset ; - sh:severity sh:Violation ; - ] ; # Decomment for debugging # sh:property [ @@ -92,6 +103,24 @@ ro:DirectoryDataEntity a sh:NodeShape ; sh:subject sh:this ; sh:predicate rdf:type ; sh:object rocrate:DataEntity ; + ] ; + + 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 ; + ] ; + + sh:property [ + a sh:PropertyShape ; + sh:name "Directory Data Entity: REQUIRED value restriction" ; + sh:description """Check if the Directory Data Entity ends with `/`""" ; + sh:path [ sh:inversePath rdf:type ] ; + sh:minCount 1 ; + sh:message """The Directory Data Entity URI MUST end with `/`""" ; + sh:pattern "/$" ; ] . From 2c2b1d690c60f8465e2e0e9e1434a3ff29d631ac Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 30 May 2024 10:43:10 +0200 Subject: [PATCH 436/902] fix(profiles/ro-crate): :bug: redifine check of value restriction of directory identifier --- .../profiles/ro-crate/must/4_data_entity_metadata.ttl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl index 672bd3e8..a6666ae1 100644 --- a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl @@ -111,7 +111,12 @@ ro:DirectoryDataEntity a sh:NodeShape ; sh:path rdf:type ; sh:hasValue schema_org:Dataset ; sh:severity sh:Violation ; - ] ; + ] . + +ro:DirectoryDataEntityRequiredValueRestriction a sh:NodeShape ; + sh:name "Directory Data Entity: REQUIRED value restriction" ; + sh:description """A Directory Data Entity MUST end with `/`""" ; + sh:targetNode rocrate:Directory ; sh:property [ a sh:PropertyShape ; @@ -123,7 +128,6 @@ ro:DirectoryDataEntity a sh:NodeShape ; sh:pattern "/$" ; ] . - # Uncomment for debugging # ro:testDirectory a sh:NodeShape ; # sh:name "Definition of Test Directory" ; From 30071023695ebd2b93187c9cc205be1ec0282cb7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 31 May 2024 14:20:28 +0200 Subject: [PATCH 437/902] fix(profiles/ro-crate): :sparkles: update query to identify Directory DataEntity instances --- .../profiles/ro-crate/must/4_data_entity_metadata.ttl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl index a6666ae1..0279229b 100644 --- a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl @@ -75,7 +75,9 @@ ro:DirectoryDataEntity a sh:NodeShape ; SELECT ?this WHERE { ?this a schema:Dataset . - FILTER NOT EXISTS { ?this a rocrate:RootDataEntity } + ?metadatafile schema:about ?root . + FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER(?this != ?root) } """ ] ; From 0b1ac6b2aaa0e15a89d0fba2f175658056c7df4f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 31 May 2024 14:37:24 +0200 Subject: [PATCH 438/902] feat(shacl): :sparkles: improve the validation of profiles that use inheritance Allow inheritance of ontologies and shapes from more general profiles to more specialized ones. --- .../requirements/shacl/checks.py | 36 ++--- .../requirements/shacl/validator.py | 127 +++++++++++++++--- 2 files changed, 130 insertions(+), 33 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 7009919a..b9598ff6 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -5,7 +5,9 @@ ValidationContext) from rocrate_validator.requirements.shacl.models import Shape -from .validator import SHACLValidationContext, SHACLValidator +from .validator import (SHACLValidationAlreadyProcessed, + SHACLValidationContext, SHACLValidationContextManager, + SHACLValidationSkip, SHACLValidator) logger = logging.getLogger(__name__) @@ -38,16 +40,24 @@ def shape(self) -> Shape: return self._shape def execute_check(self, context: ValidationContext): - # retrieve the SHACLValidationContext - shacl_context = SHACLValidationContext.get_instance(context) - + try: + result = None + with SHACLValidationContextManager(self, context) as ctx: + result = self.__do_execute_check__(ctx) + ctx.current_validation_result = result + return result + except SHACLValidationAlreadyProcessed as e: + logger.debug("SHACL Validation of profile %s already processed", self.requirement.profile) + return e.result + + def __do_execute_check__(self, shacl_context: SHACLValidationContext): # get the shapes registry shapes_registry = shacl_context.shapes_registry # set up the input data for the validator ontology_graph = shacl_context.ontology_graph data_graph = shacl_context.data_graph - shapes_graph = shacl_context.shapes_graph + shapes_graph = shapes_registry.shapes_graph # uncomment to save the graphs to the logs folder (for debugging purposes) # data_graph.serialize("logs/data_graph.ttl", format="turtle") @@ -55,11 +65,6 @@ def execute_check(self, context: ValidationContext): # if ontology_graph: # ontology_graph.serialize("logs/ontology_graph.ttl", format="turtle") - # if the SHACLvalidation has been done, skip the check - result = getattr(context, "shacl_validation", None) - if result is not None: - return result - # validate the data graph shacl_validator = SHACLValidator(shapes_graph=shapes_graph, ont_graph=ontology_graph) shacl_result = shacl_validator.validate( @@ -68,7 +73,6 @@ def execute_check(self, context: ValidationContext): logger.debug("Validation '%s' conforms: %s", self.name, shacl_result.conforms) # store the validation result in the context result = shacl_result.conforms - setattr(context, "shacl_validation", result) # if the validation failed, add the issues to the context if not shacl_result.conforms: logger.debug("Validation failed") @@ -78,11 +82,11 @@ def execute_check(self, context: ValidationContext): assert shape is not None, "Unable to map the violation to a shape" requirementCheck = SHACLCheck.get_instance(shape) assert requirementCheck is not None, "The requirement check cannot be None" - c = shacl_context.result.add_check_issue(message=violation.get_result_message(shacl_context.rocrate_path), - check=requirementCheck, - severity=violation.get_result_severity()) - logger.debug("Added validation issue to the context: %s", c) - if context.fail_fast: + c = shacl_context.result.add_check_issue(message=violation.get_result_message(shacl_context.rocrate_path), + check=requirementCheck, + severity=violation.get_result_severity()) + logger.debug("Added validation issue to the context: %s", c) + if shacl_context.base_context.fail_fast: break return result diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index cdaf2909..073fe8fb 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -10,8 +10,8 @@ from rdflib import Graph from rdflib.term import Node, URIRef -from rocrate_validator.models import (Severity, ValidationContext, - ValidationResult) +from rocrate_validator.models import (Profile, RequirementCheck, Severity, + ValidationContext, ValidationResult) from rocrate_validator.requirements.shacl.utils import (make_uris_relative, map_severity) @@ -25,6 +25,49 @@ logger = logging.getLogger(__name__) +class SHACLValidationSkip(Exception): + pass + + +class SHACLValidationAlreadyProcessed(Exception): + + def __init__(self, profile_name: str, result: SHACLValidationResult) -> None: + super().__init__(f"Profile {profile_name} has already been processed") + self.result = result + + +class SHACLValidationContextManager: + + def __init__(self, check: RequirementCheck, context: ValidationContext) -> None: + self._check = check + self._profile = check.requirement.profile + self._context = context + self._shacl_context = SHACLValidationContext.get_instance(context) + + def __enter__(self) -> SHACLValidationContext: + logger.debug("Entering SHACLValidationContextManager") + if not self._shacl_context.__set_current_validation_profile__(self._profile): + raise SHACLValidationAlreadyProcessed( + self._profile.name, self._shacl_context.get_validation_result(self._profile)) + return self._shacl_context + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self._shacl_context.__unset_current_validation_profile__() + logger.debug("Exiting SHACLValidationContextManager") + + @property + def context(self) -> ValidationContext: + return self._context + + @property + def shacl_context(self) -> SHACLValidationContext: + return self._shacl_context + + @property + def check(self) -> RequirementCheck: + return self._check + + class SHACLValidationContext(ValidationContext): def __init__(self, context: ValidationContext): @@ -33,27 +76,81 @@ def __init__(self, context: ValidationContext): # reference to the ontology path self._ontology_path: Path = None + # reference to the contextual ShapeRegistry instance + self._shapes_registry: ShapesRegistry = ShapesRegistry() + + # processed profiles + self._processed_profiles: dict[str, bool] = {} + + # reference to the current validation profile + self._current_validation_profile: Profile = None + + # store the validation result of the current profile + self._validation_result: SHACLValidationResult = None + + # reference to the contextual ontology graph + self._ontology_graph: Graph = Graph() + + def __set_current_validation_profile__(self, profile: Profile) -> bool: + if not profile.name in self._processed_profiles: + # augment the ontology graph with the profile ontology + self._ontology_graph += self.__load_ontology_graph__(profile.name) + # augment the shapes registry with the profile shapes + profile_registry = ShapesRegistry.get_instance(profile) + profile_shapes = profile_registry.get_shapes() + profile_shapes_graph = profile_registry.shapes_graph + + # 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 + + def __unset_current_validation_profile__(self) -> None: + self._current_validation_profile = None + @property def base_context(self) -> ValidationContext: return self._base_context + @property + def current_validation_profile(self) -> Profile: + return self._current_validation_profile + + @property + def current_validation_result(self) -> SHACLValidationResult: + return self._validation_result + + @current_validation_result.setter + def current_validation_result(self, result: ValidationResult): + assert self._current_validation_profile is not None, "Invalid state: current profile not set" + # store the validation result + self._validation_result = result + # mark the profile as processed and store the result + self._processed_profiles[self._current_validation_profile.name] = result + + def get_validation_result(self, profile: Profile) -> Optional[bool]: + assert profile is not None, "Invalid profile" + return self._processed_profiles.get(profile.name, None) + @property def result(self) -> ValidationResult: return self.base_context.result @property def shapes_registry(self) -> ShapesRegistry: - return ShapesRegistry.get_instance(self.base_context.profile) + return self._shapes_registry @property def shapes_graph(self) -> Graph: return self.shapes_registry.shapes_graph - @property - def ontology_path(self) -> Path: + def __get_ontology_path__(self, profile_name: str, ontology_filename: str = DEFAULT_ONTOLOGY_FILE) -> Path: if not self._ontology_path: - # TODO: implement custom ontology file ??? - supported_path = f"{self.profiles_path}/{self.profile_name}/{DEFAULT_ONTOLOGY_FILE}" + supported_path = f"{self.profiles_path}/{profile_name}/{ontology_filename}" if self.settings.get("ontology_path", None): logger.warning("Detected an ontology path. Custom ontology file is not yet supported." f"Use {supported_path} to provide an ontology for your profile.") @@ -61,24 +158,20 @@ def ontology_path(self) -> Path: self._ontology_path = Path(supported_path) return self._ontology_path - def __load_ontology_graph__(self): + def __load_ontology_graph__(self, profile_name: str, ontology_filename: Optional[str] = DEFAULT_ONTOLOGY_FILE) -> Graph: # load the graph of ontologies ontology_graph = None - if os.path.exists(self.ontology_path): - logger.debug("Loading ontologies: %s", self.ontology_path) + ontology_path = self.__get_ontology_path__(profile_name, ontology_filename) + if os.path.exists(ontology_path): + logger.debug("Loading ontologies: %s", ontology_path) ontology_graph = Graph() - ontology_graph.parse(self.ontology_path, format="ttl", + ontology_graph.parse(ontology_path, format="ttl", publicID=self.publicID) return ontology_graph @property def ontology_graph(self) -> Graph: - ontology_key = "_shacl_ontology" - ontology = getattr(self.base_context, ontology_key, None) - if not ontology: - ontology = self.__load_ontology_graph__() - setattr(self.base_context, ontology_key, ontology) - return ontology + return self._ontology_graph @ classmethod def get_instance(cls, context: ValidationContext) -> SHACLValidationContext: From 109576d14ae1867e849b4c822c2817585b55019b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 31 May 2024 14:40:12 +0200 Subject: [PATCH 439/902] fix(shacl): :bug: allow to store parent nodes --- rocrate_validator/requirements/shacl/models.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 03770720..b2746915 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -23,7 +23,7 @@ class SHACLNode: name: str = None description: str = None - def __init__(self, node: Node, graph: Graph): + def __init__(self, node: Node, graph: Graph, parent: Optional[SHACLNode] = None): # store the shape node self._node = node @@ -31,6 +31,8 @@ def __init__(self, node: Node, graph: Graph): self._graph = graph # cache the hash self._hash = None + # store the parent shape + self._parent = parent # inject attributes of the shape to the object inject_attributes(self, graph, node) @@ -45,6 +47,11 @@ def graph(self): """Return the subgraph of the shape""" return self._graph + @property + def parent(self) -> Optional[SHACLNode]: + """Return the parent shape of the shape""" + return self._parent + @property def level(self) -> RequirementLevel: """Return the requirement level of the shape""" From 63321e176ba902a5688f376d88abc53bead06d80 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 31 May 2024 14:40:56 +0200 Subject: [PATCH 440/902] feat(shacl): :sparkles: more methods to manage shapes of a registry --- rocrate_validator/requirements/shacl/models.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index b2746915..c94b8127 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -207,6 +207,11 @@ def add_shape(self, shape: Shape): assert isinstance(shape, Shape), "Invalid shape" self._shapes[f"{hash(shape)}"] = shape + def remove_shape(self, shape: Shape): + assert isinstance(shape, Shape), "Invalid shape" + self._shapes.pop(f"{hash(shape)}", None) + self._shapes_graph -= shape.graph + def get_shape(self, hash_value: int) -> Optional[Shape]: logger.debug("Searching for shape %s in the registry: %s", hash_value, self._shapes) result = self._shapes.get(f"{hash_value}", None) @@ -215,6 +220,14 @@ def get_shape(self, hash_value: int) -> Optional[Shape]: raise ValueError(f"Shape not found in the registry: {hash_value}") return result + def get_shape_key(self, shape: Shape) -> str: + assert isinstance(shape, Shape), "Invalid shape" + return f"{hash(shape)}" + + def extend(self, shapes: dict[str, Shape], graph: Graph) -> None: + self._shapes.update(shapes) + self._shapes_graph += graph + def get_shape_by_name(self, name: str) -> Optional[Shape]: for shape in self._shapes.values(): if shape.name == name: @@ -226,7 +239,9 @@ def get_shapes(self) -> dict[str, Shape]: @property def shapes_graph(self) -> Graph: - return self._shapes_graph + g = Graph() + g += self._shapes_graph + return g def load_shapes(self, shapes_path: Union[str, Path], publicID: Optional[str] = None) -> list[Shape]: """ From b70b7196dd8c1707bfe373b5e9f068fab69e40cf Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 31 May 2024 16:24:55 +0200 Subject: [PATCH 441/902] feat(core): :sparkles: enable requirement checks overriding Enable by default the ability to override requirements from more generic profiles to more specific ones. If the feature is not enabled, an exception will be raised. --- rocrate_validator/models.py | 54 +++++++++++++++++-- .../requirements/shacl/validator.py | 13 +++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index e786f6af..c6142134 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -372,7 +372,12 @@ def __do_validate__(self, context: ValidationContext) -> bool: all_passed = True for check in self._checks: try: - logger.debug("Running check '%s' - Desc: %s", check.name, check.description) + logger.debug("Running check '%s' - Desc: %s - overridden: %s.%s", + check.name, check.description, check.overridden_by, + check.overridden_by.requirement.profile if check.overridden_by else None) + if check.overridden: + logger.debug("Skipping check '%s' because overridden by '%s'", check.name, check.overridden_by.name) + continue check_result = check.execute_check(context) logger.debug("Ran check '%s'. Got result %s", check.name, check_result) if not isinstance(check_result, bool): @@ -507,6 +512,7 @@ def __init__(self, self._order_number = 0 self._name = name self._description = description + self._overridden_by: RequirementCheck = None @property def order_number(self) -> int: @@ -546,6 +552,20 @@ def level(self) -> RequirementLevel: def severity(self) -> Severity: return self.requirement.level.severity + @property + def overridden_by(self) -> RequirementCheck: + return self._overridden_by + + @overridden_by.setter + def overridden_by(self, value: RequirementCheck) -> None: + assert value is None or isinstance(value, RequirementCheck) and value != self, \ + f"Invalid value for overridden_by: {value}" + self._overridden_by = value + + @property + def overridden(self) -> bool: + return self._overridden_by is not None + @abstractmethod def execute_check(self, context: ValidationContext) -> bool: raise NotImplementedError() @@ -766,6 +786,7 @@ class ValidationSettings: profiles_path: Path = DEFAULT_PROFILES_PATH profile_name: str = DEFAULT_PROFILE_NAME inherit_profiles: bool = True + allow_shapes_override: bool = True # Ontology and inference settings ontology_path: Optional[Path] = None inference: Optional[VALID_INFERENCE_OPTIONS_TYPES] = None @@ -983,6 +1004,33 @@ def __load_profiles__(self) -> OrderedDict[str, Profile]: # Check if the target profile is in the list of profiles if self.profile_name not in profiles: raise ProfileNotFound(f"Profile '{self.profile_name}' not found in '{self.profiles_path}'") + + # navigate the profiles and check for overridden checks + # if the override is enabled in the settings + # overridden checks should be marked as such + # otherwise, raise an error + profiles_checks = {} + # visit the profiles in reverse order + # (the order is important to visit the most specific profiles first) + for profile in sorted(profiles.values(), reverse=True): + profile_checks = [_ for r in profile.get_requirements() for _ in r.get_checks()] + profile_check_names = [] + for check in profile_checks: + # ย find duplicated checks and raise an error + if check.name in profile_check_names: + raise DuplicateRequirementCheck(check.name, profile.name) + # ย add check to the list + profile_check_names.append(check.name) + # ย mark overridden checks + check_chain = profiles_checks.get(check.name, None) + if not check_chain: + profiles_checks[check.name] = [check] + elif self.settings.get("allow_shapes_override", True): + check.overridden_by = check_chain[-1] + check_chain.append(check) + else: + raise DuplicateRequirementCheck(check.name, profile.name) + return profiles @property @@ -990,7 +1038,3 @@ def profiles(self) -> OrderedDict[str, Profile]: if not self._profiles: self._profiles = self.__load_profiles__() return self._profiles.copy() - - @property - def profile(self) -> Profile: - return list(self.profiles.values())[-1] diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 073fe8fb..28fc7a48 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -100,6 +100,19 @@ def __set_current_validation_profile__(self, profile: Profile) -> bool: profile_shapes = profile_registry.get_shapes() profile_shapes_graph = profile_registry.shapes_graph + # enable overriding of checks + if self.settings.get("override_checks", False): + from rocrate_validator.requirements.shacl.requirements import \ + SHACLRequirement + for requirement in [_ for _ in profile.requirements if isinstance(_, SHACLRequirement)]: + logger.debug("Processing requirement: %s", requirement.name) + for check in requirement.get_checks(): + logger.debug("Processing check: %s", check) + if check.overridden: + logger.debug("Overridden check: %s", check) + profile_shapes_graph -= check.shape.graph + profile_shapes.pop(profile_registry.get_shape_key(check.shape)) + # add the shapes to the registry self._shapes_registry.extend(profile_shapes, profile_shapes_graph) # set the current validation profile From 8a84ed8c3cdf25062c1ad8e997114c15ae1e95d9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 31 May 2024 16:32:21 +0200 Subject: [PATCH 442/902] fix(core): :bug: fix requiremnts loading --- rocrate_validator/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index c6142134..d1ddfef1 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -876,9 +876,9 @@ def __do_validate__(self, for profile in profiles: logger.debug("Validating profile %s", profile.name) # perform the requirements validation - if not requirements: - requirements = profile.get_requirements( - context.requirement_severity, exact_match=context.requirement_severity_only) + requirements = profile.get_requirements( + context.requirement_severity, exact_match=context.requirement_severity_only) + logger.debug("Validating profile %s with %s requirements", profile.name, len(requirements)) logger.debug("For profile %s, validating these %s requirements: %s", profile.name, len(requirements), requirements) for requirement in requirements: From 9e961387bd992e3b9871c9cd0555064fc02e3089 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 31 May 2024 16:37:32 +0200 Subject: [PATCH 443/902] feat(core): :sparkles: add more specific error for duplicated requirements --- rocrate_validator/errors.py | 24 ++++++++++++++++++++++++ rocrate_validator/models.py | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 1c017508..81de638b 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -60,6 +60,30 @@ def __repr__(self): return f"ProfileNotFound({self._profile_name!r})" +class DuplicateRequirementCheck(ROCValidatorError): + """Raised when a duplicate requirement check is found.""" + + def __init__(self, check_name: str, profile_name: Optional[str] = None): + self._check_name = check_name + self._profile_name = profile_name + + @property + def check_name(self) -> str: + """The name of the duplicate requirement check.""" + return self._check_name + + @property + def profile_name(self) -> Optional[str]: + """The name of the profile.""" + return self._profile_name + + def __str__(self) -> str: + return f"Duplicate requirement check found: {self._check_name!r} in profile {self._profile_name!r}" + + def __repr__(self): + return f"DuplicateRequirementCheck({self._check_name!r}, {self._profile_name!r})" + + class InvalidSerializationFormat(ROCValidatorError): """Raised when an invalid serialization format is provided.""" diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index d1ddfef1..ef946e4e 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -22,7 +22,7 @@ RDF_SERIALIZATION_FORMATS_TYPES, ROCRATE_METADATA_FILE, VALID_INFERENCE_OPTIONS_TYPES) -from rocrate_validator.errors import InvalidProfilePath, ProfileNotFound +from rocrate_validator.errors import DuplicateRequirementCheck, InvalidProfilePath, ProfileNotFound from rocrate_validator.utils import (get_profiles_path, get_requirement_name_from_file) @@ -797,6 +797,7 @@ class ValidationSettings: inplace: Optional[bool] = False meta_shacl: bool = False iterate_rules: bool = True + target_only_validation: bool = True # Requirement severity settings requirement_severity: Union[str, Severity] = Severity.REQUIRED requirement_severity_only: bool = False From 3942d1aef92b9d950bcf5374ca5373dfb46de728 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 31 May 2024 16:40:40 +0200 Subject: [PATCH 444/902] style: :art: reformat imports --- rocrate_validator/services.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index b98291ea..a609ac74 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -1,8 +1,9 @@ import logging from pathlib import Path -from typing import Union +from typing import Union -from .models import Profile, Severity, ValidationResult, ValidationSettings, Validator +from .models import (Profile, Severity, ValidationResult, ValidationSettings, + Validator) from .utils import get_profiles_path # set the default profiles path From 1c127d56b626064df8f9db2f2a6861a1ceec6862 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 31 May 2024 16:42:41 +0200 Subject: [PATCH 445/902] fix(cli): :bug: properly set flag to enable/disable profile inheritance --- 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 a334064e..70852092 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -118,7 +118,7 @@ def validate(ctx, "profile_name": profile_name, "requirement_severity": requirement_severity, "requirement_severity_only": requirement_severity_only, - "disable_profile_inheritance": disable_profile_inheritance, + "inherit_profiles": not disable_profile_inheritance, "data_path": Path(rocrate_path).absolute(), "ontology_path": Path(ontologies_path).absolute() if ontologies_path else None, "abort_on_first": not no_fail_fast From 9e907ddd5faa90123681cb989c00b6254408666c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 31 May 2024 16:46:26 +0200 Subject: [PATCH 446/902] feat(core): --- rocrate_validator/requirements/shacl/checks.py | 9 +++++++++ rocrate_validator/requirements/shacl/validator.py | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index b9598ff6..caff2782 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -49,6 +49,11 @@ def execute_check(self, context: ValidationContext): except SHACLValidationAlreadyProcessed as e: logger.debug("SHACL Validation of profile %s already processed", self.requirement.profile) return e.result + except SHACLValidationSkip as e: + logger.debug("SHACL Validation of profile %s skipped", self.requirement.profile) + # the validation is postponed to the subsequent profiles + # ย so we return True to continue the validation + return True def __do_execute_check__(self, shacl_context: SHACLValidationContext): # get the shapes registry @@ -82,6 +87,10 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): assert shape is not None, "Unable to map the violation to a shape" requirementCheck = SHACLCheck.get_instance(shape) assert requirementCheck is not None, "The requirement check cannot be None" + # add only the issues for the current profile when the `target_profile_only` mode is disabled + # (issues related to other profiles will be added by the corresponding profile validation) + if requirementCheck.requirement.profile == shacl_context.current_validation_profile or \ + shacl_context.settings.get("target_only_validation", False): c = shacl_context.result.add_check_issue(message=violation.get_result_message(shacl_context.rocrate_path), check=requirementCheck, severity=violation.get_result_severity()) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 28fc7a48..c32566ad 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -49,6 +49,10 @@ def __enter__(self) -> SHACLValidationContext: if not self._shacl_context.__set_current_validation_profile__(self._profile): raise SHACLValidationAlreadyProcessed( self._profile.name, self._shacl_context.get_validation_result(self._profile)) + if self._context.settings.get("target_only_validation", False) and \ + self._profile.name != self._context.settings.get("profile_name", None): + logger.debug("Skipping validation of profile %s", self._profile.name) + raise SHACLValidationSkip(f"Skipping validation of profile {self._profile.name}") return self._shacl_context def __exit__(self, exc_type, exc_val, exc_tb) -> None: From aa98723568758e6c644eee2e81b0a99ef7bf46b5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 31 May 2024 17:41:27 +0200 Subject: [PATCH 447/902] test(core): :white_check_mark: add test of for the override shape feature --- tests/unit/requirements/test_profiles.py | 78 +++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 862f97d4..d3034909 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -3,7 +3,7 @@ import pytest -from rocrate_validator.errors import InvalidProfilePath +from rocrate_validator.errors import DuplicateRequirementCheck, InvalidProfilePath from rocrate_validator.models import (Profile, ValidationContext, ValidationSettings, Validator) from tests.ro_crates import InvalidFileDescriptorEntity @@ -109,3 +109,79 @@ def __perform_test__(profile_name: str, expected_profiles: int): __perform_test__("b", 2) # Test the inheritance mode with 3 profiles __perform_test__("c", 3) + + +def test_load_invalid_profile_no_override_enabled(fake_profiles_path: str): + """Test the loaded profiles from the validator context.""" + settings = { + "profiles_path": fake_profiles_path, + "profile_name": "invalid-duplicated-shapes", + "data_path": "/tmp/random_path", + "inherit_profiles": True, + "override_profiles": False + } + + settings = ValidationSettings(**settings) + assert settings.inherit_profiles, "The inheritance mode should be set to True" + assert not settings.override_profiles, "The override mode should be set to False" + + validator = Validator(settings) + # initialize the validation context + context = ValidationContext(validator, validator.validation_settings.to_dict()) + + with pytest.raises(DuplicateRequirementCheck): + # Load the profiles + profiles = context.profiles + logger.debug("The profiles: %r", profiles) + + +def test_load_invalid_profile_with_override_on_same_profile(fake_profiles_path: str): + """Test the loaded profiles from the validator context.""" + settings = { + "profiles_path": fake_profiles_path, + "profile_name": "invalid-duplicated-shapes", + "data_path": "/tmp/random_path", + "inherit_profiles": True, + "override_profiles": True + } + + settings = ValidationSettings(**settings) + assert settings.inherit_profiles, "The inheritance mode should be set to True" + assert settings.override_profiles, "The override mode should be set to `True`" + validator = Validator(settings) + # initialize the validation context + context = ValidationContext(validator, validator.validation_settings.to_dict()) + + with pytest.raises(DuplicateRequirementCheck): + # Load the profiles + profiles = context.profiles + logger.debug("The profiles: %r", profiles) + + +def test_load_valid_profile_with_override_on_inherited_profile(fake_profiles_path: str): + """Test the loaded profiles from the validator context.""" + settings = { + "profiles_path": fake_profiles_path, + "profile_name": "c-overridden", + "data_path": "/tmp/random_path", + "inherit_profiles": True, + "override_profiles": True + } + + settings = ValidationSettings(**settings) + assert settings.inherit_profiles, "The inheritance mode should be set to True" + assert settings.override_profiles, "The override mode should be set to `True`" + validator = Validator(settings) + # initialize the validation context + context = ValidationContext(validator, validator.validation_settings.to_dict()) + + # Load the profiles + profiles = context.profiles + logger.debug("The profiles: %r", profiles) + + # The number of profiles should be 2 + assert len(profiles) == 4, "The number of profiles should be 2" + + # the number of checks should be 2 + requirements_checks = [requirement for profile in profiles.values() for requirement in profile.requirements] + assert len(requirements_checks) == 4, "The number of requirements should be 2" From 65b36d82cbc62929f746a6b2c839fb27d10a8581 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 2 Jun 2024 10:38:41 +0200 Subject: [PATCH 448/902] feat(logging): :sparkles: enhance logging functionality --- rocrate_validator/cli/commands/profiles.py | 5 +- rocrate_validator/cli/commands/validate.py | 2 +- rocrate_validator/cli/main.py | 11 +- rocrate_validator/log.py | 206 ++++++++++++++++++ rocrate_validator/models.py | 5 +- .../ro-crate/must/0_file_descriptor_format.py | 2 +- .../should/2_root_data_entity_relative_uri.py | 2 +- .../requirements/python/__init__.py | 4 +- .../requirements/shacl/checks.py | 2 +- .../requirements/shacl/models.py | 2 +- .../requirements/shacl/requirements.py | 6 +- rocrate_validator/requirements/shacl/utils.py | 2 +- .../requirements/shacl/validator.py | 2 +- rocrate_validator/services.py | 3 +- rocrate_validator/utils.py | 3 +- tests/conftest.py | 5 +- 16 files changed, 236 insertions(+), 26 deletions(-) create mode 100644 rocrate_validator/log.py diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index d70f3e63..99945a04 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -1,14 +1,15 @@ -import logging from pathlib import Path from rich.markdown import Markdown from rich.table import Table +import rocrate_validator.log as logging from rocrate_validator import services from rocrate_validator.cli.main import cli, click from rocrate_validator.colors import get_severity_color from rocrate_validator.constants import DEFAULT_PROFILE_NAME -from rocrate_validator.models import LevelCollection, Requirement, RequirementLevel +from rocrate_validator.models import (LevelCollection, Requirement, + RequirementLevel) from rocrate_validator.utils import get_profiles_path # set the default profiles path diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 70852092..2f63dffd 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -1,4 +1,3 @@ -import logging import os import sys from pathlib import Path @@ -8,6 +7,7 @@ from rich.console import Console from rocrate_validator.constants import DEFAULT_PROFILE_NAME +import rocrate_validator.log as logging from ... import services from ...colors import get_severity_color diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index b74f29e2..c624a0a1 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -1,10 +1,10 @@ -import logging + import sys import rich_click as click from rich.console import Console -from rocrate_validator.config import configure_logging +import rocrate_validator.log as logging from rocrate_validator.errors import ProfilesDirectoryNotFound from rocrate_validator.utils import get_version @@ -41,7 +41,6 @@ def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool): console = Console(no_color=disable_color) # pass the console to subcommands through the click context, after configuration ctx.obj['console'] = console - try: # If the version flag is set, print the version and exit if version: @@ -49,15 +48,13 @@ def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool): f"[bold]rocrate-validator [cyan]{get_version()}[/cyan][/bold]") sys.exit(0) # Set the log level - if debug: - configure_logging(level=logging.DEBUG) - else: - configure_logging(level=logging.WARNING) + logging.basicConfig(level=logging.DEBUG if debug else logging.WARNING) # If no subcommand is provided, invoke the default command if ctx.invoked_subcommand is None: # If no subcommand is provided, invoke the default command from .commands.validate import validate ctx.invoke(validate) + except ProfilesDirectoryNotFound as e: error_message = f""" The profile folder could not be located at the specified path: [red]{e.profiles_path}[/red]. diff --git a/rocrate_validator/log.py b/rocrate_validator/log.py new file mode 100644 index 00000000..aac914c1 --- /dev/null +++ b/rocrate_validator/log.py @@ -0,0 +1,206 @@ +import sys +import threading +from logging import (CRITICAL, DEBUG, ERROR, INFO, WARNING, Logger, + StreamHandler) +from typing import Optional + +import colorlog + +# set the module to the current module +__module__ = sys.modules[__name__] + + +def get_log_format(level: int): + """Get the log format based on the log level""" + log_format = '[%(log_color)s%(asctime)s%(reset)s] %(levelname)s in %(yellow)s%(module)s%(reset)s: '\ + '%(light_white)s%(message)s%(reset)s' + if level == DEBUG: + log_format = '%(log_color)s%(levelname)s%(reset)s:%(yellow)s%(name)s:%(module)s::%(funcName)s%(reset)s '\ + '@ %(light_green)sline: %(lineno)s%(reset)s - %(light_black)s%(message)s%(reset)s' + return log_format + + +DEFAULT_SETTINGS = { + 'enabled': True, + 'level': WARNING, + 'format': get_log_format(WARNING) +} + + +# _lock is used to serialize access to shared data structures in this module. +# This needs to be an RLock because fileConfig() creates and configures +# Handlers, and so might arbitrary user threads. Since Handler code updates the +# shared dictionary _handlers, it needs to acquire the lock. But if configuring, +# the lock would already have been acquired - so we need an RLock. +# The same argument applies to Loggers and Manager.loggerDict. +# +_lock = threading.RLock() + + +def _acquireLock(): + """ + Acquire the module-level lock for serializing access to shared data. + + This should be released with _releaseLock(). + """ + if _lock: + _lock.acquire() + + +def _releaseLock(): + """ + Release the module-level lock acquired by calling _acquireLock(). + """ + if _lock: + _lock.release() + + +# reference to the list of create loggers +__loggers__ = {} + +# user settings for the loggers +__settings__ = DEFAULT_SETTINGS.copy() + + +def __setup_logger__(logger: Logger): + + # prevent the logger from propagating the log messages to the root logger + logger.propagate = False + + # get the settings for the logger + settings = __settings__.get(logger.name, __settings__) + + # parse the log level + level = settings.get('level', __settings__['level']) + if not isinstance(level, int): + level = getattr(__module__, settings['level'].upper(), None) + + # set the log format + log_format = colorlog.ColoredFormatter(get_log_format(level)) + + # set the log level + logger.setLevel(level) + if not logger.hasHandlers(): + # create a console handler + ch = StreamHandler() + ch.setLevel(level) + ch.setFormatter(log_format) + logger.addHandler(ch) + + # enable/disable the logger + if settings.get('enabled', __settings__['enabled']): + logger.disabled = False + else: + logger.disabled = True + + +def __create_logger__(name: str) -> Logger: + logger: Logger = None + if not isinstance(name, str): + raise TypeError('A logger name must be a string') + _acquireLock() + try: + if name not in __loggers__: + logger = colorlog.getLogger(name) + __setup_logger__(logger) + __loggers__[name] = logger + finally: + _releaseLock() + return logger + + +def basicConfig(level: int, modules_config: Optional[dict] = None): + """Set the log level and format for the logger""" + _acquireLock() + try: + if not isinstance(level, int): + level = getattr(__module__, level.upper(), None) + + # set the default log level and format + __settings__['level'] = level + __settings__['format'] = get_log_format(level) + + # set the log level for the modules + if modules_config: + __settings__.update(modules_config) + + # initialize the logging module + colorlog.basicConfig( + level=__settings__['level'], + format=__settings__['format'], + log_colors={ + 'DEBUG': 'cyan', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'red,bg_white', + }, + ) + + # reconfigure existing loggers + for logger in __loggers__.values(): + __setup_logger__(logger) + + finally: + _releaseLock() + + +def getLogger(name: str) -> Logger: + return LoggerProxy(name) + + +class LoggerProxy: + + """"Define a proxy class for the logger to allow lazy initialization of the logger instance""" + + def __init__(self, name: str): + self.name = name + self._instance = None + + def _initialize(self): + _acquireLock() + try: + if self._instance is None: + self._instance = __create_logger__(self.name) + finally: + _releaseLock() + + def __getattr__(self, name): + self._initialize() + return getattr(self._instance, name) + + +__export__ = [get_log_format, DEFAULT_SETTINGS, Logger, + CRITICAL, DEBUG, ERROR, INFO, WARNING, StreamHandler, Optional] + + +# Example of usage +# if __name__ == '__main__': +# log_config = { +# 'module1': {'enabled': True, 'level': 'DEBUG'}, +# 'module2': {'enabled': False, 'level': 'INFO'}, +# 'module3': {'enabled': True, 'level': 'ERROR'}, +# } +# mgt = LoggerManager(log_config) +# logger1 = mgt.getLogger('module1') +# logger2 = mgt.getLogger('module2') +# logger3 = mgt.getLogger('module3') +# logger4 = mgt.getLogger('module4') + +# logger1.debug('This is a debug message') +# logger1.info('This is an info message') +# logger1.error('This is an error message') + +# logger2.debug('This is a debug message') +# logger2.info('This is an info message') +# logger2.error('This is an error message') + +# logger3.debug('This is a debug message') +# logger3.info('This is an info message') +# logger3.error('This is an error message') +# logger3.critical('This is a critical message') + +# logger4.debug('This is a debug message') +# logger4.info('This is an info message') +# logger4.error('This is an error message') +# logger4.critical('This is a critical message') diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index ef946e4e..21849989 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -3,7 +3,6 @@ import bisect import enum import inspect -import logging from abc import ABC, abstractmethod from collections import OrderedDict from collections.abc import Collection @@ -14,6 +13,7 @@ from rdflib import Graph +import rocrate_validator.log as logging from rocrate_validator.constants import (DEFAULT_ONTOLOGY_FILE, DEFAULT_PROFILE_NAME, DEFAULT_PROFILE_README_FILE, @@ -22,7 +22,8 @@ RDF_SERIALIZATION_FORMATS_TYPES, ROCRATE_METADATA_FILE, VALID_INFERENCE_OPTIONS_TYPES) -from rocrate_validator.errors import DuplicateRequirementCheck, InvalidProfilePath, ProfileNotFound +from rocrate_validator.errors import (DuplicateRequirementCheck, + InvalidProfilePath, ProfileNotFound) from rocrate_validator.utils import (get_profiles_path, get_requirement_name_from_file) diff --git a/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py index 2fba5acb..2fc4fd28 100644 --- a/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py @@ -1,7 +1,7 @@ import json -import logging from typing import Optional +import rocrate_validator.log as logging from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import (PyFunctionCheck, check, requirement) diff --git a/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py b/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py index 35544903..a083d0ba 100644 --- a/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py +++ b/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py @@ -1,7 +1,7 @@ import json -import logging from typing import Optional +import rocrate_validator.log as logging from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import (PyFunctionCheck, check, requirement) diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index 1f72c831..6a645686 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -1,9 +1,11 @@ import inspect -import logging + import re from pathlib import Path from typing import Callable, Optional, Type +import rocrate_validator.log as logging + from ...models import (Profile, Requirement, RequirementCheck, RequirementLevel, RequirementLoader, ValidationContext) from ...utils import get_classes_from_file diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index caff2782..83d065fa 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -1,6 +1,6 @@ -import logging from typing import Optional +import rocrate_validator.log as logging from rocrate_validator.models import (Requirement, RequirementCheck, ValidationContext) from rocrate_validator.requirements.shacl.models import Shape diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index c94b8127..5a4dacc1 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -1,6 +1,5 @@ from __future__ import annotations -import logging from pathlib import Path from typing import Optional, Union @@ -8,6 +7,7 @@ from rdflib.term import Node from rocrate_validator.constants import SHACL_NS +import rocrate_validator.log as logging from rocrate_validator.models import LevelCollection, RequirementLevel from rocrate_validator.requirements.shacl.utils import (ShapesList, compute_hash, diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index de8fbe3e..e58af9bb 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -1,7 +1,8 @@ -import logging from pathlib import Path from typing import Optional +import rocrate_validator.log as logging + from ...models import (Profile, Requirement, RequirementCheck, RequirementLevel, RequirementLoader) from .checks import SHACLCheck @@ -40,7 +41,8 @@ def __init_checks__(self) -> list[RequirementCheck]: checks.append(property_check) # if no property checks, add a generic one - if len(checks) == 0: + 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 diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index 6659ff3f..26d6d75b 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -1,7 +1,6 @@ from __future__ import annotations import hashlib -import logging from pathlib import Path from typing import Union @@ -10,6 +9,7 @@ from rocrate_validator.constants import RDF_SYNTAX_NS, SHACL_NS from rocrate_validator.errors import BadSyntaxError +import rocrate_validator.log as logging from rocrate_validator.models import Severity # set up logging diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index c32566ad..8aedc602 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -1,6 +1,5 @@ from __future__ import annotations -import logging import os from pathlib import Path from typing import Optional, Union @@ -10,6 +9,7 @@ from rdflib import Graph from rdflib.term import Node, URIRef +import rocrate_validator.log as logging from rocrate_validator.models import (Profile, RequirementCheck, Severity, ValidationContext, ValidationResult) from rocrate_validator.requirements.shacl.utils import (make_uris_relative, diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index a609ac74..f164e435 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -1,7 +1,8 @@ -import logging from pathlib import Path from typing import Union +import rocrate_validator.log as logging + from .models import (Profile, Severity, ValidationResult, ValidationSettings, Validator) from .utils import get_profiles_path diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index cb07c1f1..dc8794c0 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -1,5 +1,4 @@ import inspect -import logging import os import re import sys @@ -10,6 +9,8 @@ import toml from rdflib import Graph +import rocrate_validator.log as logging + from . import constants, errors # current directory diff --git a/tests/conftest.py b/tests/conftest.py index 127666ab..e191284f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,14 +1,13 @@ # calculate the absolute path of the rocrate-validator package # and add it to the system path -import logging import os from pytest import fixture -from rocrate_validator.config import configure_logging +import rocrate_validator.log as logging # set up logging -configure_logging(level=logging.DEBUG) +logging.basicConfig(level="warning", modules_config={"rocrate_validator.models": {"level": logging.DEBUG}}) CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) From 7adf078b6863a46895bddbd5c933d8ccc97bfbdc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 2 Jun 2024 10:57:01 +0200 Subject: [PATCH 449/902] perf(logging): :zap: optimize logger initialisation: one handler per level --- rocrate_validator/log.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/log.py b/rocrate_validator/log.py index aac914c1..781b0488 100644 --- a/rocrate_validator/log.py +++ b/rocrate_validator/log.py @@ -61,6 +61,9 @@ def _releaseLock(): # user settings for the loggers __settings__ = DEFAULT_SETTINGS.copy() +# store logger handlers (only one handler per logger) +__handlers__ = {} + def __setup_logger__(logger: Logger): @@ -75,16 +78,16 @@ def __setup_logger__(logger: Logger): if not isinstance(level, int): level = getattr(__module__, settings['level'].upper(), None) - # set the log format - log_format = colorlog.ColoredFormatter(get_log_format(level)) - # set the log level logger.setLevel(level) - if not logger.hasHandlers(): - # create a console handler + + # configure the logger handler + ch = __handlers__.get(logger.name, None) + if not ch: ch = StreamHandler() ch.setLevel(level) - ch.setFormatter(log_format) + ch.setFormatter(colorlog.ColoredFormatter(get_log_format(level))) + logger.addHandler(ch) # enable/disable the logger From 81dfbba383ad0b105b2f35dada198f47bfba1dad Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Sun, 2 Jun 2024 10:58:31 +0200 Subject: [PATCH 450/902] chore(logging): :wrench: update default configuration of logging in the test env --- tests/conftest.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index e191284f..6e4e72a0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,12 @@ import rocrate_validator.log as logging # set up logging -logging.basicConfig(level="warning", modules_config={"rocrate_validator.models": {"level": logging.DEBUG}}) +logging.basicConfig( + level="warning", + modules_config={ + # "rocrate_validator.models": {"level": logging.DEBUG} + } +) CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) From ed16b5df4a1ba5f3a8e86b16d9d126c6b85e6fd3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 3 Jun 2024 09:15:29 +0200 Subject: [PATCH 451/902] test(profiles): :recycle: move integration tests --- .../profiles/ro-crate/{must => }/test_file_descriptor_entity.py | 0 .../profiles/ro-crate/{must => }/test_file_descriptor_format.py | 0 .../profiles/ro-crate/{must => }/test_root_data_entity.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename tests/integration/profiles/ro-crate/{must => }/test_file_descriptor_entity.py (100%) rename tests/integration/profiles/ro-crate/{must => }/test_file_descriptor_format.py (100%) rename tests/integration/profiles/ro-crate/{must => }/test_root_data_entity.py (100%) diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/test_file_descriptor_entity.py similarity index 100% rename from tests/integration/profiles/ro-crate/must/test_file_descriptor_entity.py rename to tests/integration/profiles/ro-crate/test_file_descriptor_entity.py diff --git a/tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py b/tests/integration/profiles/ro-crate/test_file_descriptor_format.py similarity index 100% rename from tests/integration/profiles/ro-crate/must/test_file_descriptor_format.py rename to tests/integration/profiles/ro-crate/test_file_descriptor_format.py diff --git a/tests/integration/profiles/ro-crate/must/test_root_data_entity.py b/tests/integration/profiles/ro-crate/test_root_data_entity.py similarity index 100% rename from tests/integration/profiles/ro-crate/must/test_root_data_entity.py rename to tests/integration/profiles/ro-crate/test_root_data_entity.py From 5fc73dae5a778364ad3adb8d35e16ace1141b0d4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 3 Jun 2024 16:28:18 +0200 Subject: [PATCH 452/902] feat(profiles/ro-crate): :sparkles: add rule to identity the file descriptor --- .../must/1_file-descriptor_metadata.ttl | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index 27121745..c44e4c39 100644 --- a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -5,8 +5,33 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . +@prefix rocrate: . +rocrate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape, sh:hidden; + 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.1/appendix/relative-uris.html#finding-ro-crate-root-in-rdf-triple-stores).""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:CreativeWork ; + 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 rocrate:ROCrateMetadataFileDescriptor ; + ] . ro:ROCrateMetadataFileDescriptorExistence a sh:NodeShape ; @@ -19,6 +44,7 @@ ro:ROCrateMetadataFileDescriptorExistence sh:description """Check if the RO-Crate Metadata File Descriptor entity exists, i.e., if there exists an entity with @id `ro-crate-metadata.json` and type `schema:CreativeWork`""" ; sh:path rdf:type ; + sh:hasValue rocrate:ROCrateMetadataFileDescriptor ; sh:minCount 1 ; sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; ] . From e065d81b88f1f963c5618c8f5e088f31b3b4a44f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 3 Jun 2024 16:33:07 +0200 Subject: [PATCH 453/902] feat(profiles/ro-crate): :sparkles: representation of the hasPart relationship bw Root and DataEntities --- .../must/2_root_data_entity_metadata.ttl | 17 ++++++++ .../ro-crate/must/4_data_entity_metadata.ttl | 41 +++++++++++++------ .../ro-crate/should/4_data_entity.ttl | 29 ------------- .../should/4_data_entity_metadata.ttl | 12 ++++++ 4 files changed, 57 insertions(+), 42 deletions(-) delete mode 100644 rocrate_validator/profiles/ro-crate/should/4_data_entity.ttl create mode 100644 rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl 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 8e01c3a7..2acf674c 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 @@ -77,3 +77,20 @@ ro:RootDataEntityValueRestriction sh:message """The Root Data Entity URI MUST end with `/`""" ; sh:pattern "/$" ; ] . + +ro:RootDataEntityHasPartValueRestriction + a sh:NodeShape ; + sh:name "RO-Crate Root Data Entity: `hasPart` value restriction" ; + sh:description "The Root Data Entity MUST be linked to the declared `File` and `Foldeds` instances through the `hasPart` property" ; + sh:targetClass rocrate:RootDataEntity ; + sh:property [ + a sh:PropertyShape ; + sh:name "RO-Crate Root Data Entity: `hasPart` value restriction" ; + sh:description "Check if Root Data Entity is be linked to the declared `File` and `Foldeds` instances through the `hasPart` property" ; + sh:path schema_org:hasPart ; + sh:or ( + [ sh:class rocrate:File ] + [ sh:class rocrate:Directory ] + ) ; + # sh:message """The Root Data Entity MUST be linked to either File or Directory instances, nothing else""" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl index 0279229b..39a933d9 100644 --- a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl @@ -115,20 +115,35 @@ ro:DirectoryDataEntity a sh:NodeShape ; sh:severity sh:Violation ; ] . -ro:DirectoryDataEntityRequiredValueRestriction a sh:NodeShape ; - sh:name "Directory Data Entity: REQUIRED value restriction" ; - sh:description """A Directory Data Entity MUST end with `/`""" ; - sh:targetNode rocrate:Directory ; +ro:DataEntityRquiredPropertiesShape a sh:NodeShape ; + sh:name "Data Entity: REQUIRED properties" ; + sh:description """A `DataEntity` MUST be linked, either directly or inderectly, from the Root Data Entity""" ; + sh:targetClass rocrate:DataEntity ; + sh:property + [ + a sh:PropertyShape ; + sh:path [ sh:inversePath schema_org:hasPart ] ; + sh:node schema_org:Dataset ; + sh:minCount 1 ; + sh:name "Data Entity MUST be directly referenced" ; + sh:description """Check if the Data Entity is linked, either directly of inderectly, 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" ; + ] . - sh:property [ - a sh:PropertyShape ; - sh:name "Directory Data Entity: REQUIRED value restriction" ; - sh:description """Check if the Directory Data Entity ends with `/`""" ; - sh:path [ sh:inversePath rdf:type ] ; - sh:minCount 1 ; - sh:message """The Directory Data Entity URI MUST end with `/`""" ; - sh:pattern "/$" ; - ] . +# ro:DirectoryDataEntityRequiredValueRestriction a sh:NodeShape ; +# sh:name "Directory Data Entity: REQUIRED value restriction" ; +# sh:description """A Directory Data Entity MUST end with `/`""" ; +# sh:targetNode rocrate:Directory ; + +# sh:property [ +# a sh:PropertyShape ; +# sh:name "Directory Data Entity: REQUIRED value restriction" ; +# sh:description """Check if the Directory Data Entity ends with `/`""" ; +# sh:path [ sh:inversePath rdf:type ] ; +# sh:minCount 1 ; +# sh:message """The Directory Data Entity URI MUST end with `/`""" ; +# sh:pattern "/$" ; +# ] . # Uncomment for debugging # ro:testDirectory a sh:NodeShape ; diff --git a/rocrate_validator/profiles/ro-crate/should/4_data_entity.ttl b/rocrate_validator/profiles/ro-crate/should/4_data_entity.ttl deleted file mode 100644 index 0d3e6830..00000000 --- a/rocrate_validator/profiles/ro-crate/should/4_data_entity.ttl +++ /dev/null @@ -1,29 +0,0 @@ -@prefix ro: <./> . -@prefix dct: . -@prefix rdf: . -@prefix schema_org: . -@prefix sh: . -@prefix xml1: . -@prefix xsd: . -@prefix owl: . -@prefix rdfs: . -@prefix rocrate: . - - - -ro:DataEntityRecommendedPropertiesShape a sh:NodeShape ; - sh:name "DataEntity recommended properties" ; - sh:description """A DataEntity is a digital object that is - stored in a file format""" ; - sh:targetClass rocrate:DataEntity ; - # check inverse path of hasPart relation - sh:property [ - a sh:PropertyShape ; - sh:path [ sh:inversePath schema_org:hasPart ] ; - sh:node rocrate:RootDataEntity ; - sh:minCount 1 ; - sh:name "DataEntity - RootDataEntity reference" ; - sh:description "DataEntity instances should be linked to a RootDataEntity through the schema_org:hasPart property" ; - # sh:group ro:NameGroup ; - ] . - diff --git a/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl new file mode 100644 index 00000000..92c4f715 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl @@ -0,0 +1,12 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix owl: . +@prefix rdfs: . +@prefix rocrate: . + + From c1c840f12897c8b0c548f051c9d435161ee11d7c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 3 Jun 2024 16:38:21 +0200 Subject: [PATCH 454/902] test(profiles/ro-crate): :white_check_mark: test hasPart relationship bw root and data entities --- .../ro-crate-metadata.json | 119 +++++++++++++++++ .../ro-crate-metadata.json | 114 +++++++++++++++++ .../ro-crate-metadata.json | 110 ++++++++++++++++ .../ro-crate-metadata.json | 120 ++++++++++++++++++ .../ro-crate/test_data_entity_format.py | 41 ++++++ .../ro-crate/test_root_data_entity.py | 13 +- tests/ro_crates.py | 21 +++ 7 files changed, 536 insertions(+), 2 deletions(-) create mode 100644 tests/data/crates/invalid/2_root_data_entity_metadata/invalid_referenced_data_entities/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/4_data_entity_metadata/invalid_missing_hasPart_reference/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/4_data_entity_metadata/valid_direct_hasPart_reference/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/4_data_entity_metadata/valid_indirect_hasPart_reference/ro-crate-metadata.json create mode 100644 tests/integration/profiles/ro-crate/test_data_entity_format.py diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_referenced_data_entities/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_referenced_data_entities/ro-crate-metadata.json new file mode 100644 index 00000000..7175c5a0 --- /dev/null +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_referenced_data_entities/ro-crate-metadata.json @@ -0,0 +1,119 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-04-17T13:39:44+00:00", + "name": "Invalid RO Crate: referenced entities should be files of directories", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "foo/" + }, + { + "@id": "README.md" + } + ], + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "license": "https://spdx.org/licenses/Apache-2.0.html" + }, + { + "@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"], + "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": "foo/", + "@type": "InvalidEntity", + "hasPart": [ + { + "@id": "foo/xxx" + } + ] + }, + { + "@id": "foo/xxx", + "@type": "File" + }, + { + "@id": "blank.png", + "@type": ["File", "ImageObject"] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + } + ] +} diff --git a/tests/data/crates/invalid/4_data_entity_metadata/invalid_missing_hasPart_reference/ro-crate-metadata.json b/tests/data/crates/invalid/4_data_entity_metadata/invalid_missing_hasPart_reference/ro-crate-metadata.json new file mode 100644 index 00000000..16bb00f9 --- /dev/null +++ b/tests/data/crates/invalid/4_data_entity_metadata/invalid_missing_hasPart_reference/ro-crate-metadata.json @@ -0,0 +1,114 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-04-17T13:39:44+00:00", + "name": "Missing direct or indirect references on the Root Data Entity", + "description": "sort-and-change-case.ga and foo/xxx are not referenced by the Root Data Entity", + "hasPart": [ + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "foo/" + }, + { + "@id": "README.md" + } + ], + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "license": "https://spdx.org/licenses/Apache-2.0.html" + }, + { + "@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"], + "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": "foo/", + "@type": "Dataset" + + }, + { + "@id": "foo/xxx", + "@type": "File" + + }, + { + "@id": "blank.png", + "@type": ["File", "ImageObject"] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + } + ] +} diff --git a/tests/data/crates/invalid/4_data_entity_metadata/valid_direct_hasPart_reference/ro-crate-metadata.json b/tests/data/crates/invalid/4_data_entity_metadata/valid_direct_hasPart_reference/ro-crate-metadata.json new file mode 100644 index 00000000..0c36dfff --- /dev/null +++ b/tests/data/crates/invalid/4_data_entity_metadata/valid_direct_hasPart_reference/ro-crate-metadata.json @@ -0,0 +1,110 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-04-17T13:39:44+00:00", + "name": "Valid RO Crate with foo/ Dataset directly referenced", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "foo/" + }, + { + "@id": "README.md" + } + ], + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "license": "https://spdx.org/licenses/Apache-2.0.html" + }, + { + "@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"], + "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": "foo/", + "@type": "Dataset" + }, + { + "@id": "blank.png", + "@type": ["File", "ImageObject"] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + } + ] +} diff --git a/tests/data/crates/invalid/4_data_entity_metadata/valid_indirect_hasPart_reference/ro-crate-metadata.json b/tests/data/crates/invalid/4_data_entity_metadata/valid_indirect_hasPart_reference/ro-crate-metadata.json new file mode 100644 index 00000000..9f1fa13d --- /dev/null +++ b/tests/data/crates/invalid/4_data_entity_metadata/valid_indirect_hasPart_reference/ro-crate-metadata.json @@ -0,0 +1,120 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-04-17T13:39:44+00:00", + "name": "Valid RO Crate with foo/xxx indirectly referenced", + "description": "This RO Crate contains a file foo/xxx that is not directly referenced in the metadata, but is included in a subdirectory of a Dataset and it is referenced by the `hasPart` property of that Dataset.", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "foo/" + }, + { + "@id": "README.md" + } + ], + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "license": "https://spdx.org/licenses/Apache-2.0.html" + }, + { + "@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"], + "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": "foo/", + "@type": "Dataset", + "hasPart": [ + { + "@id": "foo/xxx" + } + ] + }, + { + "@id": "foo/xxx", + "@type": "File" + }, + { + "@id": "blank.png", + "@type": ["File", "ImageObject"] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + } + ] +} diff --git a/tests/integration/profiles/ro-crate/test_data_entity_format.py b/tests/integration/profiles/ro-crate/test_data_entity_format.py new file mode 100644 index 00000000..05a96632 --- /dev/null +++ b/tests/integration/profiles/ro-crate/test_data_entity_format.py @@ -0,0 +1,41 @@ +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_missing_data_entity_reference(): + """Test a RO-Crate without a root data entity.""" + do_entity_test( + paths.missing_hasPart_data_entity_reference, + models.Severity.REQUIRED, + False, + ["Data Entity: REQUIRED properties"], + ["sort-and-change-case.ga", "foo/xxx"] + ) + + +def test_data_entity_must_be_directly_linked(): + """Test a RO-Crate without a root data entity.""" + do_entity_test( + paths.direct_hasPart_data_entity_reference, + models.Severity.REQUIRED, + True + ) + + +def test_data_entity_must_be_indirectly_linked(): + """Test a RO-Crate without a root data entity.""" + do_entity_test( + paths.indirect_hasPart_data_entity_reference, + models.Severity.REQUIRED, + True + ) 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 392acad9..71c9786e 100644 --- a/tests/integration/profiles/ro-crate/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/test_root_data_entity.py @@ -1,7 +1,5 @@ import logging -import pytest - from rocrate_validator import models from tests.ro_crates import InvalidRootDataEntity from tests.shared import do_entity_test @@ -80,6 +78,17 @@ def test_missing_root_description(): ) +def test_invalid_referenced_data_entities(): + """Test a RO-Crate with invalid referenced data entities.""" + do_entity_test( + paths.invalid_referenced_data_entities, + models.Severity.REQUIRED, + False, + ["RO-Crate Root Data Entity: `hasPart` value restriction"], + ["Node <./foo/> does not conform to one or more shapes"] + ) + + def test_missing_root_license(): """Test a RO-Crate without a root data entity license.""" do_entity_test( diff --git a/tests/ro_crates.py b/tests/ro_crates.py index fb6f57a2..c7c999b6 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -90,6 +90,10 @@ def missing_root_license_name(self) -> Path: def missing_root_license_description(self) -> Path: return self.base_path / "missing_root_license_description" + @property + def invalid_referenced_data_entities(self) -> Path: + return self.base_path / "invalid_referenced_data_entities" + class InvalidFileDescriptorEntity: @@ -122,3 +126,20 @@ def missing_conforms_to(self) -> Path: @property def invalid_conforms_to(self) -> Path: return self.base_path / "invalid_conforms_to" + + +class InvalidDataEntity: + + base_path = INVALID_CRATES_DATA_PATH / "4_data_entity_metadata" + + @property + def missing_hasPart_data_entity_reference(self) -> Path: + return self.base_path / "invalid_missing_hasPart_reference" + + @property + def direct_hasPart_data_entity_reference(self) -> Path: + return self.base_path / "valid_direct_hasPart_reference" + + @property + def indirect_hasPart_data_entity_reference(self) -> Path: + return self.base_path / "valid_indirect_hasPart_reference" From 79067b4eb06477e520ed0454e4706d0a9000bf89 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 3 Jun 2024 16:48:24 +0200 Subject: [PATCH 455/902] feat(profiles/ro-crate): :sparkles: add shape to validate the trailing `/` of Directory Data Entities --- .../ro-crate/must/4_data_entity_metadata.ttl | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl index 39a933d9..c6d9996b 100644 --- a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl @@ -130,20 +130,19 @@ ro:DataEntityRquiredPropertiesShape a sh:NodeShape ; # sh:message "A Data Entity MUST be directly or indirectly linked to the `Root Data Entity` through the `hasPart` property" ; ] . -# ro:DirectoryDataEntityRequiredValueRestriction a sh:NodeShape ; -# sh:name "Directory Data Entity: REQUIRED value restriction" ; -# sh:description """A Directory Data Entity MUST end with `/`""" ; -# sh:targetNode rocrate:Directory ; - -# sh:property [ -# a sh:PropertyShape ; -# sh:name "Directory Data Entity: REQUIRED value restriction" ; -# sh:description """Check if the Directory Data Entity ends with `/`""" ; -# sh:path [ sh:inversePath rdf:type ] ; -# sh:minCount 1 ; -# sh:message """The Directory Data Entity URI MUST end with `/`""" ; -# sh:pattern "/$" ; -# ] . +ro:DirectoryDataEntityRequiredValueRestriction a sh:NodeShape ; + sh:name "Directory Data Entity: REQUIRED value restriction" ; + sh:description """A Directory Data Entity MUST end with `/`""" ; + sh:targetClass rocrate:Directory ; + sh:property [ + a sh:PropertyShape ; + sh:name "Directory Data Entity: REQUIRED value restriction" ; + sh:description """Check if the Directory Data Entity ends with `/`""" ; + sh:path [ sh:inversePath rdf:type ] ; + sh:minCount 1 ; + # sh:message """The Directory Data Entity URI MUST end with `/`""" ; + sh:pattern "/$" ; + ] . # Uncomment for debugging # ro:testDirectory a sh:NodeShape ; From b9ffd6427dc800f775448d7dc708a13fc0e22c49 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 3 Jun 2024 18:30:08 +0200 Subject: [PATCH 456/902] refactor(profiles/ro-crate): :bulb: update comments --- .../profiles/ro-crate/must/4_data_entity_metadata.ttl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl index c6d9996b..9a845591 100644 --- a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl @@ -106,7 +106,8 @@ ro:DirectoryDataEntity a sh:NodeShape ; sh:predicate rdf:type ; sh:object rocrate: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`.""" ; From e621593acce22eebbd3cc31eab7c64846ca8da24 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 3 Jun 2024 18:36:00 +0200 Subject: [PATCH 457/902] fix(profiles/ro-crate): :ambulance: fix shape target --- .../profiles/ro-crate/must/4_data_entity_metadata.ttl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl index 9a845591..8c355ad4 100644 --- a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl @@ -134,14 +134,13 @@ ro:DataEntityRquiredPropertiesShape a sh:NodeShape ; ro:DirectoryDataEntityRequiredValueRestriction a sh:NodeShape ; sh:name "Directory Data Entity: REQUIRED value restriction" ; sh:description """A Directory Data Entity MUST end with `/`""" ; - sh:targetClass rocrate:Directory ; + sh:targetNode rocrate:Directory ; sh:property [ a sh:PropertyShape ; sh:name "Directory Data Entity: REQUIRED value restriction" ; sh:description """Check if the Directory Data Entity ends with `/`""" ; sh:path [ sh:inversePath rdf:type ] ; - sh:minCount 1 ; - # sh:message """The Directory Data Entity URI MUST end with `/`""" ; + sh:message """Every Data Entity Directory URI MUST end with `/`""" ; sh:pattern "/$" ; ] . From d2dc6d3089ce84439dbd73dcb6e268a732dcd144 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 3 Jun 2024 18:37:15 +0200 Subject: [PATCH 458/902] test(profiles/ro-crate): :white_check_mark: test trailing slash of directory data entities --- .../ro-crate-metadata.json | 124 ++++++++++++++++++ .../ro-crate/test_data_entity_format.py | 11 ++ tests/ro_crates.py | 4 + 3 files changed, 139 insertions(+) create mode 100644 tests/data/crates/invalid/4_data_entity_metadata/directory_data_entity_wo_trailing_slash/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/4_data_entity_metadata/directory_data_entity_wo_trailing_slash/ro-crate-metadata.json b/tests/data/crates/invalid/4_data_entity_metadata/directory_data_entity_wo_trailing_slash/ro-crate-metadata.json new file mode 100644 index 00000000..7662be93 --- /dev/null +++ b/tests/data/crates/invalid/4_data_entity_metadata/directory_data_entity_wo_trailing_slash/ro-crate-metadata.json @@ -0,0 +1,124 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-04-17T13:39:44+00:00", + "name": "Valid RO Crate with foo/xxx indirectly referenced", + "description": "This RO Crate contains a foo Dataset (directory) which doesn't have a trailing slash in its @id", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "foo" + }, + { + "@id": "README.md" + } + ], + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "license": "https://spdx.org/licenses/Apache-2.0.html" + }, + { + "@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"], + "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": "foo", + "@type": "Dataset", + "hasPart": [ + { + "@id": "foo/xxx" + } + ] + }, + { + "@id": "foo/xxx", + "@type": "File" + }, + { + "@id": "bar", + "@type": "Dataset" + }, + { + "@id": "blank.png", + "@type": ["File", "ImageObject"] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + } + ] +} diff --git a/tests/integration/profiles/ro-crate/test_data_entity_format.py b/tests/integration/profiles/ro-crate/test_data_entity_format.py index 05a96632..1a978ea4 100644 --- a/tests/integration/profiles/ro-crate/test_data_entity_format.py +++ b/tests/integration/profiles/ro-crate/test_data_entity_format.py @@ -39,3 +39,14 @@ def test_data_entity_must_be_indirectly_linked(): models.Severity.REQUIRED, True ) + + +def test_directory_data_entity_wo_trailing_slash(): + """Test a RO-Crate without a root data entity.""" + do_entity_test( + paths.directory_data_entity_wo_trailing_slash, + models.Severity.REQUIRED, + False, + ["Directory Data Entity: REQUIRED value restriction"], + ["Every Data Entity Directory URI MUST end with `/`"] + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index c7c999b6..466d0cf7 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -143,3 +143,7 @@ def direct_hasPart_data_entity_reference(self) -> Path: @property def indirect_hasPart_data_entity_reference(self) -> Path: return self.base_path / "valid_indirect_hasPart_reference" + + @property + def directory_data_entity_wo_trailing_slash(self) -> Path: + return self.base_path / "directory_data_entity_wo_trailing_slash" From 3d05e6576451ae30bec7169b3c85ffb5379af796 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 4 Jun 2024 12:02:53 +0200 Subject: [PATCH 459/902] fix(shacl): :bug: value is not always present in a violationResult --- rocrate_validator/requirements/shacl/validator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 8aedc602..c631cbab 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -248,7 +248,6 @@ def resultPath(self): def value(self): if not self._value: self._value = self.graph.value(self._violation_node, URIRef(f"{SHACL_NS}value")) - assert self._value is not None, f"Unable to get value from violation node {self._violation_node}" return self._value def get_result_severity(self) -> Severity: From 7b01dce44f538523d3ea549a329061a1485f71b5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 4 Jun 2024 12:03:38 +0200 Subject: [PATCH 460/902] fix(shacl): :bug: wrong URI to reference the focusNode --- rocrate_validator/requirements/shacl/validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index c631cbab..f189c730 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -233,7 +233,7 @@ def graph(self) -> Graph: @property def focusNode(self) -> Node: if not self._focus_node: - self._focus_node = self.graph.value(self._violation_node, URIRef(f"{SHACL_NS}sourceShape")) + self._focus_node = self.graph.value(self._violation_node, URIRef(f"{SHACL_NS}focusNode")) assert self._focus_node is not None, f"Unable to get focus node from violation node {self._violation_node}" return self._focus_node From bf9580174593dfbeb3e4f578eef6e732529ded51 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 4 Jun 2024 12:10:45 +0200 Subject: [PATCH 461/902] feat(core): :sparkles: extend CheckIssue model to store focusNode and value --- rocrate_validator/models.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 21849989..5bfe4c89 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -625,12 +625,16 @@ class CheckIssue: # without having it provided through an additional argument. def __init__(self, severity: Severity, check: RequirementCheck, - message: Optional[str] = None): + message: Optional[str] = None, + focusNode: Optional[str] = None, + value: Optional[str] = None): if not isinstance(severity, Severity): raise TypeError(f"CheckIssue constructed with a severity '{severity}' of type {type(severity)}") self._severity = severity self._message = message self._check: RequirementCheck = check + self._focusNode = focusNode + self._value = value @property def message(self) -> Optional[str]: @@ -656,6 +660,14 @@ def check(self) -> RequirementCheck: """The check that generated the issue""" return self._check + @property + def focusNode(self) -> Optional[str]: + return self._focusNode + + @property + def value(self) -> Optional[str]: + return self._value + def __eq__(self, other: object) -> bool: return isinstance(other, CheckIssue) and \ self._check == other._check and \ @@ -742,9 +754,11 @@ def add_issue(self, issue: CheckIssue): def add_check_issue(self, message: str, check: RequirementCheck, - severity: Optional[Severity] = None) -> CheckIssue: + severity: Optional[Severity] = None, + focusNode: Optional[str] = None, + value: Optional[str] = None) -> CheckIssue: sev_value = severity if severity is not None else check.requirement.severity - c = CheckIssue(sev_value, check, message) + c = CheckIssue(sev_value, check, message, focusNode=focusNode, value=value) # self._issues.append(c) bisect.insort(self._issues, c) return c From 4b4627332255f8ca98c84afc295263eaaacf8080 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 4 Jun 2024 12:13:50 +0200 Subject: [PATCH 462/902] feat(shacl): :sparkles: set focusNode and value on issue instances --- rocrate_validator/requirements/shacl/checks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 83d065fa..d210f0c9 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -4,6 +4,7 @@ from rocrate_validator.models import (Requirement, RequirementCheck, ValidationContext) from rocrate_validator.requirements.shacl.models import Shape +from rocrate_validator.requirements.shacl.utils import make_uris_relative from .validator import (SHACLValidationAlreadyProcessed, SHACLValidationContext, SHACLValidationContextManager, @@ -93,7 +94,10 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): shacl_context.settings.get("target_only_validation", False): c = shacl_context.result.add_check_issue(message=violation.get_result_message(shacl_context.rocrate_path), check=requirementCheck, - severity=violation.get_result_severity()) + severity=violation.get_result_severity(), + focusNode=make_uris_relative( + violation.focusNode.toPython(), shacl_context.rocrate_path), + value=violation.value) logger.debug("Added validation issue to the context: %s", c) if shacl_context.base_context.fail_fast: break From 69b7b6a07f22c2c0e30bb17696ab90e6c124f468 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 4 Jun 2024 12:16:18 +0200 Subject: [PATCH 463/902] feat(cli): :sparkles: report the `focusNode` for every violation --- 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 2f63dffd..649a6a94 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -178,5 +178,5 @@ def __print_validation_result__( key=lambda x: (-x.severity.value, x)): console.print( f"{' '*6}- [[red]Violation[/red] of " - f"[{issue_color} bold]{issue.check.identifier}[/{issue_color} bold]]: {issue.message}") + f"[{issue_color} bold]{issue.check.identifier}[/{issue_color} bold] on [cyan]<{issue.focusNode}>[/cyan]]: {issue.message}") console.print("\n", style="white") From 2f041d4ef012e4d47c8dc9e5f15867487fde53b7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 4 Jun 2024 13:07:06 +0200 Subject: [PATCH 464/902] feat(cli): :sparkles: report violation value when available --- rocrate_validator/cli/commands/validate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 649a6a94..eaef7b9e 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -176,7 +176,8 @@ def __print_validation_result__( console.print(f"\n{' '*6}Detected issues:", style="white bold") for issue in sorted(result.get_issues_by_check(check), key=lambda x: (-x.severity.value, x)): + actual_value = f"value \"[green]{issue.value}[/green]\" of " if issue.value else "" console.print( f"{' '*6}- [[red]Violation[/red] of " - f"[{issue_color} bold]{issue.check.identifier}[/{issue_color} bold] on [cyan]<{issue.focusNode}>[/cyan]]: {issue.message}") + f"[{issue_color} bold]{issue.check.identifier}[/{issue_color} bold] on {actual_value}[cyan]<{issue.focusNode}>[/cyan]]: {issue.message}",) console.print("\n", style="white") From 824f7aaf34786a33bfbb406e6d3ac3629e41471d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 4 Jun 2024 16:59:36 +0200 Subject: [PATCH 465/902] test(profiles/ro-crate): :truck: rename data entity test file --- .../{test_data_entity_format.py => test_data_entity_metadata.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/integration/profiles/ro-crate/{test_data_entity_format.py => test_data_entity_metadata.py} (100%) diff --git a/tests/integration/profiles/ro-crate/test_data_entity_format.py b/tests/integration/profiles/ro-crate/test_data_entity_metadata.py similarity index 100% rename from tests/integration/profiles/ro-crate/test_data_entity_format.py rename to tests/integration/profiles/ro-crate/test_data_entity_metadata.py From d765e08aabb42282d1ee670567390b027e6486b8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 4 Jun 2024 18:21:23 +0200 Subject: [PATCH 466/902] feat(profiles/ro-crate): :sparkles: add shape to validate File Data Entity encodings --- .../should/4_data_entity_metadata.ttl | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl index 92c4f715..1f685b2b 100644 --- a/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl @@ -1,12 +1,44 @@ @prefix ro: <./> . -@prefix dct: . +@prefix rocrate: . @prefix rdf: . +@prefix rdfs: . +@prefix dct: . @prefix schema_org: . @prefix sh: . -@prefix xml1: . +@prefix xml: . @prefix xsd: . @prefix owl: . -@prefix rdfs: . -@prefix rocrate: . - +rocrate:FileRecommendedProperties a sh:NodeShape ; + sh:targetClass rocrate:File ; + sh:name "File Data Entity: RECOMMENDED properties"; + sh:description """A `File` Data Entity SHOULD have detailed descriptions encodings through the `encodingFormat` property""" ; + 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` to a `Contextual Entity` of type `WebSite` + (see [Adding detailed descriptions of encodings](https://www.researchobject.org/ro-crate/1.1/data-entities.html#adding-detailed-descriptions-of-encodings)). + """ ; + sh:message "Missing or invalid `encodingFormat` linked to the `File Data Entity`"; + sh:or ( + [ + sh:datatype xsd:string ; + sh:pattern "^[-a-zA-Z0-9]+/[-a-zA-Z0-9]+$" ; + sh:name "File Data Entity: RECOMMENDED `PRONOM` for the `encodingFormat` property" ; + sh:description "Check if the File Data Entity is linked to its `encodingFormat` through a PRONOM identifier (e.g., application/pdf)." ; + sh:message "The `encodingFormat` SHOULD be linked using a PRONOM identifier (e.g., application/pdf)."; + ] + [ + sh:nodeKind sh:IRI ; + sh:class schema_org:WebSite ; + sh:name "File Data Entity: RECOMMENDED `Contextual Entity` linked to the `encodingFormat` property"; + sh:description "Check if the File Data Entity `encodingFormat` is linked to a `Contextual Entity of type `WebSite`." ; + sh:message "The `encodingFormat` SHOULD be linked to a `Contextual Entity` of type `Web Site`." ; + ] + ) + ] . From 81dd738a3f330487e6607951c6cc0b80b1dc9587 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 4 Jun 2024 18:23:22 +0200 Subject: [PATCH 467/902] feat(profiles/ro-crate): :sparkles: define the Shape to valid the WebSite entity --- .../ro-crate/must/5_contextual_entity.ttl | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/profiles/ro-crate/must/5_contextual_entity.ttl b/rocrate_validator/profiles/ro-crate/must/5_contextual_entity.ttl index 6fa159a5..6b7b5656 100644 --- a/rocrate_validator/profiles/ro-crate/must/5_contextual_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/must/5_contextual_entity.ttl @@ -1,7 +1,7 @@ @prefix ro: <./> . @prefix dct: . @prefix rdf: . -@prefix schema_org: . +@prefix schema: . @prefix sh: . @prefix xml1: . @prefix xsd: . @@ -31,3 +31,24 @@ rocrate:FindLicenseEntity a sh:NodeShape, sh:hidden ; sh:predicate rdf:type ; sh:object rocrate:ContextualEntity ; ] . + + +rocrate: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" ; + ] . From f691dd252827d28d2196dfb7919e7ba618083ecb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 4 Jun 2024 18:24:43 +0200 Subject: [PATCH 468/902] test(profiles/ro-crate): :white_check_mark: add tests for the encodingFormat property --- .../ro-crate-metadata.json | 97 ++++++++++++++ .../ro-crate-metadata.json | 93 ++++++++++++++ .../ro-crate-metadata.json | 89 +++++++++++++ .../ro-crate-metadata.json | 120 ++++++++++++++++++ .../ro-crate-metadata.json | 98 ++++++++++++++ .../ro-crate-metadata.json | 89 +++++++++++++ .../ro-crate/test_data_entity_metadata.py | 62 +++++++++ tests/ro_crates.py | 24 ++++ 8 files changed, 672 insertions(+) create mode 100644 tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_ctx_entity_missing_ws_name/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_ctx_entity_missing_ws_type/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_pronom/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/4_data_entity_metadata/missing_encoding_format/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_ctx_entity/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_pronom/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_ctx_entity_missing_ws_name/ro-crate-metadata.json b/tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_ctx_entity_missing_ws_name/ro-crate-metadata.json new file mode 100644 index 00000000..cdf42c6c --- /dev/null +++ b/tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_ctx_entity_missing_ws_name/ro-crate-metadata.json @@ -0,0 +1,97 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-04-17T13:39:44+00:00", + "name": "Valid RO Crate with foo/xxx indirectly referenced", + "description": "This RO Crate contains a foo Dataset (directory) which doesn't have a trailing slash in its @id", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + } + ], + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "license": [ + { + "@id": "https://spdx.org/licenses/MIT.html" + } + ] + }, + { + "@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"], + "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" + }, + "encodingFormat": ["application/galaxy"] + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "encodingFormat": ["application/galaxy"] + }, + { + "@id": "blank.png", + "@type": ["File", "ImageObject"], + "encodingFormat": [ + { + "@id": "https://www.iana.org/assignments/media-types/image/png" + } + ] + }, + { + "@id": "https://www.iana.org/assignments/media-types/image/png", + "@type": "WebSite" + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": ["text/markdown"] + }, + { "@id": "https://spdx.org/licenses/MIT.html", "@type": "license" } + ] +} diff --git a/tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_ctx_entity_missing_ws_type/ro-crate-metadata.json b/tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_ctx_entity_missing_ws_type/ro-crate-metadata.json new file mode 100644 index 00000000..2a315b6e --- /dev/null +++ b/tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_ctx_entity_missing_ws_type/ro-crate-metadata.json @@ -0,0 +1,93 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-04-17T13:39:44+00:00", + "name": "Valid RO Crate with foo/xxx indirectly referenced", + "description": "This RO Crate contains a foo Dataset (directory) which doesn't have a trailing slash in its @id", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + } + ], + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "license": [ + { + "@id": "https://spdx.org/licenses/MIT.html" + } + ] + }, + { + "@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"], + "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" + }, + "encodingFormat": ["application/galaxy-workflow+yaml"] + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "encodingFormat": ["application/galaxy-workflow+yaml"] + }, + { + "@id": "blank.png", + "@type": ["File", "ImageObject"], + "encodingFormat": [ + { + "@id": "https://www.iana.org/assignments/media-types/image/png" + } + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": ["text/markdown"] + }, + { "@id": "https://spdx.org/licenses/MIT.html", "@type": "license" } + ] +} diff --git a/tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_pronom/ro-crate-metadata.json b/tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_pronom/ro-crate-metadata.json new file mode 100644 index 00000000..35111f24 --- /dev/null +++ b/tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_pronom/ro-crate-metadata.json @@ -0,0 +1,89 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-04-17T13:39:44+00:00", + "name": "Valid RO Crate with foo/xxx indirectly referenced", + "description": "This RO Crate contains a foo Dataset (directory) which doesn't have a trailing slash in its @id", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + } + ], + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "license": [ + { + "@id": "https://spdx.org/licenses/MIT.html" + } + ] + }, + { + "@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"], + "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" + }, + "encodingFormat": ["application/galaxy-workflow+yaml"] + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "encodingFormat": ["application/galaxy-workflow+yaml"] + }, + { + "@id": "blank.png", + "@type": ["File", "ImageObject"], + "encodingFormat": ["image/png"] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": ["text/markdown"] + }, + { "@id": "https://spdx.org/licenses/MIT.html", "@type": "license" } + ] +} diff --git a/tests/data/crates/invalid/4_data_entity_metadata/missing_encoding_format/ro-crate-metadata.json b/tests/data/crates/invalid/4_data_entity_metadata/missing_encoding_format/ro-crate-metadata.json new file mode 100644 index 00000000..3744b2cf --- /dev/null +++ b/tests/data/crates/invalid/4_data_entity_metadata/missing_encoding_format/ro-crate-metadata.json @@ -0,0 +1,120 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-04-17T13:39:44+00:00", + "name": "Valid RO Crate with foo/xxx indirectly referenced", + "description": "This RO Crate contains a foo Dataset (directory) which doesn't have a trailing slash in its @id", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "foo/" + }, + { + "@id": "README.md" + } + ], + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "license": "https://spdx.org/licenses/Apache-2.0.html" + }, + { + "@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"], + "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": "foo/", + "@type": "Dataset", + "hasPart": [ + { + "@id": "foo/xxx" + } + ] + }, + { + "@id": "foo/xxx", + "@type": "File", + "name": "xxx" + }, + { + "@id": "blank.png", + "@type": ["File", "ImageObject"] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + } + } + ] +} 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 new file mode 100644 index 00000000..3e6d981c --- /dev/null +++ b/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_ctx_entity/ro-crate-metadata.json @@ -0,0 +1,98 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-04-17T13:39:44+00:00", + "name": "Valid RO Crate with foo/xxx indirectly referenced", + "description": "This RO Crate contains a foo Dataset (directory) which doesn't have a trailing slash in its @id", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + } + ], + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "license": [ + { + "@id": "https://spdx.org/licenses/MIT.html" + } + ] + }, + { + "@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"], + "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" + }, + "encodingFormat": ["application/galaxy"] + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "encodingFormat": ["application/galaxy"] + }, + { + "@id": "blank.png", + "@type": ["File", "ImageObject"], + "encodingFormat": [ + { + "@id": "https://www.iana.org/assignments/media-types/image/png" + } + ] + }, + { + "@id": "https://www.iana.org/assignments/media-types/image/png", + "@type": "WebSite", + "name": "image/png" + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": ["text/markdown"] + }, + { "@id": "https://spdx.org/licenses/MIT.html", "@type": "license" } + ] +} 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 new file mode 100644 index 00000000..96eca541 --- /dev/null +++ b/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_pronom/ro-crate-metadata.json @@ -0,0 +1,89 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "datePublished": "2024-04-17T13:39:44+00:00", + "name": "Valid RO Crate with foo/xxx indirectly referenced", + "description": "This RO Crate contains a foo Dataset (directory) which doesn't have a trailing slash in its @id", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + } + ], + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "license": [ + { + "@id": "https://spdx.org/licenses/MIT.html" + } + ] + }, + { + "@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"], + "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" + }, + "encodingFormat": ["application/galaxy"] + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "encodingFormat": ["application/galaxy"] + }, + { + "@id": "blank.png", + "@type": ["File", "ImageObject"], + "encodingFormat": ["image/png"] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": ["text/markdown"] + }, + { "@id": "https://spdx.org/licenses/MIT.html", "@type": "license" } + ] +} 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 1a978ea4..1b5f1445 100644 --- a/tests/integration/profiles/ro-crate/test_data_entity_metadata.py +++ b/tests/integration/profiles/ro-crate/test_data_entity_metadata.py @@ -50,3 +50,65 @@ def test_directory_data_entity_wo_trailing_slash(): ["Directory Data Entity: REQUIRED value restriction"], ["Every Data Entity Directory URI MUST end with `/`"] ) + + +def test_missing_data_entity_encoding_format(): + """""" + do_entity_test( + paths.missing_data_entity_encoding_format, + models.Severity.RECOMMENDED, + False, + ["File Data Entity: RECOMMENDED properties"], + ["Missing or invalid `encodingFormat` linked to the `File Data Entity`"] + ) + + +def test_invalid_data_entity_encoding_format_pronom(): + """""" + do_entity_test( + paths.invalid_data_entity_encoding_format_pronom, + models.Severity.RECOMMENDED, + False, + ["File Data Entity: RECOMMENDED properties"], + ["Missing or invalid `encodingFormat` linked to the `File Data Entity`"] + ) + + +def test_invalid_data_entity_encoding_format_ctx_website_type(): + """""" + do_entity_test( + paths.invalid_encoding_format_ctx_entity_missing_ws_type, + models.Severity.RECOMMENDED, + False, + ["File Data Entity: RECOMMENDED properties"], + ["Missing or invalid `encodingFormat` linked to the `File Data Entity`"] + ) + + +def test_invalid_data_entity_encoding_format_ctx_website_name(): + """""" + do_entity_test( + paths.invalid_encoding_format_ctx_entity_missing_ws_name, + models.Severity.RECOMMENDED, + False, + ["WebSite RECOMMENDED Properties"], + ["A WebSite MUST have a `name` property"] + ) + + +def test_valid_data_entity_encoding_format_pronom(): + """""" + do_entity_test( + paths.valid_encoding_format_pronom, + models.Severity.RECOMMENDED, + True + ) + + +def test_valid_data_entity_encoding_format_ctx_website(): + """""" + do_entity_test( + paths.valid_encoding_format_ctx_entity, + models.Severity.RECOMMENDED, + True + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 466d0cf7..5267d91d 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -147,3 +147,27 @@ def indirect_hasPart_data_entity_reference(self) -> Path: @property def directory_data_entity_wo_trailing_slash(self) -> Path: return self.base_path / "directory_data_entity_wo_trailing_slash" + + @property + def missing_data_entity_encoding_format(self) -> Path: + return self.base_path / "missing_encoding_format" + + @property + def invalid_data_entity_encoding_format_pronom(self) -> Path: + return self.base_path / "invalid_encoding_format_pronom" + + @property + def invalid_encoding_format_ctx_entity_missing_ws_type(self) -> Path: + return self.base_path / "invalid_encoding_format_ctx_entity_missing_ws_type" + + @property + def invalid_encoding_format_ctx_entity_missing_ws_name(self) -> Path: + return self.base_path / "invalid_encoding_format_ctx_entity_missing_ws_name" + + @property + def valid_encoding_format_ctx_entity(self) -> Path: + return self.base_path / "valid_encoding_format_ctx_entity" + + @property + def valid_encoding_format_pronom(self) -> Path: + return self.base_path / "valid_encoding_format_pronom" From 6c1b5ccd58dcb3f0b11257bd699ef907d84cab68 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 4 Jun 2024 18:40:54 +0200 Subject: [PATCH 469/902] test(test-conf): :white_check_mark: fix missing encondings on valid crate --- .../valid/wrroc-paper/ro-crate-metadata.json | 1449 ++++++++--------- 1 file changed, 716 insertions(+), 733 deletions(-) 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 9be1c375..a76f1197 100644 --- a/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json +++ b/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json @@ -1,814 +1,797 @@ { - "@context": [ - "https://w3id.org/ro/crate/1.1/context", - { - "Standard": "http://purl.org/dc/terms/Standard", - "Profile": "http://www.w3.org/ns/dx/prof/Profile", - "MappingSet": "https://w3id.org/sssom/schema/MappingSet" - }, - { - "copyrightNotice": "http://schema.org/copyrightNotice", - "interpretedAsClaim": "http://schema.org/interpretedAsClaim", - "archivedAt": "http://schema.org/archivedAt", - "creditText": "http://schema.org/creditText" - } - ], - "@graph": [ - { - "@id": "ro-crate-metadata.json", - "@type": "CreativeWork", - "conformsTo": { - "@id": "https://w3id.org/ro/crate/1.1" - }, - "about": { - "@id": "./" - }, - "author": { - "@id": "https://orcid.org/0000-0001-9842-9718" - }, - "license": { - "@id": "https://creativecommons.org/publicdomain/zero/1.0/" - } - }, - { - "@id": "./", - "identifier": { - "@id": "https://doi.org/10.5281/zenodo.10368990" - }, - "url": "https://w3id.org/ro/doi/10.5281/zenodo.10368989", - "@type": "Dataset", - "about": { - "@id": "https://researchobject.org/workflow-run-crate/" - }, - "author": [ - { - "@id": "https://orcid.org/0000-0001-8271-5429" - }, - { - "@id": "https://orcid.org/0000-0002-2961-9670" - }, - { - "@id": "https://orcid.org/0000-0003-4929-1219" - }, - { - "@id": "https://orcid.org/0000-0003-0606-2512" - }, - { - "@id": "https://orcid.org/0000-0002-3468-0652" - }, - { - "@id": "https://orcid.org/0000-0002-8940-4946" - }, - { - "@id": "https://orcid.org/0000-0002-0003-2024" - }, - { - "@id": "https://orcid.org/0000-0002-4663-5613" - }, - { - "@id": "https://orcid.org/0000-0003-0454-7145" - }, - { - "@id": "https://orcid.org/0000-0002-4806-5140" - }, - { - "@id": "https://orcid.org/0000-0001-9290-2017" - }, - { - "@id": "https://orcid.org/0000-0002-1119-1792" - }, + "@context": [ + "https://w3id.org/ro/crate/1.1/context", { - "@id": "https://orcid.org/0000-0003-3777-5945" + "Standard": "http://purl.org/dc/terms/Standard", + "Profile": "http://www.w3.org/ns/dx/prof/Profile", + "MappingSet": "https://w3id.org/sssom/schema/MappingSet" }, { - "@id": "https://orcid.org/0000-0003-2765-0049" - }, - { - "@id": "https://orcid.org/0000-0002-0309-604X" - }, - { - "@id": "https://orcid.org/0000-0003-0902-0086" - }, - { - "@id": "https://orcid.org/0000-0001-8250-4074" - }, - { - "@id": "https://orcid.org/0000-0001-9842-9718" + "copyrightNotice": "http://schema.org/copyrightNotice", + "interpretedAsClaim": "http://schema.org/interpretedAsClaim", + "archivedAt": "http://schema.org/archivedAt", + "creditText": "http://schema.org/creditText" } - ], - "description": "RO-Crate for the manuscript that describes Workflow Run Crate, includes mapping to PROV using SKOS/SSSOM", - "hasPart": [ - { - "@id": "https://w3id.org/ro/wfrun/process/0.3" - }, - { - "@id": "https://w3id.org/ro/wfrun/workflow/0.3" + ], + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.1" + }, + "about": { + "@id": "./" + }, + "author": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + } + }, + { + "@id": "./", + "identifier": { + "@id": "https://doi.org/10.5281/zenodo.10368990" + }, + "url": "https://w3id.org/ro/doi/10.5281/zenodo.10368989", + "@type": "Dataset", + "about": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "author": [ + { + "@id": "https://orcid.org/0000-0001-8271-5429" + }, + { + "@id": "https://orcid.org/0000-0002-2961-9670" + }, + { + "@id": "https://orcid.org/0000-0003-4929-1219" + }, + { + "@id": "https://orcid.org/0000-0003-0606-2512" + }, + { + "@id": "https://orcid.org/0000-0002-3468-0652" + }, + { + "@id": "https://orcid.org/0000-0002-8940-4946" + }, + { + "@id": "https://orcid.org/0000-0002-0003-2024" + }, + { + "@id": "https://orcid.org/0000-0002-4663-5613" + }, + { + "@id": "https://orcid.org/0000-0003-0454-7145" + }, + { + "@id": "https://orcid.org/0000-0002-4806-5140" + }, + { + "@id": "https://orcid.org/0000-0001-9290-2017" + }, + { + "@id": "https://orcid.org/0000-0002-1119-1792" + }, + { + "@id": "https://orcid.org/0000-0003-3777-5945" + }, + { + "@id": "https://orcid.org/0000-0003-2765-0049" + }, + { + "@id": "https://orcid.org/0000-0002-0309-604X" + }, + { + "@id": "https://orcid.org/0000-0003-0902-0086" + }, + { + "@id": "https://orcid.org/0000-0001-8250-4074" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718" + } + ], + "description": "RO-Crate for the manuscript that describes Workflow Run Crate, includes mapping to PROV using SKOS/SSSOM", + "hasPart": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/workflow/0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/provenance/0.3" + }, + { + "@id": "mapping/" + } + ], + "name": "Recording provenance of workflow runs with RO-Crate (RO-Crate and mapping)", + "datePublished": "2023-12-12", + "license": { + "@id": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "creator": [] + }, + { + "@id": "https://doi.org/10.5281/zenodo.10368990", + "@type": "PropertyValue", + "name": "doi", + "propertyID": "https://registry.identifiers.org/registry/doi", + "value": "doi:10.5281/zenodo.10368990", + "url": "https://doi.org/10.5281/zenodo.10368990" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718", + "@type": "Person", + "affiliation": [ + "Department of Computer Science, The University of Manchester, Manchester, United Kingdom", + "Informatics Institute, University of Amsterdam, Amsterdam, The Netherlands" + ], + "name": "Stian Soiland-Reyes" + }, + { + "@id": "https://researchobject.org/workflow-run-crate/", + "@type": "Project", + "member": [ + { + "@id": "https://orcid.org/0000-0001-8271-5429" + }, + { + "@id": "https://orcid.org/0000-0003-4929-1219" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + { + "@id": "https://orcid.org/0000-0002-5432-2748" + }, + { + "@id": "https://orcid.org/0000-0002-4806-5140" + }, + { + "@id": "https://orcid.org/0000-0003-3156-2105" + }, + { + "@id": "https://orcid.org/0000-0002-6190-122X" + }, + { + "@id": "https://orcid.org/0000-0003-0454-7145" + }, + { + "@id": "https://orcid.org/0000-0002-8940-4946" + }, + { + "@id": "https://orcid.org/0000-0003-0606-2512" + }, + { + "@id": "https://orcid.org/0000-0002-3468-0652" + }, + { + "@id": "https://orcid.org/0000-0002-2961-9670" + }, + { + "@id": "https://orcid.org/0000-0003-3986-0510" + }, + { + "@id": "https://orcid.org/0000-0002-0003-2024" + }, + { + "@id": "https://orcid.org/0000-0002-9464-6640" + }, + { + "@id": "https://orcid.org/0000-0001-5845-8880" + }, + { + "@id": "https://orcid.org/0000-0003-4894-4660" + }, + { + "@id": "https://orcid.org/0000-0002-4405-6802" + }, + { + "@id": "https://orcid.org/0000-0001-9290-2017" + }, + { + "@id": "https://orcid.org/0000-0003-0617-9219" + }, + { + "@id": "https://orcid.org/0000-0001-9228-2882" + }, + { + "@id": "https://orcid.org/0000-0003-3898-9451" + }, + { + "@id": "https://orcid.org/0000-0003-3777-5945" + }, + { + "@id": "https://orcid.org/0000-0003-2765-0049" + }, + { + "@id": "https://orcid.org/0000-0001-9818-9320" + }, + { + "@id": "https://orcid.org/0000-0002-8122-9522" + }, + { + "@id": "https://orcid.org/0000-0002-8330-4071" + }, + { + "@id": "https://orcid.org/0000-0003-4073-7456" + }, + { + "@id": "https://orcid.org/0000-0003-1361-7301" + }, + { + "@id": "https://orcid.org/0000-0002-5358-616X" + }, + { + "@id": "https://orcid.org/0000-0002-5477-287X" + }, + { + "@id": "https://orcid.org/0000-0001-8250-4074" + }, + { + "@id": "https://orcid.org/0000-0003-0902-0086" + }, + { + "@id": "https://orcid.org/0000-0001-8172-8981" + }, + { + "@id": "https://orcid.org/0000-0001-6740-9212" + } + ], + "name": "Workflow Run Crate task force", + "parentOrganization": { + "@id": "https://www.researchobject.org/ro-crate/community" + } + }, + { + "@id": "https://orcid.org/0000-0001-8271-5429", + "@type": "Person", + "affiliation": "Center for Advanced Studies, Research, and Development in Sardinia (CRS4), Pula, Sardinia, Italy", + "name": "Simone Leo" + }, + { + "@id": "https://orcid.org/0000-0002-2961-9670", + "@type": "Person", + "affiliation": [ + "Vrije Universiteit Amsterdam, Amsterdam, The Netherlands", + "DTL Projects, The Netherlands", + "Forschungszentrum Jรผlich, Germany" + ], + "name": "Michael R Crusoe" + }, + { + "@id": "https://orcid.org/0000-0003-4929-1219", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": "Laura Rodrรญguez-Navas" + }, + { + "@id": "https://orcid.org/0000-0003-0606-2512", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": "Raรผl Sirvent" + }, + { + "@id": "https://orcid.org/0000-0002-3468-0652", + "@type": "Person", + "affiliation": [ + "Biozentrum, University of Basel, Basel, Switzerland", + "Swiss Institute of Bioinformatics, Lausanne, Switzerland" + ], + "name": "Alexander Kanitz" + }, + { + "@id": "https://orcid.org/0000-0002-8940-4946", + "@type": "Person", + "affiliation": "VIB-UGent Center for Plant Systems Biology, Gent, Belgium", + "name": "Paul De Geest" + }, + { + "@id": "https://orcid.org/0000-0002-0003-2024", + "@type": "Person", + "affiliation": [ + "Faculty of Informatics, Masaryk Universit, Brno, Czech Republic", + "Institute of Computer Science, Masaryk University, Brno, Czech Republic", + "BBMRI-ERIC, Graz, Austria" + ], + "name": "Rudolf Wittner" + }, + { + "@id": "https://orcid.org/0000-0002-4663-5613", + "@type": "Person", + "affiliation": "Center for Advanced Studies, Research, and Development in Sardinia (CRS4), Pula, Sardinia, Italy", + "name": "Luca Pireddu" + }, + { + "@id": "https://orcid.org/0000-0003-0454-7145", + "@type": "Person", + "affiliation": "Ontology Engineering Group, Universidad Politรฉcnica de Madrid, Madrid, Spain", + "name": "Daniel Garijo" + }, + { + "@id": "https://orcid.org/0000-0002-4806-5140", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": ["Josรฉ Marรญa Fernรกndez", "Josรฉ M. Fernรกndez"] + }, + { + "@id": "https://orcid.org/0000-0001-9290-2017", + "@type": "Person", + "affiliation": "Computer Science Dept., Universitร  degli Studi di Torino, Torino, Italy", + "name": "Iacopo Colonnelli" + }, + { + "@id": "https://orcid.org/0000-0002-1119-1792", + "@type": "Person", + "affiliation": "Faculty of Informatics, Masaryk University, Brno, Czech Republic", + "name": "Matej Gallo" + }, + { + "@id": "https://orcid.org/0000-0003-3777-5945", + "@type": "Person", + "affiliation": [ + "Database Center for Life Science, Joint Support-Center for Data Science Research, Research Organization of Information and Systems, Shizuoka, Japan", + "Institute for Advanced Academic Research, Chiba University, Chiba, Japan" + ], + "name": "Tazro Ohta" + }, + { + "@id": "https://orcid.org/0000-0003-2765-0049", + "@type": "Person", + "affiliation": "Sator Inc., Tokyo, Japan", + "name": "Hirotaka Suetake" + }, + { + "@id": "https://orcid.org/0000-0002-0309-604X", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": "Salvador Capella-Gutierrez" + }, + { + "@id": "https://orcid.org/0000-0003-0902-0086", + "@type": "Person", + "affiliation": "Vrije Universiteit Amsterdam, Amsterdam, The Netherlands", + "name": "Renske de Wit" + }, + { + "@id": "https://orcid.org/0000-0001-8250-4074", + "@type": "Person", + "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", + "name": ["Bruno P. Kinoshita", "Bruno de Paula Kinoshita"] + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.3", + "@type": "Profile", + "author": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "name": "Process Run Crate", + "version": "0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/workflow/0.3", + "@type": "Profile", + "author": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "name": "Workflow Run Crate", + "version": "0.3" + }, + { + "@id": "https://w3id.org/ro/wfrun/provenance/0.3", + "@type": "Profile", + "author": { + "@id": "https://researchobject.org/workflow-run-crate/" + }, + "name": "Provenance Run Crate", + "version": "0.3" + }, + { + "@id": "mapping/environment.yml", + "@type": "File", + "conformsTo": { + "@id": "https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually" + }, + "encodingFormat": [ + "application/yaml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818" + } + ], + "name": "Conda environment for sssom" + }, + { + "@id": "mapping/environment.lock.yml", + "@type": "File", + "conformsTo": { + "@id": "https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually" + }, + "encodingFormat": [ + "application/yaml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818" + } + ], + "name": "Conda environment for sssom, version-pinned" + }, + { + "@id": "mapping/prov-mapping.tsv", + "@type": ["File", "MappingSet"], + "conformsTo": [ + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#tsv" + } + ], + "author": [ + { + "@id": "https://orcid.org/0000-0003-0454-7145" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718" + } + ], + "creator": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "encodingFormat": [ + "text/tab-separated-values", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/x-fmt/13" + } + ], + "name": "PROV mapping to Workflow Run Crate (SSSOM TSV)" + }, + { + "@id": "mapping/prov-mapping.yml", + "@type": ["File", "MappingSet"], + "conformsTo": { + "@id": "https://w3id.org/sssom/" + }, + "encodingFormat": [ + "text/yaml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818" + } + ], + "name": "PROV mapping to Workflow Run Crate (SSSOM metadata)" + }, + { + "@id": "mapping/prov-mapping-w-metadata.tsv", + "@type": ["File", "MappingSet"], + "conformsTo": [ + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#tsv" + } + ], + "encodingFormat": "text/tab-separated-values", + "isBasedOn": [ + { + "@id": "mapping/prov-mapping.tsv" + }, + { + "@id": "mapping/prov-mapping.yml" + } + ], + "name": "PROV mapping to Workflow Run Crate (SSSOM TSV with metadata)" + }, + { + "@id": "mapping/prov-mapping.ttl", + "@type": ["File", "MappingSet"], + "conformsTo": [ + { + "@id": "http://www.w3.org/TR/owl2-mapping-to-rdf/" + }, + { + "@id": "http://www.w3.org/TR/skos-reference" + } + ], + "encodingFormat": [ + "text/turtle", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/874" + } + ], + "isBasedOn": { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + "name": "PROV to Workflow Run Crate (SKOS and SSSOM OWL axioms)" + }, + { + "@id": "mapping/prov-mapping.rdf", + "@type": ["File", "MappingSet"], + "conformsTo": [ + { + "@id": "http://www.w3.org/TR/owl2-mapping-to-rdf/" + }, + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#rdfxml-serialised-re-ified-owl-axioms" + } + ], + "encodingFormat": [ + "application/rdf+xml", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/875" + } + ], + "isBasedOn": { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + "name": "PROV mapping to Workflow Run Crate (SSSOM OWL axioms)" + }, + { + "@id": "mapping/prov-mapping.json", + "@type": ["File", "MappingSet"], + "conformsTo": [ + { + "@id": "https://w3id.org/sssom/" + }, + { + "@id": "https://mapping-commons.github.io/sssom/spec/#json" + } + ], + "encodingFormat": [ + "application/json", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/817" + } + ], + "isBasedOn": { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + "name": "PROV mapping to Workflow Run Crate (SSSOM JSON)" + }, + { + "@id": "http://www.w3.org/TR/owl2-mapping-to-rdf/", + "@type": "CreativeWork", + "name": "OWL 2 (in RDF)" }, { - "@id": "https://w3id.org/ro/wfrun/provenance/0.3" + "@id": "http://www.w3.org/TR/skos-reference", + "@type": "CreativeWork", + "alternateName": "SKOS Simple Knowledge Organization System Reference", + "name": "SKOS" }, { - "@id": "mapping/" - } - ], - "name": "Recording provenance of workflow runs with RO-Crate (RO-Crate and mapping)", - "datePublished": "2023-12-12", - "license": { - "@id": "https://www.apache.org/licenses/LICENSE-2.0" - }, - "creator": [] - }, - { - "@id": "https://doi.org/10.5281/zenodo.10368990", - "@type": "PropertyValue", - "name": "doi", - "propertyID": "https://registry.identifiers.org/registry/doi", - "value": "doi:10.5281/zenodo.10368990", - "url": "https://doi.org/10.5281/zenodo.10368990" - }, - { - "@id": "https://orcid.org/0000-0001-9842-9718", - "@type": "Person", - "affiliation": [ - "Department of Computer Science, The University of Manchester, Manchester, United Kingdom", - "Informatics Institute, University of Amsterdam, Amsterdam, The Netherlands" - ], - "name": "Stian Soiland-Reyes" - }, - { - "@id": "https://researchobject.org/workflow-run-crate/", - "@type": "Project", - "member": [ - { - "@id": "https://orcid.org/0000-0001-8271-5429" + "@id": "http://www.w3.org/ns/dx/prof/Profile", + "@type": "DefinedTerm", + "name": "Profile", + "url": "https://www.w3.org/TR/dx-prof/" }, { - "@id": "https://orcid.org/0000-0003-4929-1219" + "@id": "https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually", + "@type": "CreativeWork", + "name": "Conda environment file format" }, { - "@id": "https://orcid.org/0000-0001-9842-9718" + "@id": "https://mapping-commons.github.io/sssom/spec/#json", + "@type": "WebPageElement", + "name": "SSSOM JSON" }, { - "@id": "https://orcid.org/0000-0002-5432-2748" + "@id": "https://mapping-commons.github.io/sssom/spec/#rdfxml-serialised-re-ified-owl-axioms", + "@type": "WebPageElement", + "name": "SSSOM RDF/XML serialised re-ified OWL axioms" }, { - "@id": "https://orcid.org/0000-0002-4806-5140" + "@id": "https://mapping-commons.github.io/sssom/spec/#tsv", + "@type": "WebPageElement", + "name": "SSSOM TSV" }, { - "@id": "https://orcid.org/0000-0003-3156-2105" + "@id": "https://orcid.org/0000-0001-5845-8880", + "@type": "Person", + "name": "Sebastiaan Huber" }, { - "@id": "https://orcid.org/0000-0002-6190-122X" + "@id": "https://orcid.org/0000-0001-6740-9212", + "@type": "Person", + "name": "Samuel Lampa" }, { - "@id": "https://orcid.org/0000-0003-0454-7145" + "@id": "https://orcid.org/0000-0001-8172-8981", + "@type": "Person", + "name": "Jasper Koehorst" }, { - "@id": "https://orcid.org/0000-0002-8940-4946" + "@id": "https://orcid.org/0000-0001-9228-2882", + "@type": "Person", + "name": "Abigail Miller" }, { - "@id": "https://orcid.org/0000-0003-0606-2512" + "@id": "https://orcid.org/0000-0001-9818-9320", + "@type": "Person", + "name": "Johannes Kรถster" }, { - "@id": "https://orcid.org/0000-0002-3468-0652" + "@id": "https://orcid.org/0000-0002-4405-6802", + "@type": "Person", + "name": "Haris Zafeiropoulos" }, { - "@id": "https://orcid.org/0000-0002-2961-9670" + "@id": "https://orcid.org/0000-0002-5358-616X", + "@type": "Person", + "name": "Petr Holub" }, { - "@id": "https://orcid.org/0000-0003-3986-0510" + "@id": "https://orcid.org/0000-0002-5432-2748", + "@type": "Person", + "name": "Paul Brack" }, { - "@id": "https://orcid.org/0000-0002-0003-2024" + "@id": "https://orcid.org/0000-0002-5477-287X", + "@type": "Person", + "name": "Milan Markovic" }, { - "@id": "https://orcid.org/0000-0002-9464-6640" + "@id": "https://orcid.org/0000-0002-6190-122X", + "@type": "Person", + "name": "Ignacio Eguinoa" }, { - "@id": "https://orcid.org/0000-0001-5845-8880" + "@id": "https://orcid.org/0000-0002-8122-9522", + "@type": "Person", + "name": "Luiz Gadelha" }, { - "@id": "https://orcid.org/0000-0003-4894-4660" + "@id": "https://orcid.org/0000-0002-8330-4071", + "@type": "Person", + "name": "Mahnoor Zulfiqar" }, { - "@id": "https://orcid.org/0000-0002-4405-6802" + "@id": "https://orcid.org/0000-0002-9464-6640", + "@type": "Person", + "name": "Wolfgang Maier" }, { - "@id": "https://orcid.org/0000-0001-9290-2017" + "@id": "https://orcid.org/0000-0003-0617-9219", + "@type": "Person", + "name": "Jake Emerson" }, { - "@id": "https://orcid.org/0000-0003-0617-9219" + "@id": "https://orcid.org/0000-0003-1361-7301", + "@type": "Person", + "name": "Maciek Bฤ…k" }, { - "@id": "https://orcid.org/0000-0001-9228-2882" + "@id": "https://orcid.org/0000-0003-3156-2105", + "@type": "Person", + "name": "Alan R Williams" }, { - "@id": "https://orcid.org/0000-0003-3898-9451" + "@id": "https://orcid.org/0000-0003-3898-9451", + "@type": "Person", + "name": "Stelios Ninidakis" }, { - "@id": "https://orcid.org/0000-0003-3777-5945" + "@id": "https://orcid.org/0000-0003-3986-0510", + "@type": "Person", + "name": "LJ Garcia Castro" }, { - "@id": "https://orcid.org/0000-0003-2765-0049" + "@id": "https://orcid.org/0000-0003-4073-7456", + "@type": "Person", + "name": "Romain David" }, { - "@id": "https://orcid.org/0000-0001-9818-9320" + "@id": "https://orcid.org/0000-0003-4894-4660", + "@type": "Person", + "name": "Kevin Jablonka" }, { - "@id": "https://orcid.org/0000-0002-8122-9522" + "@id": "https://www.researchobject.org/ro-crate/community", + "@type": "Project", + "name": "RO-Crate Community" }, { - "@id": "https://orcid.org/0000-0002-8330-4071" + "@id": "https://w3id.org/sssom/", + "@type": ["WebPage", "Standard"], + "alternateName": "Simple Standard for Sharing Ontological Mappings", + "name": "SSSOM", + "version": "0.15.0" }, { - "@id": "https://orcid.org/0000-0003-4073-7456" + "@id": "https://w3id.org/sssom/schema/MappingSet", + "@type": "DefinedTerm", + "url": "https://mapping-commons.github.io/sssom/MappingSet/" }, { - "@id": "https://orcid.org/0000-0003-1361-7301" + "@id": "https://www.apache.org/licenses/LICENSE-2.0", + "@type": "CreativeWork", + "name": "Apache License, Version 2.0", + "version": "2.0", + "identifier": { + "@id": "http://spdx.org/licenses/Apache-2.0" + } }, { - "@id": "https://orcid.org/0000-0002-5358-616X" + "@id": "https://creativecommons.org/publicdomain/zero/1.0/", + "@type": "CreativeWork", + "identifier": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "name": "Creative Commons Zero v1.0 Universal", + "version": "1.0" }, { - "@id": "https://orcid.org/0000-0002-5477-287X" + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "identifier": { + "@id": "http://spdx.org/licenses/CC-BY-4.0" + }, + "name": "Creative Commons Attribution 4.0 International", + "version": "4.0" }, { - "@id": "https://orcid.org/0000-0001-8250-4074" + "@id": "http://spdx.org/licenses/Apache-2.0", + "@type": "PropertyValue", + "propertyID": "http://spdx.org/rdf/terms#licenseId", + "name": "spdx", + "value": "Apache-2.0" }, { - "@id": "https://orcid.org/0000-0003-0902-0086" + "@id": "http://spdx.org/licenses/CC0-1.0", + "@type": "PropertyValue", + "propertyID": "http://spdx.org/rdf/terms#licenseId", + "name": "spdx", + "value": "CC0-1.0" }, { - "@id": "https://orcid.org/0000-0001-8172-8981" + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/817", + "@type": "WebSite", + "name": "JSON" }, { - "@id": "https://orcid.org/0000-0001-6740-9212" - } - ], - "name": "Workflow Run Crate task force", - "parentOrganization": { - "@id": "https://www.researchobject.org/ro-crate/community" - } - }, - { - "@id": "https://orcid.org/0000-0001-8271-5429", - "@type": "Person", - "affiliation": "Center for Advanced Studies, Research, and Development in Sardinia (CRS4), Pula, Sardinia, Italy", - "name": "Simone Leo" - }, - { - "@id": "https://orcid.org/0000-0002-2961-9670", - "@type": "Person", - "affiliation": [ - "Vrije Universiteit Amsterdam, Amsterdam, The Netherlands", - "DTL Projects, The Netherlands", - "Forschungszentrum Jรผlich, Germany" - ], - "name": "Michael R Crusoe" - }, - { - "@id": "https://orcid.org/0000-0003-4929-1219", - "@type": "Person", - "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", - "name": "Laura Rodrรญguez-Navas" - }, - { - "@id": "https://orcid.org/0000-0003-0606-2512", - "@type": "Person", - "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", - "name": "Raรผl Sirvent" - }, - { - "@id": "https://orcid.org/0000-0002-3468-0652", - "@type": "Person", - "affiliation": [ - "Biozentrum, University of Basel, Basel, Switzerland", - "Swiss Institute of Bioinformatics, Lausanne, Switzerland" - ], - "name": "Alexander Kanitz" - }, - { - "@id": "https://orcid.org/0000-0002-8940-4946", - "@type": "Person", - "affiliation": "VIB-UGent Center for Plant Systems Biology, Gent, Belgium", - "name": "Paul De Geest" - }, - { - "@id": "https://orcid.org/0000-0002-0003-2024", - "@type": "Person", - "affiliation": [ - "Faculty of Informatics, Masaryk Universit, Brno, Czech Republic", - "Institute of Computer Science, Masaryk University, Brno, Czech Republic", - "BBMRI-ERIC, Graz, Austria" - ], - "name": "Rudolf Wittner" - }, - { - "@id": "https://orcid.org/0000-0002-4663-5613", - "@type": "Person", - "affiliation": "Center for Advanced Studies, Research, and Development in Sardinia (CRS4), Pula, Sardinia, Italy", - "name": "Luca Pireddu" - }, - { - "@id": "https://orcid.org/0000-0003-0454-7145", - "@type": "Person", - "affiliation": "Ontology Engineering Group, Universidad Politรฉcnica de Madrid, Madrid, Spain", - "name": "Daniel Garijo" - }, - { - "@id": "https://orcid.org/0000-0002-4806-5140", - "@type": "Person", - "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", - "name": [ - "Josรฉ Marรญa Fernรกndez", - "Josรฉ M. Fernรกndez" - ] - }, - { - "@id": "https://orcid.org/0000-0001-9290-2017", - "@type": "Person", - "affiliation": "Computer Science Dept., Universitร  degli Studi di Torino, Torino, Italy", - "name": "Iacopo Colonnelli" - }, - { - "@id": "https://orcid.org/0000-0002-1119-1792", - "@type": "Person", - "affiliation": "Faculty of Informatics, Masaryk University, Brno, Czech Republic", - "name": "Matej Gallo" - }, - { - "@id": "https://orcid.org/0000-0003-3777-5945", - "@type": "Person", - "affiliation": [ - "Database Center for Life Science, Joint Support-Center for Data Science Research, Research Organization of Information and Systems, Shizuoka, Japan", - "Institute for Advanced Academic Research, Chiba University, Chiba, Japan" - ], - "name": "Tazro Ohta" - }, - { - "@id": "https://orcid.org/0000-0003-2765-0049", - "@type": "Person", - "affiliation": "Sator Inc., Tokyo, Japan", - "name": "Hirotaka Suetake" - }, - { - "@id": "https://orcid.org/0000-0002-0309-604X", - "@type": "Person", - "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", - "name": "Salvador Capella-Gutierrez" - }, - { - "@id": "https://orcid.org/0000-0003-0902-0086", - "@type": "Person", - "affiliation": "Vrije Universiteit Amsterdam, Amsterdam, The Netherlands", - "name": "Renske de Wit" - }, - { - "@id": "https://orcid.org/0000-0001-8250-4074", - "@type": "Person", - "affiliation": "Barcelona Supercomputing Center, Barcelona, Spain", - "name": [ - "Bruno P. Kinoshita", - "Bruno de Paula Kinoshita" - ] - }, - { - "@id": "https://w3id.org/ro/wfrun/process/0.3", - "@type": "Profile", - "author": { - "@id": "https://researchobject.org/workflow-run-crate/" - }, - "name": "Process Run Crate", - "version": "0.3" - }, - { - "@id": "https://w3id.org/ro/wfrun/workflow/0.3", - "@type": "Profile", - "author": { - "@id": "https://researchobject.org/workflow-run-crate/" - }, - "name": "Workflow Run Crate", - "version": "0.3" - }, - { - "@id": "https://w3id.org/ro/wfrun/provenance/0.3", - "@type": "Profile", - "author": { - "@id": "https://researchobject.org/workflow-run-crate/" - }, - "name": "Provenance Run Crate", - "version": "0.3" - }, - { - "@id": "mapping/environment.yml", - "@type": "File", - "conformsTo": { - "@id": "https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually" - }, - "encodingFormat": [ - "application/yaml", - { - "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818" - } - ], - "name": "Conda environment for sssom" - }, - { - "@id": "mapping/environment.lock.yml", - "@type": "File", - "conformsTo": { - "@id": "https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually" + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818", + "@type": "WebSite", + "name": "YAML" }, - "encodingFormat": [ - "application/yaml", - { - "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818" - } - ], - "name": "Conda environment for sssom, version-pinned" - }, - { - "@id": "mapping/prov-mapping.tsv", - "@type": [ - "File", - "MappingSet" - ], - "conformsTo": [ - { - "@id": "https://w3id.org/sssom/" - }, - { - "@id": "https://mapping-commons.github.io/sssom/spec/#tsv" - } - ], - "author": [ - { - "@id": "https://orcid.org/0000-0003-0454-7145" - }, - { - "@id": "https://orcid.org/0000-0001-9842-9718" - } - ], - "creator": { - "@id": "https://orcid.org/0000-0001-9842-9718" - }, - "encodingFormat": [ - "text/tab-separated-values", - { - "@id": "https://www.nationalarchives.gov.uk/PRONOM/x-fmt/13" - } - ], - "name": "PROV mapping to Workflow Run Crate (SSSOM TSV)" - }, - { - "@id": "mapping/prov-mapping.yml", - "@type": [ - "File", - "MappingSet" - ], - "conformsTo": { - "@id": "https://w3id.org/sssom/" - }, - "encodingFormat": [ - "text/yaml", - { - "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818" - } - ], - "name": "PROV mapping to Workflow Run Crate (SSSOM metadata)" - }, - { - "@id": "mapping/prov-mapping-w-metadata.tsv", - "@type": [ - "File", - "MappingSet" - ], - "conformsTo": [ - { - "@id": "https://w3id.org/sssom/" - }, - { - "@id": "https://mapping-commons.github.io/sssom/spec/#tsv" - } - ], - "encodingFormat": "text/tab-separated-values", - "isBasedOn": [ - { - "@id": "mapping/prov-mapping.tsv" - }, - { - "@id": "mapping/prov-mapping.yml" - } - ], - "name": "PROV mapping to Workflow Run Crate (SSSOM TSV with metadata)" - }, - { - "@id": "mapping/prov-mapping.ttl", - "@type": [ - "File", - "MappingSet" - ], - "conformsTo": [ - { - "@id": "http://www.w3.org/TR/owl2-mapping-to-rdf/" - }, - { - "@id": "http://www.w3.org/TR/skos-reference" - } - ], - "encodingFormat": [ - "text/turtle", - { - "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/874" - } - ], - "isBasedOn": { - "@id": "mapping/prov-mapping-w-metadata.tsv" - }, - "name": "PROV to Workflow Run Crate (SKOS and SSSOM OWL axioms)" - }, - { - "@id": "mapping/prov-mapping.rdf", - "@type": [ - "File", - "MappingSet" - ], - "conformsTo": [ - { - "@id": "http://www.w3.org/TR/owl2-mapping-to-rdf/" - }, - { - "@id": "https://w3id.org/sssom/" - }, - { - "@id": "https://mapping-commons.github.io/sssom/spec/#rdfxml-serialised-re-ified-owl-axioms" - } - ], - "encodingFormat": [ - "application/rdf+xml", - { - "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/875" - } - ], - "isBasedOn": { - "@id": "mapping/prov-mapping-w-metadata.tsv" - }, - "name": "PROV mapping to Workflow Run Crate (SSSOM OWL axioms)" - }, - { - "@id": "mapping/prov-mapping.json", - "@type": [ - "File", - "MappingSet" - ], - "conformsTo": [ - { - "@id": "https://w3id.org/sssom/" - }, - { - "@id": "https://mapping-commons.github.io/sssom/spec/#json" - } - ], - "encodingFormat": [ - "application/json", - { - "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/817" - } - ], - "isBasedOn": { - "@id": "mapping/prov-mapping-w-metadata.tsv" - }, - "name": "PROV mapping to Workflow Run Crate (SSSOM JSON)" - }, - { - "@id": "http://www.w3.org/TR/owl2-mapping-to-rdf/", - "@type": "CreativeWork", - "name": "OWL 2 (in RDF)" - }, - { - "@id": "http://www.w3.org/TR/skos-reference", - "@type": "CreativeWork", - "alternateName": "SKOS Simple Knowledge Organization System Reference", - "name": "SKOS" - }, - { - "@id": "http://www.w3.org/ns/dx/prof/Profile", - "@type": "DefinedTerm", - "name": "Profile", - "url": "https://www.w3.org/TR/dx-prof/" - }, - { - "@id": "https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually", - "@type": "CreativeWork", - "name": "Conda environment file format" - }, - { - "@id": "https://mapping-commons.github.io/sssom/spec/#json", - "@type": "WebPageElement", - "name": "SSSOM JSON" - }, - { - "@id": "https://mapping-commons.github.io/sssom/spec/#rdfxml-serialised-re-ified-owl-axioms", - "@type": "WebPageElement", - "name": "SSSOM RDF/XML serialised re-ified OWL axioms" - }, - { - "@id": "https://mapping-commons.github.io/sssom/spec/#tsv", - "@type": "WebPageElement", - "name": "SSSOM TSV" - }, - { - "@id": "https://orcid.org/0000-0001-5845-8880", - "@type": "Person", - "name": "Sebastiaan Huber" - }, - { - "@id": "https://orcid.org/0000-0001-6740-9212", - "@type": "Person", - "name": "Samuel Lampa" - }, - { - "@id": "https://orcid.org/0000-0001-8172-8981", - "@type": "Person", - "name": "Jasper Koehorst" - }, - { - "@id": "https://orcid.org/0000-0001-9228-2882", - "@type": "Person", - "name": "Abigail Miller" - }, - { - "@id": "https://orcid.org/0000-0001-9818-9320", - "@type": "Person", - "name": "Johannes Kรถster" - }, - { - "@id": "https://orcid.org/0000-0002-4405-6802", - "@type": "Person", - "name": "Haris Zafeiropoulos" - }, - { - "@id": "https://orcid.org/0000-0002-5358-616X", - "@type": "Person", - "name": "Petr Holub" - }, - { - "@id": "https://orcid.org/0000-0002-5432-2748", - "@type": "Person", - "name": "Paul Brack" - }, - { - "@id": "https://orcid.org/0000-0002-5477-287X", - "@type": "Person", - "name": "Milan Markovic" - }, - { - "@id": "https://orcid.org/0000-0002-6190-122X", - "@type": "Person", - "name": "Ignacio Eguinoa" - }, - { - "@id": "https://orcid.org/0000-0002-8122-9522", - "@type": "Person", - "name": "Luiz Gadelha" - }, - { - "@id": "https://orcid.org/0000-0002-8330-4071", - "@type": "Person", - "name": "Mahnoor Zulfiqar" - }, - { - "@id": "https://orcid.org/0000-0002-9464-6640", - "@type": "Person", - "name": "Wolfgang Maier" - }, - { - "@id": "https://orcid.org/0000-0003-0617-9219", - "@type": "Person", - "name": "Jake Emerson" - }, - { - "@id": "https://orcid.org/0000-0003-1361-7301", - "@type": "Person", - "name": "Maciek Bฤ…k" - }, - { - "@id": "https://orcid.org/0000-0003-3156-2105", - "@type": "Person", - "name": "Alan R Williams" - }, - { - "@id": "https://orcid.org/0000-0003-3898-9451", - "@type": "Person", - "name": "Stelios Ninidakis" - }, - { - "@id": "https://orcid.org/0000-0003-3986-0510", - "@type": "Person", - "name": "LJ Garcia Castro" - }, - { - "@id": "https://orcid.org/0000-0003-4073-7456", - "@type": "Person", - "name": "Romain David" - }, - { - "@id": "https://orcid.org/0000-0003-4894-4660", - "@type": "Person", - "name": "Kevin Jablonka" - }, - { - "@id": "https://www.researchobject.org/ro-crate/community", - "@type": "Project", - "name": "RO-Crate Community" - }, - { - "@id": "https://w3id.org/sssom/", - "@type": [ - "WebPage", - "Standard" - ], - "alternateName": "Simple Standard for Sharing Ontological Mappings", - "name": "SSSOM", - "version": "0.15.0" - }, - { - "@id": "https://w3id.org/sssom/schema/MappingSet", - "@type": "DefinedTerm", - "url": "https://mapping-commons.github.io/sssom/MappingSet/" - }, - { - "@id": "https://www.apache.org/licenses/LICENSE-2.0", - "@type": "CreativeWork", - "name": "Apache License, Version 2.0", - "version": "2.0", - "identifier": { - "@id": "http://spdx.org/licenses/Apache-2.0" - } - }, - { - "@id": "https://creativecommons.org/publicdomain/zero/1.0/", - "@type": "CreativeWork", - "identifier": { - "@id": "http://spdx.org/licenses/CC0-1.0" - }, - "name": "Creative Commons Zero v1.0 Universal", - "version": "1.0" - }, - { - "@id": "https://creativecommons.org/licenses/by/4.0/", - "@type": "CreativeWork", - "identifier": { - "@id": "http://spdx.org/licenses/CC-BY-4.0" - }, - "name": "Creative Commons Attribution 4.0 International", - "version": "4.0" - }, - { - "@id": "http://spdx.org/licenses/Apache-2.0", - "@type": "PropertyValue", - "propertyID": "http://spdx.org/rdf/terms#licenseId", - "name": "spdx", - "value": "Apache-2.0" - }, - { - "@id": "http://spdx.org/licenses/CC0-1.0", - "@type": "PropertyValue", - "propertyID": "http://spdx.org/rdf/terms#licenseId", - "name": "spdx", - "value": "CC0-1.0" - }, - { - "@id": "mapping/", - "@type": "Dataset", - "name": "PROV mapping to Workflow Run Crate", - "description": "Mapping using SKOS and SSSOM", - "hasPart": [ - { - "@id": "mapping/environment.yml" - }, - { - "@id": "mapping/environment.lock.yml" - }, - { - "@id": "mapping/prov-mapping.tsv" - }, - { - "@id": "mapping/prov-mapping.yml" - }, - { - "@id": "mapping/prov-mapping-w-metadata.tsv" - }, - { - "@id": "mapping/prov-mapping.ttl" - }, - { - "@id": "mapping/prov-mapping.rdf" - }, { - "@id": "mapping/prov-mapping.json" + "@id": "mapping/", + "@type": "Dataset", + "name": "PROV mapping to Workflow Run Crate", + "description": "Mapping using SKOS and SSSOM", + "hasPart": [ + { + "@id": "mapping/environment.yml" + }, + { + "@id": "mapping/environment.lock.yml" + }, + { + "@id": "mapping/prov-mapping.tsv" + }, + { + "@id": "mapping/prov-mapping.yml" + }, + { + "@id": "mapping/prov-mapping-w-metadata.tsv" + }, + { + "@id": "mapping/prov-mapping.ttl" + }, + { + "@id": "mapping/prov-mapping.rdf" + }, + { + "@id": "mapping/prov-mapping.json" + } + ] } - ] - } - ] + ] } From dd8e556581fa0d07871bfe589ddacaf39d00e2a5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 4 Jun 2024 18:50:04 +0200 Subject: [PATCH 470/902] fix(profiles/ro-crate): :pencil2: Foldeds -> Directories --- .../profiles/ro-crate/must/2_root_data_entity_metadata.ttl | 4 ++-- 1 file changed, 2 insertions(+), 2 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 2acf674c..9c8a7fa4 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 @@ -81,12 +81,12 @@ ro:RootDataEntityValueRestriction ro:RootDataEntityHasPartValueRestriction a sh:NodeShape ; sh:name "RO-Crate Root Data Entity: `hasPart` value restriction" ; - sh:description "The Root Data Entity MUST be linked to the declared `File` and `Foldeds` instances through the `hasPart` property" ; + sh:description "The Root Data Entity MUST be linked to the declared `File` and `Directories` instances through the `hasPart` property" ; sh:targetClass rocrate:RootDataEntity ; sh:property [ a sh:PropertyShape ; sh:name "RO-Crate Root Data Entity: `hasPart` value restriction" ; - sh:description "Check if Root Data Entity is be linked to the declared `File` and `Foldeds` instances through the `hasPart` property" ; + sh:description "Check if Root Data Entity is be linked to the declared `File` and `Directories` instances through the `hasPart` property" ; sh:path schema_org:hasPart ; sh:or ( [ sh:class rocrate:File ] From dc4e7b4d482ac12f7bc619b826c4a9a44f26039d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 4 Jun 2024 19:51:35 +0200 Subject: [PATCH 471/902] refactor(profiles/ro-crate): :truck: rename file containing shapes --- .../ro-crate/may/{5_license_entity.ttl => 61_license_entity.ttl} | 0 .../must/{5_contextual_entity.ttl => 6_contextual_entity.ttl} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename rocrate_validator/profiles/ro-crate/may/{5_license_entity.ttl => 61_license_entity.ttl} (100%) rename rocrate_validator/profiles/ro-crate/must/{5_contextual_entity.ttl => 6_contextual_entity.ttl} (100%) diff --git a/rocrate_validator/profiles/ro-crate/may/5_license_entity.ttl b/rocrate_validator/profiles/ro-crate/may/61_license_entity.ttl similarity index 100% rename from rocrate_validator/profiles/ro-crate/may/5_license_entity.ttl rename to rocrate_validator/profiles/ro-crate/may/61_license_entity.ttl diff --git a/rocrate_validator/profiles/ro-crate/must/5_contextual_entity.ttl b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl similarity index 100% rename from rocrate_validator/profiles/ro-crate/must/5_contextual_entity.ttl rename to rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl From 123344581068d4c86b5bcf36cd70a00443bf26a2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 4 Jun 2024 19:56:27 +0200 Subject: [PATCH 472/902] feat(profiles/ro-crate): :sparkles: add SHACL rule to identity Web Data Entities --- .../must/5_web_data_entity_metadata.ttl | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl diff --git a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl new file mode 100644 index 00000000..42bbba20 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl @@ -0,0 +1,37 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix owl: . +@prefix rdfs: . +@prefix rocrate: . + + +ro:WebBasedDataEntity a sh:NodeShape, sh:hidden ; + sh:name "WebBased Data Entity: REQUIRED properties" ; + sh:description """A WebBased Data Entity is a `File` identified by an absolute URL""" ; + + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:MediaObject . + FILTER(?this != ro: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 rocrate:WebDataEntity ; + ] . + From 22bb6c0f24600bb2b572e6cb75b17270917b471d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 4 Jun 2024 21:29:57 +0200 Subject: [PATCH 473/902] build(core): :building_construction: add `requests` to the list of dependencies --- poetry.lock | 593 +++++++++++++++++++++++++++++++------------------ pyproject.toml | 1 + 2 files changed, 375 insertions(+), 219 deletions(-) diff --git a/poetry.lock b/poetry.lock index f58b98c3..7ce5ca3c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,13 +13,13 @@ files = [ [[package]] name = "astroid" -version = "3.1.0" +version = "3.2.2" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.8.0" files = [ - {file = "astroid-3.1.0-py3-none-any.whl", hash = "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819"}, - {file = "astroid-3.1.0.tar.gz", hash = "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"}, + {file = "astroid-3.2.2-py3-none-any.whl", hash = "sha256:e8a0083b4bb28fcffb6207a3bfc9e5d0a68be951dd7e336d5dcf639c682388c0"}, + {file = "astroid-3.2.2.tar.gz", hash = "sha256:8ead48e31b92b2e217b6c9733a21afafe479d52d6e164dd25fb1a770c7c3cf94"}, ] [package.dependencies] @@ -54,6 +54,17 @@ files = [ {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, ] +[[package]] +name = "certifi" +version = "2024.6.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, + {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, +] + [[package]] name = "cffi" version = "1.16.0" @@ -118,6 +129,105 @@ files = [ [package.dependencies] pycparser = "*" +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + [[package]] name = "click" version = "8.1.7" @@ -179,63 +289,63 @@ test = ["pytest"] [[package]] name = "coverage" -version = "7.4.4" +version = "7.5.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, - {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, - {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, - {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, - {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, - {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, - {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, - {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, - {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, - {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, - {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, - {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, - {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, - {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, + {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"}, + {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"}, + {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"}, + {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"}, + {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"}, + {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"}, + {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"}, + {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"}, + {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"}, + {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"}, + {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"}, + {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"}, + {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"}, + {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"}, + {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"}, + {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"}, + {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"}, + {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"}, + {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"}, + {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"}, + {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"}, + {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"}, ] [package.dependencies] @@ -303,13 +413,13 @@ profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] [package.extras] @@ -366,6 +476,17 @@ chardet = ["chardet (>=2.2)"] genshi = ["genshi"] lxml = ["lxml"] +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + [[package]] name = "importlib-metadata" version = "7.1.0" @@ -398,13 +519,13 @@ files = [ [[package]] name = "ipykernel" -version = "6.29.3" +version = "6.29.4" description = "IPython Kernel for Jupyter" optional = false python-versions = ">=3.8" files = [ - {file = "ipykernel-6.29.3-py3-none-any.whl", hash = "sha256:5aa086a4175b0229d4eca211e181fb473ea78ffd9869af36ba7694c947302a21"}, - {file = "ipykernel-6.29.3.tar.gz", hash = "sha256:e14c250d1f9ea3989490225cc1a542781b095a18a19447fcf2b5eaf7d0ac5bd2"}, + {file = "ipykernel-6.29.4-py3-none-any.whl", hash = "sha256:1181e653d95c6808039c509ef8e67c4126b3b3af7781496c7cbfb5ed938a27da"}, + {file = "ipykernel-6.29.4.tar.gz", hash = "sha256:3d44070060f9475ac2092b760123fadf105d2e2493c24848b6691a7c4f42af5c"}, ] [package.dependencies] @@ -517,13 +638,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] name = "jupyter-client" -version = "8.6.1" +version = "8.6.2" description = "Jupyter protocol implementation and client libraries" optional = false python-versions = ">=3.8" files = [ - {file = "jupyter_client-8.6.1-py3-none-any.whl", hash = "sha256:3b7bd22f058434e3b9a7ea4b1500ed47de2713872288c0d511d19926f99b459f"}, - {file = "jupyter_client-8.6.1.tar.gz", hash = "sha256:e842515e2bab8e19186d89fdfea7abd15e39dd581f94e399f00e2af5a1652d3f"}, + {file = "jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f"}, + {file = "jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df"}, ] [package.dependencies] @@ -536,7 +657,7 @@ traitlets = ">=5.3" [package.extras] docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] [[package]] name = "jupyter-core" @@ -584,13 +705,13 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "matplotlib-inline" -version = "0.1.6" +version = "0.1.7" description = "Inline Matplotlib backend for Jupyter" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, - {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, ] [package.dependencies] @@ -656,18 +777,18 @@ files = [ [[package]] name = "parso" -version = "0.8.3" +version = "0.8.4" description = "A Python Parser" optional = false python-versions = ">=3.6" files = [ - {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, - {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, + {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, + {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, ] [package.extras] -qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["docopt", "pytest (<6.0.0)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["docopt", "pytest"] [[package]] name = "pexpect" @@ -696,28 +817,29 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -743,13 +865,13 @@ tests = ["pytest", "pytest-cov", "pytest-lazy-fixtures"] [[package]] name = "prompt-toolkit" -version = "3.0.43" +version = "3.0.45" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, - {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, + {file = "prompt_toolkit-3.0.45-py3-none-any.whl", hash = "sha256:a29b89160e494e3ea8622b09fa5897610b437884dcdcd054fdc1308883326c2a"}, + {file = "prompt_toolkit-3.0.45.tar.gz", hash = "sha256:07c60ee4ab7b7e90824b61afa840c8f5aad2d46b3e2e10acc33d8ecc94a49089"}, ] [package.dependencies] @@ -821,13 +943,13 @@ files = [ [[package]] name = "pycparser" -version = "2.21" +version = "2.22" description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] [[package]] @@ -843,32 +965,31 @@ files = [ [[package]] name = "pygments" -version = "2.17.2" +version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] -plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "3.1.0" +version = "3.2.2" description = "python code static checker" optional = false python-versions = ">=3.8.0" files = [ - {file = "pylint-3.1.0-py3-none-any.whl", hash = "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74"}, - {file = "pylint-3.1.0.tar.gz", hash = "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"}, + {file = "pylint-3.2.2-py3-none-any.whl", hash = "sha256:3f8788ab20bb8383e06dd2233e50f8e08949cfd9574804564803441a4946eab4"}, + {file = "pylint-3.2.2.tar.gz", hash = "sha256:d068ca1dfd735fb92a07d33cb8f288adc0f6bc1287a139ca2425366f7cbe38f8"}, ] [package.dependencies] -astroid = ">=3.1.0,<=3.2.0-dev0" +astroid = ">=3.2.2,<=3.3.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, @@ -946,13 +1067,13 @@ js = ["pyduktape2 (>=0.4.6,<0.5.0)"] [[package]] name = "pytest" -version = "8.1.1" +version = "8.2.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, - {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, + {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, + {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, ] [package.dependencies] @@ -960,11 +1081,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.4,<2.0" +pluggy = ">=1.5,<2.0" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" @@ -1023,104 +1144,99 @@ files = [ [[package]] name = "pyzmq" -version = "25.1.2" +version = "26.0.3" description = "Python bindings for 0MQ" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:e624c789359f1a16f83f35e2c705d07663ff2b4d4479bad35621178d8f0f6ea4"}, - {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49151b0efece79f6a79d41a461d78535356136ee70084a1c22532fc6383f4ad0"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9a5f194cf730f2b24d6af1f833c14c10f41023da46a7f736f48b6d35061e76e"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:faf79a302f834d9e8304fafdc11d0d042266667ac45209afa57e5efc998e3872"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f51a7b4ead28d3fca8dda53216314a553b0f7a91ee8fc46a72b402a78c3e43d"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0ddd6d71d4ef17ba5a87becf7ddf01b371eaba553c603477679ae817a8d84d75"}, - {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:246747b88917e4867e2367b005fc8eefbb4a54b7db363d6c92f89d69abfff4b6"}, - {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:00c48ae2fd81e2a50c3485de1b9d5c7c57cd85dc8ec55683eac16846e57ac979"}, - {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a68d491fc20762b630e5db2191dd07ff89834086740f70e978bb2ef2668be08"}, - {file = "pyzmq-25.1.2-cp310-cp310-win32.whl", hash = "sha256:09dfe949e83087da88c4a76767df04b22304a682d6154de2c572625c62ad6886"}, - {file = "pyzmq-25.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:fa99973d2ed20417744fca0073390ad65ce225b546febb0580358e36aa90dba6"}, - {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:82544e0e2d0c1811482d37eef297020a040c32e0687c1f6fc23a75b75db8062c"}, - {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01171fc48542348cd1a360a4b6c3e7d8f46cdcf53a8d40f84db6707a6768acc1"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc69c96735ab501419c432110016329bf0dea8898ce16fab97c6d9106dc0b348"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e124e6b1dd3dfbeb695435dff0e383256655bb18082e094a8dd1f6293114642"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7598d2ba821caa37a0f9d54c25164a4fa351ce019d64d0b44b45540950458840"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d1299d7e964c13607efd148ca1f07dcbf27c3ab9e125d1d0ae1d580a1682399d"}, - {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4e6f689880d5ad87918430957297c975203a082d9a036cc426648fcbedae769b"}, - {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cc69949484171cc961e6ecd4a8911b9ce7a0d1f738fcae717177c231bf77437b"}, - {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9880078f683466b7f567b8624bfc16cad65077be046b6e8abb53bed4eeb82dd3"}, - {file = "pyzmq-25.1.2-cp311-cp311-win32.whl", hash = "sha256:4e5837af3e5aaa99a091302df5ee001149baff06ad22b722d34e30df5f0d9097"}, - {file = "pyzmq-25.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:25c2dbb97d38b5ac9fd15586e048ec5eb1e38f3d47fe7d92167b0c77bb3584e9"}, - {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:11e70516688190e9c2db14fcf93c04192b02d457b582a1f6190b154691b4c93a"}, - {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:313c3794d650d1fccaaab2df942af9f2c01d6217c846177cfcbc693c7410839e"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b3cbba2f47062b85fe0ef9de5b987612140a9ba3a9c6d2543c6dec9f7c2ab27"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc31baa0c32a2ca660784d5af3b9487e13b61b3032cb01a115fce6588e1bed30"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c9087b109070c5ab0b383079fa1b5f797f8d43e9a66c07a4b8b8bdecfd88ee"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f8429b17cbb746c3e043cb986328da023657e79d5ed258b711c06a70c2ea7537"}, - {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5074adeacede5f810b7ef39607ee59d94e948b4fd954495bdb072f8c54558181"}, - {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7ae8f354b895cbd85212da245f1a5ad8159e7840e37d78b476bb4f4c3f32a9fe"}, - {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b264bf2cc96b5bc43ce0e852be995e400376bd87ceb363822e2cb1964fcdc737"}, - {file = "pyzmq-25.1.2-cp312-cp312-win32.whl", hash = "sha256:02bbc1a87b76e04fd780b45e7f695471ae6de747769e540da909173d50ff8e2d"}, - {file = "pyzmq-25.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:ced111c2e81506abd1dc142e6cd7b68dd53747b3b7ae5edbea4578c5eeff96b7"}, - {file = "pyzmq-25.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7b6d09a8962a91151f0976008eb7b29b433a560fde056ec7a3db9ec8f1075438"}, - {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967668420f36878a3c9ecb5ab33c9d0ff8d054f9c0233d995a6d25b0e95e1b6b"}, - {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5edac3f57c7ddaacdb4d40f6ef2f9e299471fc38d112f4bc6d60ab9365445fb0"}, - {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0dabfb10ef897f3b7e101cacba1437bd3a5032ee667b7ead32bbcdd1a8422fe7"}, - {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2c6441e0398c2baacfe5ba30c937d274cfc2dc5b55e82e3749e333aabffde561"}, - {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:16b726c1f6c2e7625706549f9dbe9b06004dfbec30dbed4bf50cbdfc73e5b32a"}, - {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:a86c2dd76ef71a773e70551a07318b8e52379f58dafa7ae1e0a4be78efd1ff16"}, - {file = "pyzmq-25.1.2-cp36-cp36m-win32.whl", hash = "sha256:359f7f74b5d3c65dae137f33eb2bcfa7ad9ebefd1cab85c935f063f1dbb245cc"}, - {file = "pyzmq-25.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:55875492f820d0eb3417b51d96fea549cde77893ae3790fd25491c5754ea2f68"}, - {file = "pyzmq-25.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b8c8a419dfb02e91b453615c69568442e897aaf77561ee0064d789705ff37a92"}, - {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8807c87fa893527ae8a524c15fc505d9950d5e856f03dae5921b5e9aa3b8783b"}, - {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5e319ed7d6b8f5fad9b76daa0a68497bc6f129858ad956331a5835785761e003"}, - {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3c53687dde4d9d473c587ae80cc328e5b102b517447456184b485587ebd18b62"}, - {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9add2e5b33d2cd765ad96d5eb734a5e795a0755f7fc49aa04f76d7ddda73fd70"}, - {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e690145a8c0c273c28d3b89d6fb32c45e0d9605b2293c10e650265bf5c11cfec"}, - {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:00a06faa7165634f0cac1abb27e54d7a0b3b44eb9994530b8ec73cf52e15353b"}, - {file = "pyzmq-25.1.2-cp37-cp37m-win32.whl", hash = "sha256:0f97bc2f1f13cb16905a5f3e1fbdf100e712d841482b2237484360f8bc4cb3d7"}, - {file = "pyzmq-25.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6cc0020b74b2e410287e5942e1e10886ff81ac77789eb20bec13f7ae681f0fdd"}, - {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:bef02cfcbded83473bdd86dd8d3729cd82b2e569b75844fb4ea08fee3c26ae41"}, - {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e10a4b5a4b1192d74853cc71a5e9fd022594573926c2a3a4802020360aa719d8"}, - {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8c5f80e578427d4695adac6fdf4370c14a2feafdc8cb35549c219b90652536ae"}, - {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5dde6751e857910c1339890f3524de74007958557593b9e7e8c5f01cd919f8a7"}, - {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea1608dd169da230a0ad602d5b1ebd39807ac96cae1845c3ceed39af08a5c6df"}, - {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0f513130c4c361201da9bc69df25a086487250e16b5571ead521b31ff6b02220"}, - {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:019744b99da30330798bb37df33549d59d380c78e516e3bab9c9b84f87a9592f"}, - {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e2713ef44be5d52dd8b8e2023d706bf66cb22072e97fc71b168e01d25192755"}, - {file = "pyzmq-25.1.2-cp38-cp38-win32.whl", hash = "sha256:07cd61a20a535524906595e09344505a9bd46f1da7a07e504b315d41cd42eb07"}, - {file = "pyzmq-25.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb7e49a17fb8c77d3119d41a4523e432eb0c6932187c37deb6fbb00cc3028088"}, - {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:94504ff66f278ab4b7e03e4cba7e7e400cb73bfa9d3d71f58d8972a8dc67e7a6"}, - {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6dd0d50bbf9dca1d0bdea219ae6b40f713a3fb477c06ca3714f208fd69e16fd8"}, - {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:004ff469d21e86f0ef0369717351073e0e577428e514c47c8480770d5e24a565"}, - {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c0b5ca88a8928147b7b1e2dfa09f3b6c256bc1135a1338536cbc9ea13d3b7add"}, - {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9a79f1d2495b167119d02be7448bfba57fad2a4207c4f68abc0bab4b92925b"}, - {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:518efd91c3d8ac9f9b4f7dd0e2b7b8bf1a4fe82a308009016b07eaa48681af82"}, - {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1ec23bd7b3a893ae676d0e54ad47d18064e6c5ae1fadc2f195143fb27373f7f6"}, - {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db36c27baed588a5a8346b971477b718fdc66cf5b80cbfbd914b4d6d355e44e2"}, - {file = "pyzmq-25.1.2-cp39-cp39-win32.whl", hash = "sha256:39b1067f13aba39d794a24761e385e2eddc26295826530a8c7b6c6c341584289"}, - {file = "pyzmq-25.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:8e9f3fabc445d0ce320ea2c59a75fe3ea591fdbdeebec5db6de530dd4b09412e"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a8c1d566344aee826b74e472e16edae0a02e2a044f14f7c24e123002dcff1c05"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759cfd391a0996345ba94b6a5110fca9c557ad4166d86a6e81ea526c376a01e8"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c61e346ac34b74028ede1c6b4bcecf649d69b707b3ff9dc0fab453821b04d1e"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cb8fc1f8d69b411b8ec0b5f1ffbcaf14c1db95b6bccea21d83610987435f1a4"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3c00c9b7d1ca8165c610437ca0c92e7b5607b2f9076f4eb4b095c85d6e680a1d"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:df0c7a16ebb94452d2909b9a7b3337940e9a87a824c4fc1c7c36bb4404cb0cde"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:45999e7f7ed5c390f2e87ece7f6c56bf979fb213550229e711e45ecc7d42ccb8"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ac170e9e048b40c605358667aca3d94e98f604a18c44bdb4c102e67070f3ac9b"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1b604734bec94f05f81b360a272fc824334267426ae9905ff32dc2be433ab96"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a793ac733e3d895d96f865f1806f160696422554e46d30105807fdc9841b9f7d"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0806175f2ae5ad4b835ecd87f5f85583316b69f17e97786f7443baaf54b9bb98"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ef12e259e7bc317c7597d4f6ef59b97b913e162d83b421dd0db3d6410f17a244"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea253b368eb41116011add00f8d5726762320b1bda892f744c91997b65754d73"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b9b1f2ad6498445a941d9a4fee096d387fee436e45cc660e72e768d3d8ee611"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8b14c75979ce932c53b79976a395cb2a8cd3aaf14aef75e8c2cb55a330b9b49d"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:889370d5174a741a62566c003ee8ddba4b04c3f09a97b8000092b7ca83ec9c49"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a18fff090441a40ffda8a7f4f18f03dc56ae73f148f1832e109f9bffa85df15"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99a6b36f95c98839ad98f8c553d8507644c880cf1e0a57fe5e3a3f3969040882"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4345c9a27f4310afbb9c01750e9461ff33d6fb74cd2456b107525bbeebcb5be3"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3516e0b6224cf6e43e341d56da15fd33bdc37fa0c06af4f029f7d7dfceceabbc"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:146b9b1f29ead41255387fb07be56dc29639262c0f7344f570eecdcd8d683314"}, - {file = "pyzmq-25.1.2.tar.gz", hash = "sha256:93f1aa311e8bb912e34f004cf186407a4e90eec4f0ecc0efd26056bf7eda0226"}, + {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:44dd6fc3034f1eaa72ece33588867df9e006a7303725a12d64c3dff92330f625"}, + {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:acb704195a71ac5ea5ecf2811c9ee19ecdc62b91878528302dd0be1b9451cc90"}, + {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbb9c997932473a27afa93954bb77a9f9b786b4ccf718d903f35da3232317de"}, + {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bcb34f869d431799c3ee7d516554797f7760cb2198ecaa89c3f176f72d062be"}, + {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ece17ec5f20d7d9b442e5174ae9f020365d01ba7c112205a4d59cf19dc38ee"}, + {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ba6e5e6588e49139a0979d03a7deb9c734bde647b9a8808f26acf9c547cab1bf"}, + {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3bf8b000a4e2967e6dfdd8656cd0757d18c7e5ce3d16339e550bd462f4857e59"}, + {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2136f64fbb86451dbbf70223635a468272dd20075f988a102bf8a3f194a411dc"}, + {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e8918973fbd34e7814f59143c5f600ecd38b8038161239fd1a3d33d5817a38b8"}, + {file = "pyzmq-26.0.3-cp310-cp310-win32.whl", hash = "sha256:0aaf982e68a7ac284377d051c742610220fd06d330dcd4c4dbb4cdd77c22a537"}, + {file = "pyzmq-26.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:f1a9b7d00fdf60b4039f4455afd031fe85ee8305b019334b72dcf73c567edc47"}, + {file = "pyzmq-26.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:80b12f25d805a919d53efc0a5ad7c0c0326f13b4eae981a5d7b7cc343318ebb7"}, + {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:a72a84570f84c374b4c287183debc776dc319d3e8ce6b6a0041ce2e400de3f32"}, + {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ca684ee649b55fd8f378127ac8462fb6c85f251c2fb027eb3c887e8ee347bcd"}, + {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e222562dc0f38571c8b1ffdae9d7adb866363134299264a1958d077800b193b7"}, + {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f17cde1db0754c35a91ac00b22b25c11da6eec5746431d6e5092f0cd31a3fea9"}, + {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7c0c0b3244bb2275abe255d4a30c050d541c6cb18b870975553f1fb6f37527"}, + {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac97a21de3712afe6a6c071abfad40a6224fd14fa6ff0ff8d0c6e6cd4e2f807a"}, + {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:88b88282e55fa39dd556d7fc04160bcf39dea015f78e0cecec8ff4f06c1fc2b5"}, + {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:72b67f966b57dbd18dcc7efbc1c7fc9f5f983e572db1877081f075004614fcdd"}, + {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4b6cecbbf3b7380f3b61de3a7b93cb721125dc125c854c14ddc91225ba52f83"}, + {file = "pyzmq-26.0.3-cp311-cp311-win32.whl", hash = "sha256:eed56b6a39216d31ff8cd2f1d048b5bf1700e4b32a01b14379c3b6dde9ce3aa3"}, + {file = "pyzmq-26.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:3191d312c73e3cfd0f0afdf51df8405aafeb0bad71e7ed8f68b24b63c4f36500"}, + {file = "pyzmq-26.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:b6907da3017ef55139cf0e417c5123a84c7332520e73a6902ff1f79046cd3b94"}, + {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:068ca17214038ae986d68f4a7021f97e187ed278ab6dccb79f837d765a54d753"}, + {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7821d44fe07335bea256b9f1f41474a642ca55fa671dfd9f00af8d68a920c2d4"}, + {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb438a26d87c123bb318e5f2b3d86a36060b01f22fbdffd8cf247d52f7c9a2b"}, + {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69ea9d6d9baa25a4dc9cef5e2b77b8537827b122214f210dd925132e34ae9b12"}, + {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7daa3e1369355766dea11f1d8ef829905c3b9da886ea3152788dc25ee6079e02"}, + {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6ca7a9a06b52d0e38ccf6bca1aeff7be178917893f3883f37b75589d42c4ac20"}, + {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1b7d0e124948daa4d9686d421ef5087c0516bc6179fdcf8828b8444f8e461a77"}, + {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e746524418b70f38550f2190eeee834db8850088c834d4c8406fbb9bc1ae10b2"}, + {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6b3146f9ae6af82c47a5282ac8803523d381b3b21caeae0327ed2f7ecb718798"}, + {file = "pyzmq-26.0.3-cp312-cp312-win32.whl", hash = "sha256:2b291d1230845871c00c8462c50565a9cd6026fe1228e77ca934470bb7d70ea0"}, + {file = "pyzmq-26.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:926838a535c2c1ea21c903f909a9a54e675c2126728c21381a94ddf37c3cbddf"}, + {file = "pyzmq-26.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:5bf6c237f8c681dfb91b17f8435b2735951f0d1fad10cc5dfd96db110243370b"}, + {file = "pyzmq-26.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c0991f5a96a8e620f7691e61178cd8f457b49e17b7d9cfa2067e2a0a89fc1d5"}, + {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dbf012d8fcb9f2cf0643b65df3b355fdd74fc0035d70bb5c845e9e30a3a4654b"}, + {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:01fbfbeb8249a68d257f601deb50c70c929dc2dfe683b754659569e502fbd3aa"}, + {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c8eb19abe87029c18f226d42b8a2c9efdd139d08f8bf6e085dd9075446db450"}, + {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5344b896e79800af86ad643408ca9aa303a017f6ebff8cee5a3163c1e9aec987"}, + {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:204e0f176fd1d067671157d049466869b3ae1fc51e354708b0dc41cf94e23a3a"}, + {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a42db008d58530efa3b881eeee4991146de0b790e095f7ae43ba5cc612decbc5"}, + {file = "pyzmq-26.0.3-cp37-cp37m-win32.whl", hash = "sha256:8d7a498671ca87e32b54cb47c82a92b40130a26c5197d392720a1bce1b3c77cf"}, + {file = "pyzmq-26.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:3b4032a96410bdc760061b14ed6a33613ffb7f702181ba999df5d16fb96ba16a"}, + {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2cc4e280098c1b192c42a849de8de2c8e0f3a84086a76ec5b07bfee29bda7d18"}, + {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bde86a2ed3ce587fa2b207424ce15b9a83a9fa14422dcc1c5356a13aed3df9d"}, + {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34106f68e20e6ff253c9f596ea50397dbd8699828d55e8fa18bd4323d8d966e6"}, + {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ebbbd0e728af5db9b04e56389e2299a57ea8b9dd15c9759153ee2455b32be6ad"}, + {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b1d1c631e5940cac5a0b22c5379c86e8df6a4ec277c7a856b714021ab6cfad"}, + {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e891ce81edd463b3b4c3b885c5603c00141151dd9c6936d98a680c8c72fe5c67"}, + {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9b273ecfbc590a1b98f014ae41e5cf723932f3b53ba9367cfb676f838038b32c"}, + {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b32bff85fb02a75ea0b68f21e2412255b5731f3f389ed9aecc13a6752f58ac97"}, + {file = "pyzmq-26.0.3-cp38-cp38-win32.whl", hash = "sha256:f6c21c00478a7bea93caaaef9e7629145d4153b15a8653e8bb4609d4bc70dbfc"}, + {file = "pyzmq-26.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:3401613148d93ef0fd9aabdbddb212de3db7a4475367f49f590c837355343972"}, + {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:2ed8357f4c6e0daa4f3baf31832df8a33334e0fe5b020a61bc8b345a3db7a606"}, + {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1c8f2a2ca45292084c75bb6d3a25545cff0ed931ed228d3a1810ae3758f975f"}, + {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b63731993cdddcc8e087c64e9cf003f909262b359110070183d7f3025d1c56b5"}, + {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b3cd31f859b662ac5d7f4226ec7d8bd60384fa037fc02aee6ff0b53ba29a3ba8"}, + {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115f8359402fa527cf47708d6f8a0f8234f0e9ca0cab7c18c9c189c194dbf620"}, + {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:715bdf952b9533ba13dfcf1f431a8f49e63cecc31d91d007bc1deb914f47d0e4"}, + {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e1258c639e00bf5e8a522fec6c3eaa3e30cf1c23a2f21a586be7e04d50c9acab"}, + {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15c59e780be8f30a60816a9adab900c12a58d79c1ac742b4a8df044ab2a6d920"}, + {file = "pyzmq-26.0.3-cp39-cp39-win32.whl", hash = "sha256:d0cdde3c78d8ab5b46595054e5def32a755fc028685add5ddc7403e9f6de9879"}, + {file = "pyzmq-26.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ce828058d482ef860746bf532822842e0ff484e27f540ef5c813d516dd8896d2"}, + {file = "pyzmq-26.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:788f15721c64109cf720791714dc14afd0f449d63f3a5487724f024345067381"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c18645ef6294d99b256806e34653e86236eb266278c8ec8112622b61db255de"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e6bc96ebe49604df3ec2c6389cc3876cabe475e6bfc84ced1bf4e630662cb35"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:971e8990c5cc4ddcff26e149398fc7b0f6a042306e82500f5e8db3b10ce69f84"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8416c23161abd94cc7da80c734ad7c9f5dbebdadfdaa77dad78244457448223"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:082a2988364b60bb5de809373098361cf1dbb239623e39e46cb18bc035ed9c0c"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d57dfbf9737763b3a60d26e6800e02e04284926329aee8fb01049635e957fe81"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77a85dca4c2430ac04dc2a2185c2deb3858a34fe7f403d0a946fa56970cf60a1"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4c82a6d952a1d555bf4be42b6532927d2a5686dd3c3e280e5f63225ab47ac1f5"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4496b1282c70c442809fc1b151977c3d967bfb33e4e17cedbf226d97de18f709"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e4946d6bdb7ba972dfda282f9127e5756d4f299028b1566d1245fa0d438847e6"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03c0ae165e700364b266876d712acb1ac02693acd920afa67da2ebb91a0b3c09"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3e3070e680f79887d60feeda051a58d0ac36622e1759f305a41059eff62c6da7"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6ca08b840fe95d1c2bd9ab92dac5685f949fc6f9ae820ec16193e5ddf603c3b2"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e76654e9dbfb835b3518f9938e565c7806976c07b37c33526b574cc1a1050480"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:871587bdadd1075b112e697173e946a07d722459d20716ceb3d1bd6c64bd08ce"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d0a2d1bd63a4ad79483049b26514e70fa618ce6115220da9efdff63688808b17"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0270b49b6847f0d106d64b5086e9ad5dc8a902413b5dbbb15d12b60f9c1747a4"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:703c60b9910488d3d0954ca585c34f541e506a091a41930e663a098d3b794c67"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74423631b6be371edfbf7eabb02ab995c2563fee60a80a30829176842e71722a"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4adfbb5451196842a88fda3612e2c0414134874bffb1c2ce83ab4242ec9e027d"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3516119f4f9b8671083a70b6afaa0a070f5683e431ab3dc26e9215620d7ca1ad"}, + {file = "pyzmq-26.0.3.tar.gz", hash = "sha256:dba7d9f2e047dfa2bca3b01f4f84aa5246725203d6284e3790f2ca15fba6b40a"}, ] [package.dependencies] @@ -1147,6 +1263,27 @@ html = ["html5lib (>=1.0,<2.0)"] lxml = ["lxml (>=4.3.0,<5.0.0)"] networkx = ["networkx (>=2.0.0,<3.0.0)"] +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "rich" version = "13.7.1" @@ -1168,22 +1305,23 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rich-click" -version = "1.7.4" +version = "1.8.2" description = "Format click help output nicely with rich" optional = false python-versions = ">=3.7" files = [ - {file = "rich-click-1.7.4.tar.gz", hash = "sha256:7ce5de8e4dc0333aec946113529b3eeb349f2e5d2fafee96b9edf8ee36a01395"}, - {file = "rich_click-1.7.4-py3-none-any.whl", hash = "sha256:e363655475c60fec5a3e16a1eb618118ed79e666c365a36006b107c17c93ac4e"}, + {file = "rich_click-1.8.2-py3-none-any.whl", hash = "sha256:b57856f304e4fe0394b82d7ce0784450758f8c8b4e201ccc4320501cc201806b"}, + {file = "rich_click-1.8.2.tar.gz", hash = "sha256:8e29bdede858b59aa2859a1ab1c4ccbd39ed7ed5870262dae756fba6b5dc72e8"}, ] [package.dependencies] click = ">=7" -rich = ">=10.7.0" +rich = ">=10.7" typing-extensions = "*" [package.extras] -dev = ["flake8", "flake8-docstrings", "mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "types-setuptools"] +dev = ["mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "rich-codex", "ruff", "types-setuptools"] +docs = ["markdown-include", "mkdocs", "mkdocs-glightbox", "mkdocs-material-extensions", "mkdocs-material[imaging] (>=9.5.18,<9.6.0)", "mkdocs-rss-plugin", "mkdocstrings[python]", "rich-codex"] [[package]] name = "six" @@ -1239,13 +1377,13 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.4" +version = "0.12.5" description = "Style preserving TOML library" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, - {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, + {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, + {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, ] [[package]] @@ -1270,30 +1408,47 @@ files = [ [[package]] name = "traitlets" -version = "5.14.2" +version = "5.14.3" description = "Traitlets Python configuration system" optional = false python-versions = ">=3.8" files = [ - {file = "traitlets-5.14.2-py3-none-any.whl", hash = "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80"}, - {file = "traitlets-5.14.2.tar.gz", hash = "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9"}, + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, ] [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.1)", "pytest-mock", "pytest-mypy-testing"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] [[package]] name = "typing-extensions" -version = "4.10.0" +version = "4.12.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, + {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, + {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, ] +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "wcwidth" version = "0.2.13" @@ -1318,20 +1473,20 @@ files = [ [[package]] name = "zipp" -version = "3.18.1" +version = "3.19.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, - {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, + {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, + {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "814f72a53f7fe0ca98bde407c1501fd12201b7c040b1c68fd6737a666988fefd" +content-hash = "440400ea408f3383bedded6191b9298acf6192ed4738b1642cfa06adacba25cb" diff --git a/pyproject.toml b/pyproject.toml index 44d7d160..28f58dc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ rich = "^13.7.1" toml = "^0.10.2" rich-click = "^1.7.3" colorlog = "^6.8" +requests = "^2.32.3" [tool.poetry.group.dev.dependencies] pyproject-flake8 = "^6.1.0" From dfdb04f0a2a056a2943e89221bbeec9cf11109fd Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 00:07:59 +0200 Subject: [PATCH 474/902] feat(profiles/ro-crate): :sparkles: add shape to check recommended properties of WebData entities --- .../must/5_web_data_entity_metadata.ttl | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl index 42bbba20..76ea6adf 100644 --- a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl @@ -35,3 +35,31 @@ ro:WebBasedDataEntity a sh:NodeShape, sh:hidden ; sh:object rocrate:WebDataEntity ; ] . + +ro:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; + sh:name "WebBased Data Entity: RECOMMENDED properties" ; + sh:description """A WebBased Data Entity MUST be identified by an absolute URL and + SHOULD have a `contentSize` and `sdDatePublished` property""" ; + sh:targetClass rocrate:WebDataEntity ; + # Check if the WebBased Data Entity has a contentSize property + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "WebBased Data Entity: RECOMMENDED `contentSize` property" ; + sh:description """Check if the WebBased Data Entity has a `contentSize` property""" ; + sh:path schema_org:contentSize ; + sh:datatype xsd:int ; + sh:severity sh:Warning ; + sh:message """WebBased Data Entities SHOULD have a `contentSize` property""" ; + ] ; + # Check if the WebBased Data Entity has a sdDatePublished property + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "WebBased Data Entity: RECOMMENDED `sdDatePublished` property" ; + sh:description """Check if the WebBased Data Entity has a `sdSatePublished` property""" ; + sh:path schema_org:sdDatePublished ; + sh:datatype xsd:date ; + sh:severity sh:Warning ; + sh:message """WebBased Data Entities SHOULD have a `sdDatePublished` property""" ; + ] . From ee328c1857b741a89f94e9f5e096a176bbbde97a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 00:09:24 +0200 Subject: [PATCH 475/902] feat(shacl): :sparkles: add missing shape related to DataEntity --- .../ro-crate/may/4_data_entity_metadata.ttl | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl diff --git a/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl new file mode 100644 index 00000000..19cfd25b --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl @@ -0,0 +1,26 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix owl: . +@prefix rdfs: . +@prefix rocrate: . + + +ro: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 sbsolute URI""" ; + sh:targetClass rocrate: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.""" ; + ] . From 1a074dbd71eb9243885cb81e39ac5887b34ff8ef Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 00:10:42 +0200 Subject: [PATCH 476/902] feat(profiles/ro-crate): :sparkles: add py-checks for the WebData entity --- .../ro-crate/should/5_web_data_entity.py | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 rocrate_validator/profiles/ro-crate/should/5_web_data_entity.py diff --git a/rocrate_validator/profiles/ro-crate/should/5_web_data_entity.py b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity.py new file mode 100644 index 00000000..75fffe51 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity.py @@ -0,0 +1,179 @@ +import json +from typing import Optional + +import requests + +import rocrate_validator.log as logging +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import (PyFunctionCheck, check, + requirement) + +# set up logging +logger = logging.getLogger(__name__) + + +class WebDataEntity: + + def __init__(self, entity: dict): + self._data = entity + self._http_response = None + self._download_exception = None + + @property + def data(self) -> dict: + return self._data.copy() + + @property + def id(self) -> str: + return self._data.get("@id", "") + + def get_property(self, property_name: str) -> dict: + return self._data.get(property_name, None) + + def is_web_data_entity(self) -> bool: + return self._data.get("@id", "").startswith(("http", "https")) + + def is_ftp_data_entity(self) -> bool: + return self._data.get("@id", "").startswith(("ftp", "ftps")) + + def download_entity(self) -> Optional[bytes]: + if not self.is_downloadable(): + return None + return self._http_response.content + + def is_downloadable(self) -> bool: + if not self.is_web_data_entity(): + return False + # if self.is_ftp_data_entity() and not self._data.get("@id", "").startswith(("ftp://", "ftps://")): + # return False + try: + if self._http_response is None: + self._http_response = requests.get(self._data.get("@id", ""), allow_redirects=True) + return self._http_response and self._http_response.status_code == 200 + except Exception as e: + self._download_exception = e + return False + + def get_content_size(self) -> Optional[int]: + if not self.is_downloadable(): + return None + return len(self._http_response.content) + + def get_http_response(self) -> Optional[requests.Response]: + return self._http_response + + def get_download_exception(self) -> Optional[Exception]: + return self._download_exception + + +@requirement(name="Web Data Entity: RECOMMENDED resource availability") +class WebDataEntityRecommendedChecker(PyFunctionCheck): + """ + Web Data Entity instances SHOULD be available at the URIs specified in the `@id` property of the Web Data Entity. + """ + + _json_dict_cache: Optional[dict] = None + _resources_cache: dict[str, requests.Response] = {} + + def get_json_dict(self, context: ValidationContext) -> dict: + if self._json_dict_cache is None or \ + self._json_dict_cache['file_descriptor_path'] != context.file_descriptor_path: + # invalid cache + try: + with open(context.file_descriptor_path, "r") as file: + self._json_dict_cache = dict( + json=json.load(file), + file_descriptor_path=context.file_descriptor_path) + except Exception as e: + context.result.add_error( + f'file descriptor "{context.rel_fd_path}" is not in the correct format', self) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return {} + return self._json_dict_cache['json'] + + def find_entity(self, context: ValidationContext, entity_id: str) -> dict: + json_dict = self.get_json_dict(context) + for entity in json_dict["@graph"]: + if entity["@id"] == entity_id: + return entity + return {} + + def find_property(self, context: ValidationContext, entity_id: str, property_name: str) -> dict: + entity = self.find_entity(context, entity_id) + if entity: + return entity.get(property_name, {}) + return {} + + def get_resource(self, context: ValidationContext, entity_id: str) -> Optional[requests.Response]: + response = self._resources_cache.get(entity_id, None) + if response is None: + response = requests.get(entity_id, allow_redirects=True) + self._resources_cache[entity_id] = response + return response + + def get_web_data_entities(self, context: ValidationContext) -> list[WebDataEntity]: + json_dict = self.get_json_dict(context) + web_data_entities = [] + for entity in json_dict["@graph"]: + entity_id = entity.get("@id", None) + if entity_id is not None and entity_id.startswith(("http", "https")): + web_data_entities.append(WebDataEntity(entity)) + return web_data_entities + + @check(name="Web Data Entity availability") + def check_availability(self, context: ValidationContext) -> bool: + """ + Check if the Web Data Entity is directly downloadable + by a simple retrieval (e.g. HTTP GET) permitting redirection and HTTP/HTTPS URIs + """ + result = True + for entity in self.get_web_data_entities(context): + assert entity.id is not None, "Entity has no @id" + logger.error("Is a web data entity") + try: + if not entity.is_downloadable(): + + response = entity.get_http_response() + if response is None: + context.result.add_error( + f'Web Data Entity {entity.id} is not available', self) + result = False + elif response.status_code != 200: + context.result.add_error( + f'Web Data Entity {entity.id} is not available (HTTP {response.status_code})', self) + result = False + except Exception as e: + context.result.add_error( + f'Web Data Entity {entity.id} is not available: {e}', self) + result = False + if not result and context.fail_fast: + return result + return result + + @check(name="Web Data Entity: `contentSize` property") + def check_content_size(self, context: ValidationContext) -> bool: + """ + Check if the Web Data Entity has a `contentSize` property + and if it is set to actual size of the downloadable content + """ + result = True + for entity in self.get_web_data_entities(context): + assert entity.id is not None, "Entity has no @id" + if entity.is_downloadable(): + response = entity.get_http_response() + if response is not None: + content_size = entity.get_property("contentSize") + if content_size is None: + context.result.add_error( + f'Web Data Entity {entity.id} has no `contentSize` property', self) + result = False + elif content_size != entity.get_content_size(): + context.result.add_error( + f'Web Data Entity {entity.id} `contentSize={content_size}` property does not match the actual size of ' + f'the downloadable content, i.e., `{entity.get_content_size()}`bytes', self) + result = False + if not result and context.fail_fast: + return result + + return result From ec1ba879862f9e300393391c67e5c0b8f84efee4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 12:48:34 +0200 Subject: [PATCH 477/902] feat(core): :sparkles: inject resultPath of violation result on check issue instances --- rocrate_validator/models.py | 6 ++++++ rocrate_validator/requirements/shacl/checks.py | 1 + 2 files changed, 7 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 5bfe4c89..cdae30c2 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -626,6 +626,7 @@ class CheckIssue: def __init__(self, severity: Severity, check: RequirementCheck, message: Optional[str] = None, + resultPath: Optional[str] = None, focusNode: Optional[str] = None, value: Optional[str] = None): if not isinstance(severity, Severity): @@ -633,6 +634,7 @@ def __init__(self, severity: Severity, self._severity = severity self._message = message self._check: RequirementCheck = check + self._resultPath = resultPath self._focusNode = focusNode self._value = value @@ -660,6 +662,10 @@ def check(self) -> RequirementCheck: """The check that generated the issue""" return self._check + @property + def resultPath(self) -> Optional[str]: + return self._resultPath + @property def focusNode(self) -> Optional[str]: return self._focusNode diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index d210f0c9..a1670d92 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -95,6 +95,7 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): c = shacl_context.result.add_check_issue(message=violation.get_result_message(shacl_context.rocrate_path), check=requirementCheck, severity=violation.get_result_severity(), + resultPath=violation.resultPath.toPython() if violation.resultPath else None, focusNode=make_uris_relative( violation.focusNode.toPython(), shacl_context.rocrate_path), value=violation.value) From b2bc72f9bfdd14a6eb337ff7388e4c292362b2bc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 12:49:41 +0200 Subject: [PATCH 478/902] feat(cli): :lipstick: add resultPath on CLI validation output --- rocrate_validator/cli/commands/validate.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index eaef7b9e..6fe30ba4 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -166,18 +166,27 @@ def __print_validation_result__( ) console.print(f"\n{' '*4}{requirement.description}\n", style="white italic") - console.print(f"{' '*4}Failed checks:\n", style="white bold") + console.print(f"{' '*4}[cyan u]Failed checks[/cyan u]:\n", style="white bold") for check in sorted(result.get_failed_checks_by_requirement(requirement), key=lambda x: (-x.severity.value, x)): issue_color = get_severity_color(check.level.severity) console.print( f"{' '*4}- " f"[magenta bold]{check.name}[/magenta bold]: {check.description}") - console.print(f"\n{' '*6}Detected issues:", style="white bold") + console.print(f"\n{' '*6}[u cyan]Detected issues[/u cyan]:", style="white bold") for issue in sorted(result.get_issues_by_check(check), key=lambda x: (-x.severity.value, x)): - actual_value = f"value \"[green]{issue.value}[/green]\" of " if issue.value else "" + path = "" + if issue.resultPath and issue.value: + path = f"on [yellow]{issue.resultPath}[/yellow]" + if issue.value: + if issue.resultPath: + path += "=" + path += f"\"[green]{issue.value}[/green]\"" + # if len(path) > 0: + path = path + " of " if len(path) > 0 else "on " console.print( - f"{' '*6}- [[red]Violation[/red] of " - f"[{issue_color} bold]{issue.check.identifier}[/{issue_color} bold] on {actual_value}[cyan]<{issue.focusNode}>[/cyan]]: {issue.message}",) + f"\n{' ' * 6}- [[red]Violation[/red] of " + f"[{issue_color} bold]{issue.check.identifier}[/{issue_color} bold] {path}[cyan]<{issue.focusNode}>[/cyan]]: " + f"{issue.message}",) console.print("\n", style="white") From ef10a9476cadc50f2739c3f4ea1aed60bef91a60 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 12:51:15 +0200 Subject: [PATCH 479/902] fix(core): :bug: missing property injection --- rocrate_validator/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index cdae30c2..339c6e70 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -761,10 +761,11 @@ def add_check_issue(self, message: str, check: RequirementCheck, severity: Optional[Severity] = None, + resultPath: Optional[str] = None, focusNode: Optional[str] = None, value: Optional[str] = None) -> CheckIssue: sev_value = severity if severity is not None else check.requirement.severity - c = CheckIssue(sev_value, check, message, focusNode=focusNode, value=value) + c = CheckIssue(sev_value, check, message, resultPath=resultPath, focusNode=focusNode, value=value) # self._issues.append(c) bisect.insort(self._issues, c) return c From e8410eeec96122919e2825b432e27f1a9673fd0d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 14:53:40 +0200 Subject: [PATCH 480/902] fix(profiles/ro-crate): :bug: improve validation of contentSize check --- .../must/5_web_data_entity_metadata.ttl | 16 +++- .../ro-crate/should/5_web_data_entity.py | 88 +++++++++---------- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl index 76ea6adf..ace02ca2 100644 --- a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl @@ -45,12 +45,24 @@ ro:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; sh:property [ a sh:PropertyShape ; sh:minCount 1 ; - sh:name "WebBased Data Entity: RECOMMENDED `contentSize` property" ; + sh:name "WebBased Data Entity: RECOMMENDED `contentSize` property string" ; sh:description """Check if the WebBased Data Entity has a `contentSize` property""" ; sh:path schema_org:contentSize ; - sh:datatype xsd:int ; + sh:datatype xsd:string ; sh:severity sh:Warning ; sh:message """WebBased Data Entities SHOULD have a `contentSize` property""" ; + sh:sparql [ + sh:message "If the value is a string it must be a string representing an integer." ; + sh:select """ + SELECT ?this ?value + WHERE { + ?this schema:contentSize ?value . + FILTER NOT EXISTS { + FILTER (xsd:integer(?value) = ?value) + } + } + """ ; + ] ; ] ; # Check if the WebBased Data Entity has a sdDatePublished property sh:property [ diff --git a/rocrate_validator/profiles/ro-crate/should/5_web_data_entity.py b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity.py index 75fffe51..63141f60 100644 --- a/rocrate_validator/profiles/ro-crate/should/5_web_data_entity.py +++ b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity.py @@ -1,4 +1,6 @@ +import http import json +import urllib.request from typing import Optional import requests @@ -16,7 +18,7 @@ class WebDataEntity: def __init__(self, entity: dict): self._data = entity - self._http_response = None + self._remote_site = None self._download_exception = None @property @@ -36,35 +38,39 @@ def is_web_data_entity(self) -> bool: def is_ftp_data_entity(self) -> bool: return self._data.get("@id", "").startswith(("ftp", "ftps")) - def download_entity(self) -> Optional[bytes]: - if not self.is_downloadable(): - return None - return self._http_response.content + @property + def remote_resource(self) -> http.client.HTTPResponse: + if not self._remote_site: + self._remote_site = urllib.request.urlopen(self.id) + return self._remote_site - def is_downloadable(self) -> bool: + @property + def content_size(self) -> Optional[int]: + r = self.remote_resource + if not r: + return -1 + try: + return int(r.info().get('Content-Length')) + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return -1 + + def is_downloadable(self, silent: bool = True) -> bool: if not self.is_web_data_entity(): return False # if self.is_ftp_data_entity() and not self._data.get("@id", "").startswith(("ftp://", "ftps://")): # return False try: - if self._http_response is None: - self._http_response = requests.get(self._data.get("@id", ""), allow_redirects=True) - return self._http_response and self._http_response.status_code == 200 + remote = self.remote_resource + return remote and remote.status == 200 except Exception as e: - self._download_exception = e + logger.exception(e) + # if not silent raise the exception + if not silent: + raise e return False - def get_content_size(self) -> Optional[int]: - if not self.is_downloadable(): - return None - return len(self._http_response.content) - - def get_http_response(self) -> Optional[requests.Response]: - return self._http_response - - def get_download_exception(self) -> Optional[Exception]: - return self._download_exception - @requirement(name="Web Data Entity: RECOMMENDED resource availability") class WebDataEntityRecommendedChecker(PyFunctionCheck): @@ -105,18 +111,18 @@ def find_property(self, context: ValidationContext, entity_id: str, property_nam return entity.get(property_name, {}) return {} - def get_resource(self, context: ValidationContext, entity_id: str) -> Optional[requests.Response]: - response = self._resources_cache.get(entity_id, None) - if response is None: - response = requests.get(entity_id, allow_redirects=True) - self._resources_cache[entity_id] = response - return response - def get_web_data_entities(self, context: ValidationContext) -> list[WebDataEntity]: json_dict = self.get_json_dict(context) web_data_entities = [] for entity in json_dict["@graph"]: entity_id = entity.get("@id", None) + entity_type = entity.get("@type", None) + # Skip entity if it is not a File + if isinstance(entity_type, list): + if not "File" in entity_type: + continue + if not "File" in entity_type: + continue if entity_id is not None and entity_id.startswith(("http", "https")): web_data_entities.append(WebDataEntity(entity)) return web_data_entities @@ -132,14 +138,13 @@ def check_availability(self, context: ValidationContext) -> bool: assert entity.id is not None, "Entity has no @id" logger.error("Is a web data entity") try: - if not entity.is_downloadable(): - - response = entity.get_http_response() + if not entity.is_downloadable(silent=False): + response = entity.remote_resource if response is None: context.result.add_error( f'Web Data Entity {entity.id} is not available', self) result = False - elif response.status_code != 200: + elif response.status != 200: context.result.add_error( f'Web Data Entity {entity.id} is not available (HTTP {response.status_code})', self) result = False @@ -161,18 +166,13 @@ def check_content_size(self, context: ValidationContext) -> bool: for entity in self.get_web_data_entities(context): assert entity.id is not None, "Entity has no @id" if entity.is_downloadable(): - response = entity.get_http_response() - if response is not None: - content_size = entity.get_property("contentSize") - if content_size is None: - context.result.add_error( - f'Web Data Entity {entity.id} has no `contentSize` property', self) - result = False - elif content_size != entity.get_content_size(): - context.result.add_error( - f'Web Data Entity {entity.id} `contentSize={content_size}` property does not match the actual size of ' - f'the downloadable content, i.e., `{entity.get_content_size()}`bytes', self) - result = False + content_size = entity.get_property("contentSize") + if content_size and int(content_size) != entity.content_size: + context.result.add_check_issue( + f'The property contentSize={content_size} of the Web Data Entity {entity.id} does not match the actual size of ' + f'the downloadable content, i.e., {entity.content_size} (bytes)', self, + focusNode=entity.id, resultPath='contentSize', value=content_size) + result = False if not result and context.fail_fast: return result From 99415fcff52466c6c07435f4fcfe7aed50cef9d6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 14:58:01 +0200 Subject: [PATCH 481/902] refactor(cli): :lipstick: minor update of CLI validation output --- rocrate_validator/cli/commands/validate.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 6fe30ba4..974b900e 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -173,7 +173,7 @@ def __print_validation_result__( console.print( f"{' '*4}- " f"[magenta bold]{check.name}[/magenta bold]: {check.description}") - console.print(f"\n{' '*6}[u cyan]Detected issues[/u cyan]:", style="white bold") + console.print(f"\n{' '*6}[u]Detected issues[/u]:", style="white bold") for issue in sorted(result.get_issues_by_check(check), key=lambda x: (-x.severity.value, x)): path = "" @@ -183,10 +183,9 @@ def __print_validation_result__( if issue.resultPath: path += "=" path += f"\"[green]{issue.value}[/green]\"" - # if len(path) > 0: path = path + " of " if len(path) > 0 else "on " console.print( - f"\n{' ' * 6}- [[red]Violation[/red] of " + f"{' ' * 6}- [[red]Violation[/red] of " f"[{issue_color} bold]{issue.check.identifier}[/{issue_color} bold] {path}[cyan]<{issue.focusNode}>[/cyan]]: " f"{issue.message}",) console.print("\n", style="white") From cb01dbe4d2fef90f3818f8571c98ff1fb1407131 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 15:05:31 +0200 Subject: [PATCH 482/902] fix(profiles/ro-crate): :pencil2: fix sdDatePublished --- .../profiles/ro-crate/must/5_web_data_entity_metadata.ttl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl index ace02ca2..f505553f 100644 --- a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl @@ -69,7 +69,7 @@ ro:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; a sh:PropertyShape ; sh:minCount 1 ; sh:name "WebBased Data Entity: RECOMMENDED `sdDatePublished` property" ; - sh:description """Check if the WebBased Data Entity has a `sdSatePublished` property""" ; + sh:description """Check if the WebBased Data Entity has a `sdDatePublished` property""" ; sh:path schema_org:sdDatePublished ; sh:datatype xsd:date ; sh:severity sh:Warning ; From 79b343c7d799021c281324009baf61baf2fbd104 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 15:12:13 +0200 Subject: [PATCH 483/902] fix(profiles/ro-crate): :sparkles: custom pattern to validate datePublished of WebData entities --- .../profiles/ro-crate/must/5_web_data_entity_metadata.ttl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl index f505553f..d3f887c3 100644 --- a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl @@ -71,7 +71,8 @@ ro:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; sh:name "WebBased Data Entity: RECOMMENDED `sdDatePublished` property" ; sh:description """Check if the WebBased Data Entity has a `sdDatePublished` property""" ; sh:path schema_org:sdDatePublished ; - sh:datatype xsd:date ; + # 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 """WebBased Data Entities SHOULD have a `sdDatePublished` property""" ; ] . From af41930b25a6b5b0e0755a86c0a8a2f0d51c97bb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 16:39:38 +0200 Subject: [PATCH 484/902] fix(cli): :fire: remove unused import --- rocrate_validator/cli/commands/profiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 99945a04..6f28e7e4 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -8,7 +8,7 @@ from rocrate_validator.cli.main import cli, click from rocrate_validator.colors import get_severity_color from rocrate_validator.constants import DEFAULT_PROFILE_NAME -from rocrate_validator.models import (LevelCollection, Requirement, +from rocrate_validator.models import (LevelCollection, RequirementLevel) from rocrate_validator.utils import get_profiles_path From 29cfb7f25622fd186f4656f060557e6eb2244dc0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 16:50:05 +0200 Subject: [PATCH 485/902] fix(cli): :ambulance: missing severity param on output formatter --- 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 974b900e..0f7b75a0 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -126,7 +126,7 @@ def validate(ctx, ) # Print the validation result - __print_validation_result__(console, result) + __print_validation_result__(console, result, result.context.requirement_severity) # using ctx.exit seems to raise an Exception that gets caught below, # so we use sys.exit instead. From 14105230aeacc44327afdaf656e4bf9d75f2ebe6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 16:50:43 +0200 Subject: [PATCH 486/902] feat(cli): :lipstick: enable markdown on issue description --- rocrate_validator/cli/commands/validate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 0f7b75a0..7dc05647 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -161,10 +161,10 @@ def __print_validation_result__( f"profile: [magenta bold]{requirement.profile.name }[/magenta bold]]", align="right") ) console.print( - f" [bold][cyan][{requirement.order_number}] [u]{requirement.name}[/u][/cyan][/bold]", + f" [bold][cyan][{requirement.order_number}] [u]{Markdown(requirement.name).markup}[/u][/cyan][/bold]", style="white", ) - console.print(f"\n{' '*4}{requirement.description}\n", style="white italic") + console.print(f"\n{' '*4}{Markdown(requirement.description).markup}\n", style="white italic") console.print(f"{' '*4}[cyan u]Failed checks[/cyan u]:\n", style="white bold") for check in sorted(result.get_failed_checks_by_requirement(requirement), @@ -172,7 +172,7 @@ def __print_validation_result__( issue_color = get_severity_color(check.level.severity) console.print( f"{' '*4}- " - f"[magenta bold]{check.name}[/magenta bold]: {check.description}") + f"[magenta bold]{check.name}[/magenta bold]: {Markdown(check.description).markup}") console.print(f"\n{' '*6}[u]Detected issues[/u]:", style="white bold") for issue in sorted(result.get_issues_by_check(check), key=lambda x: (-x.severity.value, x)): @@ -187,5 +187,5 @@ def __print_validation_result__( console.print( f"{' ' * 6}- [[red]Violation[/red] of " f"[{issue_color} bold]{issue.check.identifier}[/{issue_color} bold] {path}[cyan]<{issue.focusNode}>[/cyan]]: " - f"{issue.message}",) + f"{Markdown(issue.message).markup}",) console.print("\n", style="white") From 74c31816796186bb7af64261633053b849fb49ee Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 16:53:01 +0200 Subject: [PATCH 487/902] refactor(profiles/ro-crate): :truck: rename file with web-based data entity definitions --- ...ntity.py => 5_web_data_entity_metadata.py} | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) rename rocrate_validator/profiles/ro-crate/should/{5_web_data_entity.py => 5_web_data_entity_metadata.py} (88%) diff --git a/rocrate_validator/profiles/ro-crate/should/5_web_data_entity.py b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py similarity index 88% rename from rocrate_validator/profiles/ro-crate/should/5_web_data_entity.py rename to rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py index 63141f60..4ade478e 100644 --- a/rocrate_validator/profiles/ro-crate/should/5_web_data_entity.py +++ b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py @@ -72,10 +72,10 @@ def is_downloadable(self, silent: bool = True) -> bool: return False -@requirement(name="Web Data Entity: RECOMMENDED resource availability") +@requirement(name="Web-based Data Entity: RECOMMENDED resource availability") class WebDataEntityRecommendedChecker(PyFunctionCheck): """ - Web Data Entity instances SHOULD be available at the URIs specified in the `@id` property of the Web Data Entity. + Web-based Data Entity instances SHOULD be available at the URIs specified in the `@id` property of the Web-based Data Entity. """ _json_dict_cache: Optional[dict] = None @@ -127,39 +127,38 @@ def get_web_data_entities(self, context: ValidationContext) -> list[WebDataEntit web_data_entities.append(WebDataEntity(entity)) return web_data_entities - @check(name="Web Data Entity availability") + @check(name="Web-based Data Entity: resource availability") def check_availability(self, context: ValidationContext) -> bool: """ - Check if the Web Data Entity is directly downloadable + Check if the Web-based Data Entity is directly downloadable by a simple retrieval (e.g. HTTP GET) permitting redirection and HTTP/HTTPS URIs """ result = True for entity in self.get_web_data_entities(context): assert entity.id is not None, "Entity has no @id" - logger.error("Is a web data entity") try: if not entity.is_downloadable(silent=False): response = entity.remote_resource if response is None: context.result.add_error( - f'Web Data Entity {entity.id} is not available', self) + f'Web-based Data Entity {entity.id} is not available', self) result = False elif response.status != 200: context.result.add_error( - f'Web Data Entity {entity.id} is not available (HTTP {response.status_code})', self) + f'Web-based Data Entity {entity.id} is not available (HTTP {response.status_code})', self) result = False except Exception as e: context.result.add_error( - f'Web Data Entity {entity.id} is not available: {e}', self) + f'Web-based Data Entity {entity.id} is not available: {e}', self) result = False if not result and context.fail_fast: return result return result - @check(name="Web Data Entity: `contentSize` property") + @check(name="Web-based Data Entity: `contentSize` property") def check_content_size(self, context: ValidationContext) -> bool: """ - Check if the Web Data Entity has a `contentSize` property + Check if the Web-based Data Entity has a `contentSize` property and if it is set to actual size of the downloadable content """ result = True @@ -169,7 +168,7 @@ def check_content_size(self, context: ValidationContext) -> bool: content_size = entity.get_property("contentSize") if content_size and int(content_size) != entity.content_size: context.result.add_check_issue( - f'The property contentSize={content_size} of the Web Data Entity {entity.id} does not match the actual size of ' + f'The property contentSize={content_size} of the Web-based Data Entity {entity.id} does not match the actual size of ' f'the downloadable content, i.e., {entity.content_size} (bytes)', self, focusNode=entity.id, resultPath='contentSize', value=content_size) result = False From 4424eff96d44a4f5a9e8ffcd1c0f64918720555a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 16:57:02 +0200 Subject: [PATCH 488/902] feat(profiles/ro-crate): :sparkles: add shape to validate file with optional web presence --- .../ro-crate/may/4_data_entity_metadata.ttl | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl index 19cfd25b..c56c2b4e 100644 --- a/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl @@ -8,6 +8,7 @@ @prefix owl: . @prefix rdfs: . @prefix rocrate: . +@prefix schema: . ro:DataEntityRequiredProperties a sh:NodeShape ; @@ -24,3 +25,57 @@ ro:DataEntityRequiredProperties a sh:NodeShape ; sh:severity sh:Violation ; sh:message """Data Entities MUST have an absolute or relative URI as @id.""" ; ] . + +rocrate:FileDataEntityWebOptionalProperties a sh:NodeShape ; + sh:name "Web-based Data Entity: OPTIONAL properties" ; + sh:description """A Web-based 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: + `รฌdentifier`, `url`, `subjectOf`and `mainEntityOfPage`""" ; + sh:targetClass rocrate:File ; + # Check if the Web-based Data Entity has a contentSize property + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "Web-based Data Entity: optional formal `identifier` (e.g. DOI)" ; + sh:description """Check if the Web-based 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 Web Based Data Entity MAY have a formal identifier specified through an `identifier` property""" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "Web-based Data Entity: optional `url` property" ; + sh:description """Check if the Web-based Data Entity has a `download` link""" ; + sh:path schema:url ; + sh:datatype xsd:anyURI ; + sh:severity sh:Info ; + sh:message """The Web Based Data Entity MAY use a `url` property to denote a `download` link""" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "Web-based Data Entity: optional `subjectOf` property" ; + sh:description """Check if the Web-based 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 Web Based Data Entity MAY include a `subjectOf` property to link `CreativeWork` instances that mention it.""" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "Web-based Data Entity: optional `mainEntityOfPage` property" ; + sh:description """Check if the Web-based Data Entity has a `mainEntityOfPage` property""" ; + sh:path schema:mainEntityOfPage ; + sh:class schema:WebPage, schema:CreativeWork ; + sh:severity sh:Info ; + sh:message """The Web Based Data Entity MAY have a `mainEntityOfPage` property""" ; + ] . + + + + From 64a803f87bddcb6ffc470232d9aa38a0cbe46b19 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 17:09:26 +0200 Subject: [PATCH 489/902] refactor(profiles/ro-crate): :sparkles: update description of file with optional web presence --- .../ro-crate/may/4_data_entity_metadata.ttl | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl index c56c2b4e..d248986c 100644 --- a/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl @@ -27,8 +27,8 @@ ro:DataEntityRequiredProperties a sh:NodeShape ; ] . rocrate:FileDataEntityWebOptionalProperties a sh:NodeShape ; - sh:name "Web-based Data Entity: OPTIONAL properties" ; - sh:description """A Web-based Data Entity which have a corresponding web presence, + 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: @@ -38,42 +38,42 @@ rocrate:FileDataEntityWebOptionalProperties a sh:NodeShape ; sh:property [ a sh:PropertyShape ; sh:minCount 1 ; - sh:name "Web-based Data Entity: optional formal `identifier` (e.g. DOI)" ; - sh:description """Check if the Web-based Data Entity has a formal identifier string such as a DOI""" ; + 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 Web Based Data Entity MAY have a formal identifier specified through an `identifier` property""" ; + 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 "Web-based Data Entity: optional `url` property" ; - sh:description """Check if the Web-based Data Entity has a `download` link""" ; + 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 Web Based Data Entity MAY use a `url` property to denote a `download` link""" ; + 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 "Web-based Data Entity: optional `subjectOf` property" ; - sh:description """Check if the Web-based Data Entity includes a `subjectOf` property to link `CreativeWork` instances that mention it.""" ; + 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 Web Based Data Entity MAY include a `subjectOf` property to link `CreativeWork` instances that mention it.""" ; + 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 "Web-based Data Entity: optional `mainEntityOfPage` property" ; - sh:description """Check if the Web-based Data Entity has a `mainEntityOfPage` property""" ; + 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 Web Based Data Entity MAY have a `mainEntityOfPage` property""" ; + sh:message """The File Data Entity MAY have a `mainEntityOfPage` property""" ; ] . From fff8b5903468ae6237d38c5dd3771e7522d1acce Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 5 Jun 2024 17:10:00 +0200 Subject: [PATCH 490/902] feat(profiles/ro-crate): :sparkles: add shape to validate optional `distribution` property --- .../ro-crate/may/4_data_entity_metadata.ttl | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl index d248986c..d05b1dd6 100644 --- a/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl @@ -77,5 +77,19 @@ rocrate:FileDataEntityWebOptionalProperties a sh:NodeShape ; ] . - +rocrate: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 rocrate:File ; + # 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""" ; + ] . From 3e75c702f927616d37338ce63d7dcf9533c272d1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 08:08:00 +0200 Subject: [PATCH 491/902] feat(profiles/ro-crate): :sparkles: add rule to define CreativeAuthor type --- .../profiles/ro-crate/must/6_contextual_entity.ttl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl index 6b7b5656..4b0b2396 100644 --- a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl @@ -52,3 +52,15 @@ rocrate:WebSiteRecommendedProperties a sh:NodeShape ; sh:description "Check if the WebSite has a `name` property" ; sh:message "A WebSite MUST have a `name` property" ; ] . + + +rocrate:CreativeWorkAuthorDefinition a sh:NodeShape, sh:hidden ; + sh:name "CreativeWork Author Definition" ; + sh:description """A `CreativeWork` MUST have an `author` property.""" ; + sh:targetObjectsOf schema:author ; + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object rocrate:CreativeWorkAuthor ; + ] . From a51184c585876ee9eb18500ae066f9d2f50aba36 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 08:09:57 +0200 Subject: [PATCH 492/902] refactor(profiles/ro-crate): :truck: fix Web-based Entity references --- .../must/5_web_data_entity_metadata.ttl | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl index d3f887c3..76a42f93 100644 --- a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl @@ -11,8 +11,8 @@ ro:WebBasedDataEntity a sh:NodeShape, sh:hidden ; - sh:name "WebBased Data Entity: REQUIRED properties" ; - sh:description """A WebBased Data Entity is a `File` identified by an absolute URL""" ; + 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 ; @@ -37,20 +37,20 @@ ro:WebBasedDataEntity a sh:NodeShape, sh:hidden ; ro:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; - sh:name "WebBased Data Entity: RECOMMENDED properties" ; - sh:description """A WebBased Data Entity MUST be identified by an absolute URL and + sh:name "Web-based Data Entity: RECOMMENDED properties" ; + sh:description """A Web-based Data Entity MUST be identified by an absolute URL and SHOULD have a `contentSize` and `sdDatePublished` property""" ; sh:targetClass rocrate:WebDataEntity ; - # Check if the WebBased Data Entity has a contentSize property + # Check if the Web-based Data Entity has a contentSize property sh:property [ a sh:PropertyShape ; sh:minCount 1 ; - sh:name "WebBased Data Entity: RECOMMENDED `contentSize` property string" ; - sh:description """Check if the WebBased Data Entity has a `contentSize` property""" ; + sh:name "Web-based Data Entity: RECOMMENDED `contentSize` property string" ; + 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 """WebBased Data Entities SHOULD have a `contentSize` property""" ; + sh:message """Web-based Data Entities SHOULD have a `contentSize` property""" ; sh:sparql [ sh:message "If the value is a string it must be a string representing an integer." ; sh:select """ @@ -64,15 +64,15 @@ ro:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; """ ; ] ; ] ; - # Check if the WebBased Data Entity has a sdDatePublished property + # Check if the Web-based Data Entity has a sdDatePublished property sh:property [ a sh:PropertyShape ; sh:minCount 1 ; - sh:name "WebBased Data Entity: RECOMMENDED `sdDatePublished` property" ; - sh:description """Check if the WebBased Data Entity has a `sdDatePublished` property""" ; + sh:name "Web-based Data Entity: RECOMMENDED `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 """WebBased Data Entities SHOULD have a `sdDatePublished` property""" ; + sh:message """Web-based Data Entities SHOULD have a `sdDatePublished` property""" ; ] . From 123dcf1dfd604e1b3252652a50be2b9dd499ca13 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 08:11:06 +0200 Subject: [PATCH 493/902] feat(profiles/ro-crate): :sparkles: add recommended author property on RooData Entity --- .../ro-crate/should/2_root_data_entity_metadata.ttl | 10 ++++++++++ 1 file changed, 10 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 d1688b73..fd91a211 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 @@ -55,4 +55,14 @@ ro:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; 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 SHOULD 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:property [ + a sh:PropertyShape ; + sh:name "Root Data Entity: `author` property" ; + sh:description """Check if the Root Data Entity includes a `author` property (as specified by schema.org) + to provide information about its author.""" ; + sh:class rocrate:CreativeWorkAuthor ; + sh:path schema_org:author; + sh:minCount 1 ; + sh:message """The Root Data Entity SHOULD have a link to a Contextual Entity representing the `author` of the RO-Crate""" ; ] . From e9b786e54c69d892298033395b06ba169af69cef Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 08:13:13 +0200 Subject: [PATCH 494/902] feat(profiles/ro-crate): :sparkles: add minimal definition of CrativeWork author The affiliation property SHOULD be extended to support the link the corresponding Contextual Entity --- .../should/6_contextual_entity_metadata.ttl | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl diff --git a/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl new file mode 100644 index 00000000..b3c02e4f --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl @@ -0,0 +1,31 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix owl: . +@prefix rdfs: . +@prefix rocrate: . + +rocrate:CreativeWorkAuthorMinimuRecommendedProperties 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 rocrate: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:dataType xsd:string ; + 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." ; + ] . From 9be515f987ee1b1b22ec5a9fb5754c241fa0ff38 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 09:01:30 +0200 Subject: [PATCH 495/902] feat(profiles/ro-crate): :sparkles: recommend a Contextual Entity for the organizational affiliation --- .../should/6_contextual_entity_metadata.ttl | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl index b3c02e4f..24eae4c9 100644 --- a/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl @@ -24,8 +24,24 @@ rocrate:CreativeWorkAuthorMinimuRecommendedProperties a sh:NodeShape ; sh:property [ sh:path schema:affiliation ; sh:minCount 1 ; - sh:dataType xsd:string ; + sh:or ( + [ sh:dataType xsd:string ; ] + [ sh:class schema:Organization ;] + ) ; + sh:severity sh:Violation ; 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`." ; + ] . + + ] . From 9d962f16741b9c0cf15b4105d7675baeffa6aee9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 09:02:45 +0200 Subject: [PATCH 496/902] feat(profiles/ro-crate): :sparkles: add shape to validate recommended properties of the Organization entity --- .../should/6_contextual_entity_metadata.ttl | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl index 24eae4c9..120f4820 100644 --- a/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl @@ -44,4 +44,23 @@ rocrate:CreativeWorkAuthorMinimuRecommendedProperties a sh:NodeShape ; ] . +rocrate: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." ; ] . From 8c849688dcdafbfaef8ae3a0ecbc7e80ee66825a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 11:40:57 +0200 Subject: [PATCH 497/902] fix(test-conf): :white_check_mark: update test data --- .../ro-crate-metadata.json | 25 ++++++++++++++++++- .../ro-crate-metadata.json | 24 +++++++++++++++++- 2 files changed, 47 insertions(+), 2 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 3e6d981c..4beca5a1 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 @@ -25,7 +25,30 @@ { "@id": "https://spdx.org/licenses/MIT.html" } - ] + ], + + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + } + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Stian Soiland-Reyes", + "affiliation": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "url": { + "@id": "https://orcid.org/0000-0002-1825-0097" + } + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718", + "@type": "Organization", + "name": "The University of Manchester", + "url": { + "@id": "https://www.manchester.ac.uk/" + } }, { "@id": "ro-crate-metadata.json", 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 96eca541..5f85597a 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 @@ -25,7 +25,29 @@ { "@id": "https://spdx.org/licenses/MIT.html" } - ] + ], + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + } + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Stian Soiland-Reyes", + "affiliation": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "url": { + "@id": "https://orcid.org/0000-0002-1825-0097" + } + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718", + "@type": "Organization", + "name": "The University of Manchester", + "url": { + "@id": "https://www.manchester.ac.uk/" + } }, { "@id": "ro-crate-metadata.json", From 19089d7c0c27f391a78f69dd0118766bdd21d75f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 11:41:49 +0200 Subject: [PATCH 498/902] refactor(cli): --- rocrate_validator/cli/commands/validate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 7dc05647..1ecfd8e9 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -5,9 +5,10 @@ from rich.align import Align from rich.console import Console +from rich.markdown import Markdown -from rocrate_validator.constants import DEFAULT_PROFILE_NAME import rocrate_validator.log as logging +from rocrate_validator.constants import DEFAULT_PROFILE_NAME from ... import services from ...colors import get_severity_color From beba7eb87b6ba6fd2de1110ef3cf44672f0c5ec9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 13:15:56 +0200 Subject: [PATCH 499/902] fix(core): :ambulance: fix fail fast evaluation --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 339c6e70..b8142680 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -911,7 +911,7 @@ def __do_validate__(self, logger.debug("Validation Requirement passed") else: logger.debug(f"Validation Requirement {requirement} failed ") - if context.settings.get("abort_on_first") is True: + if context.settings.get("abort_on_first") is True and context.profile_name == profile.name: logger.debug("Aborting on first requirement failure") return context.result From c226efbb7966d67afc1fb61f5f2a815d2ad34194 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 13:28:25 +0200 Subject: [PATCH 500/902] test(profiles): :white_check_mark: add missing test data --- .../profiles/fake/c-overridden/shape_c.ttl | 22 ++++++++++++ .../invalid-duplicated-shapes/shape_a.ttl | 36 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 tests/data/profiles/fake/c-overridden/shape_c.ttl create mode 100644 tests/data/profiles/fake/invalid-duplicated-shapes/shape_a.ttl diff --git a/tests/data/profiles/fake/c-overridden/shape_c.ttl b/tests/data/profiles/fake/c-overridden/shape_c.ttl new file mode 100644 index 00000000..cb9a0b59 --- /dev/null +++ b/tests/data/profiles/fake/c-overridden/shape_c.ttl @@ -0,0 +1,22 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +ro:ShapeC + a sh:NodeShape ; + sh:name "The Shape C" ; + sh:description "This is the Shape C" ; + sh:targetNode ro:ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check Metadata File Descriptor entity existence" ; + sh:description "Check if the RO-Crate Metadata File Descriptor entity exists" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + ] . diff --git a/tests/data/profiles/fake/invalid-duplicated-shapes/shape_a.ttl b/tests/data/profiles/fake/invalid-duplicated-shapes/shape_a.ttl new file mode 100644 index 00000000..dc86d8c4 --- /dev/null +++ b/tests/data/profiles/fake/invalid-duplicated-shapes/shape_a.ttl @@ -0,0 +1,36 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +ro:ShapeA + a sh:NodeShape ; + sh:name "The Shape A" ; + sh:description "This is the Shape A" ; + sh:targetNode ro:ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check Metadata File Descriptor entity existence" ; + sh:description "Check if the RO-Crate Metadata File Descriptor entity exists" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + ] . + +ro:ShapeA + a sh:NodeShape ; + sh:name "The Shape A" ; + sh:description "This is the Shape A" ; + sh:targetNode ro:ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check Metadata File Descriptor entity existence" ; + sh:description "Check if the RO-Crate Metadata File Descriptor entity exists" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + ] . From dc9e5b39fd80b7be1bd0737e8c45e7785223ab7c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 13:40:00 +0200 Subject: [PATCH 501/902] test(profiles/ro-crate): :ambulance: fix the sorting issue with profiles --- tests/unit/requirements/test_profiles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index d3034909..5a6a37e3 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -24,12 +24,12 @@ def test_order_of_loaded_profiles(profiles_path: str): assert len(profiles) > 0 # Extract the profile names - profile_names = [profile for profile in profiles] + profile_names = sorted([profile for profile in profiles]) logger.debug("The profile names: %r", profile_names) # The order of the profiles should be the same as the order of the directories # in the profiles directory - profile_directories = os.listdir(profiles_path) + profile_directories = sorted(os.listdir(profiles_path)) logger.debug("The profile directories: %r", profile_directories) assert profile_names == profile_directories From d5df28dc1b934c9f111db8965370a44708c66909 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 17:50:06 +0200 Subject: [PATCH 502/902] fix(profiles/ro-crate): :ambulance: allow more generic DataEntity instances According to the specs, a DataEntity is either a File or a Directory or any other entity mentioned in the hasPart property of the RootDataEntity --- .../must/2_root_data_entity_metadata.ttl | 5 +- .../ro-crate/must/4_data_entity_metadata.ttl | 56 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 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 9c8a7fa4..f109cc8f 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 @@ -81,16 +81,17 @@ ro:RootDataEntityValueRestriction ro:RootDataEntityHasPartValueRestriction a sh:NodeShape ; sh:name "RO-Crate Root Data Entity: `hasPart` value restriction" ; - sh:description "The Root Data Entity MUST be linked to the declared `File` and `Directories` instances through the `hasPart` property" ; + sh:description "The Root Data Entity MUST be linked to the declared `File`, `Directory` and other types of instances through the `hasPart` property" ; sh:targetClass rocrate:RootDataEntity ; sh:property [ a sh:PropertyShape ; sh:name "RO-Crate Root Data Entity: `hasPart` value restriction" ; - sh:description "Check if Root Data Entity is be linked to the declared `File` and `Directories` instances through the `hasPart` property" ; + sh:description "Check if the Root Data Entity is linked to the declared `File`, `Directory` and other types of instances through the `hasPart` property" ; sh:path schema_org:hasPart ; sh:or ( [ sh:class rocrate:File ] [ sh:class rocrate:Directory ] + [ sh:class rocrate:GenericDataEntity ] ) ; # sh:message """The Root Data Entity MUST be linked to either File or Directory instances, nothing else""" ; ] . diff --git a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl index 8c355ad4..abe7b0cd 100644 --- a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl @@ -144,6 +144,62 @@ ro:DirectoryDataEntityRequiredValueRestriction a sh:NodeShape ; sh:pattern "/$" ; ] . +ro:GenericDataEntity 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: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 rocrate: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 rocrate:DataEntity ; + ] . + + +# Uncomment for debugging +# rocrate:TestGenericDataEntity a sh:NodeShape ; +# sh:disabled true ; +# sh:targetClass rocrate: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 rocrate: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" ; From 4babc69cc53bdc3d69e972e68d6a7aea5af512b3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 18:08:46 +0200 Subject: [PATCH 503/902] test(profiles/ro-crate): :white_check_mark: update obsolete test --- .../ro-crate-metadata.json | 2 +- .../profiles/ro-crate/test_root_data_entity.py | 8 +++----- tests/ro_crates.py | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) rename tests/data/crates/invalid/2_root_data_entity_metadata/{invalid_referenced_data_entities => valid_referenced_generic_data_entities}/ro-crate-metadata.json (98%) diff --git a/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_referenced_data_entities/ro-crate-metadata.json b/tests/data/crates/invalid/2_root_data_entity_metadata/valid_referenced_generic_data_entities/ro-crate-metadata.json similarity index 98% rename from tests/data/crates/invalid/2_root_data_entity_metadata/invalid_referenced_data_entities/ro-crate-metadata.json rename to tests/data/crates/invalid/2_root_data_entity_metadata/valid_referenced_generic_data_entities/ro-crate-metadata.json index 7175c5a0..cfe2ab8f 100644 --- a/tests/data/crates/invalid/2_root_data_entity_metadata/invalid_referenced_data_entities/ro-crate-metadata.json +++ b/tests/data/crates/invalid/2_root_data_entity_metadata/valid_referenced_generic_data_entities/ro-crate-metadata.json @@ -92,7 +92,7 @@ }, { "@id": "foo/", - "@type": "InvalidEntity", + "@type": "UnknownCollection", "hasPart": [ { "@id": "foo/xxx" 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 71c9786e..dc151d0f 100644 --- a/tests/integration/profiles/ro-crate/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/test_root_data_entity.py @@ -78,14 +78,12 @@ def test_missing_root_description(): ) -def test_invalid_referenced_data_entities(): +def test_valid_referenced_generic_data_entities(): """Test a RO-Crate with invalid referenced data entities.""" do_entity_test( - paths.invalid_referenced_data_entities, + paths.valid_referenced_generic_data_entities, models.Severity.REQUIRED, - False, - ["RO-Crate Root Data Entity: `hasPart` value restriction"], - ["Node <./foo/> does not conform to one or more shapes"] + True ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 5267d91d..ab6d9761 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -91,8 +91,8 @@ def missing_root_license_description(self) -> Path: return self.base_path / "missing_root_license_description" @property - def invalid_referenced_data_entities(self) -> Path: - return self.base_path / "invalid_referenced_data_entities" + def valid_referenced_generic_data_entities(self) -> Path: + return self.base_path / "valid_referenced_generic_data_entities" class InvalidFileDescriptorEntity: From 06bab649daa7ce76b68cf5a21fce13f1521e80b8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 18:20:39 +0200 Subject: [PATCH 504/902] feat(profiles/ro-crate): :sparkles: allow more complex types of MIME types for the encondingFormat pronom --- .../profiles/ro-crate/should/4_data_entity_metadata.ttl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl index 1f685b2b..c7c1430a 100644 --- a/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl @@ -28,9 +28,11 @@ rocrate:FileRecommendedProperties a sh:NodeShape ; sh:or ( [ sh:datatype xsd:string ; - sh:pattern "^[-a-zA-Z0-9]+/[-a-zA-Z0-9]+$" ; + sh:pattern "^(\\w*)\\/(\\w[\\w\\.-]*)(?:\\+(\\w[\\w\\.-]*))?(?:;(\\w+=[^;]+))*$" ; sh:name "File Data Entity: RECOMMENDED `PRONOM` for the `encodingFormat` property" ; - sh:description "Check if the File Data Entity is linked to its `encodingFormat` through a PRONOM identifier (e.g., application/pdf)." ; + sh:description """Check if the File Data Entity is linked to its `encodingFormat` through a PRONOM identifier + (e.g., application/pdf, application/text, image/svg+xml, image/svg;q=0.9,/;q=0.8,image/svg+xml;q=0.9,/;q=0.8, application/vnd.uplanet.listcmd-wbxml;charset=utf-8). + """ ; sh:message "The `encodingFormat` SHOULD be linked using a PRONOM identifier (e.g., application/pdf)."; ] [ From 46fca0c7a74786f61a3d3118304dcda1630f30bf Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 18:27:23 +0200 Subject: [PATCH 505/902] fix(test-conf): :white_check_mark: fix missing contextual entities for some enconding format --- .../valid/wrroc-paper/ro-crate-metadata.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 a76f1197..6c7049d4 100644 --- a/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json +++ b/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json @@ -756,6 +756,21 @@ "@type": "WebSite", "name": "JSON" }, + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/x-fmt/13", + "@type": "WebSite", + "name": "Tab-separated Values" + }, + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/874", + "@type": "WebSite", + "name": "Turtle" + }, + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/875", + "@type": "WebSite", + "name": "RDF/XML" + }, { "@id": "https://www.nationalarchives.gov.uk/PRONOM/fmt/818", "@type": "WebSite", From 1e0c91fef111e86a33b02a2542c12071662b3afe Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 18:50:17 +0200 Subject: [PATCH 506/902] fix(profiles/ro-crate): :white_check_mark: update test data --- .../invalid_encoding_format_pronom/ro-crate-metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_pronom/ro-crate-metadata.json b/tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_pronom/ro-crate-metadata.json index 35111f24..384d0cef 100644 --- a/tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_pronom/ro-crate-metadata.json +++ b/tests/data/crates/invalid/4_data_entity_metadata/invalid_encoding_format_pronom/ro-crate-metadata.json @@ -74,7 +74,7 @@ { "@id": "blank.png", "@type": ["File", "ImageObject"], - "encodingFormat": ["image/png"] + "encodingFormat": 1234566 }, { "@id": "README.md", From a14c065817437bb5a8c745a5a2f28469f588ac87 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 19:06:18 +0200 Subject: [PATCH 507/902] fix(profiles/ro-crate): :ambulance: update rule to define the CreativeWorkAuthor as subclass of person --- .../profiles/ro-crate/must/6_contextual_entity.ttl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl index 4b0b2396..1765439c 100644 --- a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl @@ -56,11 +56,17 @@ rocrate:WebSiteRecommendedProperties a sh:NodeShape ; rocrate:CreativeWorkAuthorDefinition a sh:NodeShape, sh:hidden ; sh:name "CreativeWork Author Definition" ; - sh:description """A `CreativeWork` MUST have an `author` property.""" ; + sh:description """Define the `CretiveWorkAuthor` 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 rocrate:CreativeWorkAuthor ; + sh:condition [ + sh:property [ sh:path rdf:type ; sh:hasValue schema:Person ; sh:minCount 1 ] ; + ] ; + ] . + + ] . From a8b57c323d8a00c128721054da94b1deb41aaf97 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 6 Jun 2024 23:40:00 +0200 Subject: [PATCH 508/902] test(profiles/ro-crate): :white_check_mark: update test data: remove authors and full affiliation data --- .../valid/wrroc-paper/ro-crate-metadata.json | 68 +++---------------- 1 file changed, 11 insertions(+), 57 deletions(-) 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 6c7049d4..8b80a703 100644 --- a/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json +++ b/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json @@ -24,7 +24,7 @@ "@id": "./" }, "author": { - "@id": "https://orcid.org/0000-0001-9842-9718" + "@id": "https://orcid.org/0000-0001-8271-5429" }, "license": { "@id": "https://creativecommons.org/publicdomain/zero/1.0/" @@ -43,57 +43,6 @@ "author": [ { "@id": "https://orcid.org/0000-0001-8271-5429" - }, - { - "@id": "https://orcid.org/0000-0002-2961-9670" - }, - { - "@id": "https://orcid.org/0000-0003-4929-1219" - }, - { - "@id": "https://orcid.org/0000-0003-0606-2512" - }, - { - "@id": "https://orcid.org/0000-0002-3468-0652" - }, - { - "@id": "https://orcid.org/0000-0002-8940-4946" - }, - { - "@id": "https://orcid.org/0000-0002-0003-2024" - }, - { - "@id": "https://orcid.org/0000-0002-4663-5613" - }, - { - "@id": "https://orcid.org/0000-0003-0454-7145" - }, - { - "@id": "https://orcid.org/0000-0002-4806-5140" - }, - { - "@id": "https://orcid.org/0000-0001-9290-2017" - }, - { - "@id": "https://orcid.org/0000-0002-1119-1792" - }, - { - "@id": "https://orcid.org/0000-0003-3777-5945" - }, - { - "@id": "https://orcid.org/0000-0003-2765-0049" - }, - { - "@id": "https://orcid.org/0000-0002-0309-604X" - }, - { - "@id": "https://orcid.org/0000-0003-0902-0086" - }, - { - "@id": "https://orcid.org/0000-0001-8250-4074" - }, - { - "@id": "https://orcid.org/0000-0001-9842-9718" } ], "description": "RO-Crate for the manuscript that describes Workflow Run Crate, includes mapping to PROV using SKOS/SSSOM", @@ -253,9 +202,17 @@ { "@id": "https://orcid.org/0000-0001-8271-5429", "@type": "Person", - "affiliation": "Center for Advanced Studies, Research, and Development in Sardinia (CRS4), Pula, Sardinia, Italy", + "affiliation": { + "@id": "https://crs4.it" + }, "name": "Simone Leo" }, + { + "@id": "https://crs4.it", + "@type": "Organization", + "name": "Center for Advanced Studies, Research, and Development in Sardinia (CRS4)", + "url": "https://crs4.it" + }, { "@id": "https://orcid.org/0000-0002-2961-9670", "@type": "Person", @@ -434,10 +391,7 @@ ], "author": [ { - "@id": "https://orcid.org/0000-0003-0454-7145" - }, - { - "@id": "https://orcid.org/0000-0001-9842-9718" + "@id": "https://orcid.org/0000-0001-8271-5429" } ], "creator": { From d117cdfe3f9bf330aafd8bd871d2211b7044aa2b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 7 Jun 2024 08:32:12 +0200 Subject: [PATCH 509/902] fix(profiles/ro-crate): :sparkles: an organization can act as root data entity author --- .../profiles/ro-crate/should/2_root_data_entity_metadata.ttl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 fd91a211..b5cf2dad 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 @@ -61,7 +61,10 @@ ro:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; sh:name "Root Data Entity: `author` property" ; sh:description """Check if the Root Data Entity includes a `author` property (as specified by schema.org) to provide information about its author.""" ; - sh:class rocrate:CreativeWorkAuthor ; + sh:or ( + [ sh:class schema_org:Person ;] + [ sh:class schema_org:Organization ;] + ) ; sh:path schema_org:author; sh:minCount 1 ; sh:message """The Root Data Entity SHOULD have a link to a Contextual Entity representing the `author` of the RO-Crate""" ; From 28069f025ff465d818270529d8fd0e855a62fcc9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 7 Jun 2024 08:52:04 +0200 Subject: [PATCH 510/902] fix(profiles/ro-crate): :fire: clean up --- .../profiles/ro-crate/must/6_contextual_entity.ttl | 2 -- 1 file changed, 2 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl index 1765439c..b6bfd78e 100644 --- a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl @@ -68,5 +68,3 @@ rocrate:CreativeWorkAuthorDefinition a sh:NodeShape, sh:hidden ; ] ; ] . - - ] . From 21f7704dab276f1c59b1ce1b4b54e5345752f634 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 7 Jun 2024 17:21:40 +0200 Subject: [PATCH 511/902] feat(profiles/ro-crate): :sparkles: Value restriction of the recommended `publisher` property --- .../ro-crate/must/2_root_data_entity_metadata.ttl | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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 f109cc8f..5c5a31bd 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 @@ -31,8 +31,19 @@ ro:RootDataEntityType 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`.""" ; ] . - rocrate:FindRootDataEntity a sh:NodeShape, sh:hidden; From 921449f7bd8bcd638d20d015ac2a9cdf00e69f23 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 7 Jun 2024 17:22:43 +0200 Subject: [PATCH 512/902] feat(profiles/ro-crate): :sparkles: validate RECOMMENDED publisher value --- .../ro-crate/should/2_root_data_entity_metadata.ttl | 11 +++++++++++ 1 file changed, 11 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 b5cf2dad..28cd96aa 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 @@ -68,4 +68,15 @@ ro:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; sh:path schema_org:author; sh:minCount 1 ; sh:message """The Root Data Entity SHOULD have a link to a Contextual Entity representing the `author` of the RO-Crate""" ; + ] ; + sh:property [ + sh:minCount 1 ; + sh:maxCount 1 ; + sh:path schema_org:publisher ; + sh:severity sh:Warning ; + sh:name "Root Data Entity: `publisher` property" ; + sh:description """Check if the Root Data Entity has a `publisher` property of type `Organization`.""" ; + sh:message "The `publisher` property of a `Root Data Entity` SHOULD be an `Organization`"; + sh:nodeKind sh:IRI ; + sh:class schema_org:Organization ; ] . From f82fe2378b71ece1fc13d0d9d7086b2c9b3c9d6c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 7 Jun 2024 17:33:46 +0200 Subject: [PATCH 513/902] fix(profiles/ro-crate): :fire: remove duplicate shape --- .../ro-crate/may/4_data_entity_metadata.ttl | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl index d05b1dd6..3ecd5fa7 100644 --- a/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl @@ -11,21 +11,6 @@ @prefix schema: . -ro: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 sbsolute URI""" ; - sh:targetClass rocrate: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.""" ; - ] . - rocrate: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, From 075e388227a8b7bcf1e3904b8a3a2c744a1b52c8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 7 Jun 2024 17:37:27 +0200 Subject: [PATCH 514/902] fix(test-conf): :white_check_mark: update test data to fix publisher property of valid rocrate --- .../valid_encoding_format_ctx_entity/ro-crate-metadata.json | 4 +++- .../valid_encoding_format_pronom/ro-crate-metadata.json | 3 +++ tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json | 5 ++++- 3 files changed, 10 insertions(+), 2 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 4beca5a1..e8fa1ae0 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 @@ -26,9 +26,11 @@ "@id": "https://spdx.org/licenses/MIT.html" } ], - "author": { "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "publisher": { + "@id": "https://orcid.org/0000-0001-9842-9718" } }, { 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 5f85597a..5c479161 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 @@ -28,6 +28,9 @@ ], "author": { "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "publisher": { + "@id": "https://orcid.org/0000-0001-9842-9718" } }, { 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 8b80a703..3f99ec94 100644 --- a/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json +++ b/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json @@ -65,7 +65,10 @@ "license": { "@id": "https://www.apache.org/licenses/LICENSE-2.0" }, - "creator": [] + "creator": [], + "publisher": { + "@id": "https://crs4.it" + } }, { "@id": "https://doi.org/10.5281/zenodo.10368990", From 9a09453bb19e2018a2ae26c5e1080a0f8f506e70 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 21 Jun 2024 19:23:27 +0200 Subject: [PATCH 515/902] feat(shacl): :sparkles: add methods to compute a key for a shape --- rocrate_validator/requirements/shacl/models.py | 14 +++++++++++++- rocrate_validator/requirements/shacl/utils.py | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 5a4dacc1..23f58f2a 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -10,7 +10,7 @@ import rocrate_validator.log as logging from rocrate_validator.models import LevelCollection, RequirementLevel from rocrate_validator.requirements.shacl.utils import (ShapesList, - compute_hash, + compute_key, inject_attributes) # set up logging @@ -25,6 +25,8 @@ class SHACLNode: def __init__(self, node: Node, graph: Graph, parent: Optional[SHACLNode] = None): + # store the shape key + self._key = None # store the shape node self._node = node # store the shapes graph @@ -37,6 +39,13 @@ def __init__(self, node: Node, graph: Graph, parent: Optional[SHACLNode] = None) # inject attributes of the shape to the object inject_attributes(self, graph, node) + @property + def key(self) -> str: + """Return the key of the shape""" + if self._key is None: + return compute_key(self.graph, self.node) + return self._key + @property def node(self): """Return the node of the shape""" @@ -90,6 +99,9 @@ def __hash__(self): self._hash = hash(shape_hash) return self._hash + @staticmethod + def compute_key(graph: Graph, node: Node) -> str: + return compute_key(graph, node) class SHACLNodeCollection(SHACLNode): diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index 26d6d75b..3bd401f0 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -99,7 +99,7 @@ def __compute_values__(g: Graph, s: Node) -> list[tuple]: def compute_hash(g: Graph, s: Node): """ - Compute the hash of the triples in the graph (excluding BNodes) + Compute the hash of the triples in the graph (including BNodes) starting from the given subject node `s`. """ @@ -113,6 +113,19 @@ def compute_hash(g: Graph, s: Node): return hash_value +def compute_key(g: Graph, s: Node) -> str: + """ + Compute the key of the node `s` in the graph `g`. + If the node is a URI, return the URI as a string. + If the node is a BNode, return the hash of the triples in the graph starting from the BNode. + """ + + if isinstance(s, BNode): + return compute_hash(g, s) + else: + return s.toPython() + + class ShapesList: def __init__(self, node_shapes: list[Node], From bd538bb71ad11efcef920b960be698af7a5fac3f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 21 Jun 2024 19:25:29 +0200 Subject: [PATCH 516/902] refactor(shacl): :recycle: redefine hash for shape objects --- rocrate_validator/requirements/shacl/models.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 23f58f2a..422ed2da 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -95,14 +95,18 @@ def __eq__(self, other): def __hash__(self): if self._hash is None: - shape_hash = compute_hash(self.graph, self.node) - self._hash = hash(shape_hash) + self._hash = hash(self.key) return self._hash @staticmethod def compute_key(graph: Graph, node: Node) -> str: return compute_key(graph, node) + @staticmethod + def compute_hash(graph: Graph, node: Node) -> int: + return hash(compute_key(graph, node)) + + class SHACLNodeCollection(SHACLNode): def __init__(self, node: Node, graph: Graph, properties: list[PropertyShape] = None): From 782f78950f8743976a4e2a9894672d48f40d2feb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 21 Jun 2024 19:27:36 +0200 Subject: [PATCH 517/902] refactor(shacl): :recycle: update `ShapeRegistry` to use reference shapes by key --- rocrate_validator/requirements/shacl/models.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 422ed2da..9cbb3f19 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -221,19 +221,19 @@ def __init__(self): def add_shape(self, shape: Shape): assert isinstance(shape, Shape), "Invalid shape" - self._shapes[f"{hash(shape)}"] = shape + self._shapes[shape.key] = shape def remove_shape(self, shape: Shape): assert isinstance(shape, Shape), "Invalid shape" - self._shapes.pop(f"{hash(shape)}", None) + self._shapes.pop(shape.key, None) self._shapes_graph -= shape.graph - def get_shape(self, hash_value: int) -> Optional[Shape]: - logger.debug("Searching for shape %s in the registry: %s", hash_value, self._shapes) - result = self._shapes.get(f"{hash_value}", None) + def get_shape(self, shape_key: str) -> Optional[Shape]: + logger.debug("Searching for shape %s in the registry: %s", shape_key, self._shapes) + result = self._shapes.get(shape_key, None) if not result: - logger.debug(f"Shape {hash_value} not found in the registry") - raise ValueError(f"Shape not found in the registry: {hash_value}") + logger.debug(f"Shape {shape_key} not found in the registry") + raise ValueError(f"Shape not found in the registry: {shape_key}") return result def get_shape_key(self, shape: Shape) -> str: From 55454fc0874a3648c8f335820a93eba0e3b181a1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 21 Jun 2024 19:28:52 +0200 Subject: [PATCH 518/902] refactor(shacl): :fire: clean up --- rocrate_validator/requirements/shacl/models.py | 4 ---- rocrate_validator/requirements/shacl/validator.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 9cbb3f19..ee53f240 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -236,10 +236,6 @@ def get_shape(self, shape_key: str) -> Optional[Shape]: raise ValueError(f"Shape not found in the registry: {shape_key}") return result - def get_shape_key(self, shape: Shape) -> str: - assert isinstance(shape, Shape), "Invalid shape" - return f"{hash(shape)}" - def extend(self, shapes: dict[str, Shape], graph: Graph) -> None: self._shapes.update(shapes) self._shapes_graph += graph diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index f189c730..d5ce2f20 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -115,7 +115,7 @@ def __set_current_validation_profile__(self, profile: Profile) -> bool: if check.overridden: logger.debug("Overridden check: %s", check) profile_shapes_graph -= check.shape.graph - profile_shapes.pop(profile_registry.get_shape_key(check.shape)) + profile_shapes.pop(check.shape.key) # add the shapes to the registry self._shapes_registry.extend(profile_shapes, profile_shapes_graph) From be6694b352a104cb5b21a85f38bc61877e7104ef Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 21 Jun 2024 19:31:56 +0200 Subject: [PATCH 519/902] refactor(shacl): :building_construction: simplify getter of the sourceShape property --- rocrate_validator/requirements/shacl/validator.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index d5ce2f20..2ae64809 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -6,7 +6,7 @@ import pyshacl from pyshacl.pytypes import GraphLike -from rdflib import Graph +from rdflib import BNode, Graph from rdflib.term import Node, URIRef import rocrate_validator.log as logging @@ -19,7 +19,7 @@ RDF_SERIALIZATION_FORMATS_TYPES, SHACL_NS, VALID_INFERENCE_OPTIONS, VALID_INFERENCE_OPTIONS_TYPES) -from .models import PropertyShape, ShapesRegistry +from .models import ShapesRegistry # set up logging logger = logging.getLogger(__name__) @@ -274,12 +274,11 @@ def get_result_message(self, ro_crate_path: Union[Path, str]) -> str: return self._result_message @property - def sourceShape(self) -> PropertyShape: + def sourceShape(self) -> Union[URIRef, BNode]: if not self._source_shape_node: self._source_shape_node = self.graph.value(self._violation_node, URIRef(f"{SHACL_NS}sourceShape")) assert self._source_shape_node is not None, f"Unable to get source shape node from violation node {self._violation_node}" - self._source_shape = PropertyShape(self._source_shape_node, self.graph) - return self._source_shape + return self._source_shape_node class SHACLValidationResult: From 92e42122c0310504a58ccccd957363cebd0f13b6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 21 Jun 2024 19:32:49 +0200 Subject: [PATCH 520/902] fix(shacl): :bug: the `resultPath` is optional --- rocrate_validator/requirements/shacl/validator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 2ae64809..c5096c96 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -241,7 +241,6 @@ def focusNode(self) -> Node: def resultPath(self): if not self._result_path: self._result_path = self.graph.value(self._violation_node, URIRef(f"{SHACL_NS}resultPath")) - assert self._result_path is not None, f"Unable to get result path from violation node {self._violation_node}" return self._result_path @property From 32a64df1b8441be2610fba4d1b987159d806c318 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 21 Jun 2024 19:34:05 +0200 Subject: [PATCH 521/902] refactor(shacl): :recycle: use the appropriate function ot retrieve the shape from the registry --- rocrate_validator/requirements/shacl/checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index a1670d92..90b237e4 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -84,7 +84,7 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): logger.debug("Validation failed") logger.debug("Parsing Validation result: %s", result) for violation in shacl_result.violations: - shape = shapes_registry.get_shape(hash(violation.sourceShape)) + shape = shapes_registry.get_shape(Shape.compute_key(shacl_context.shapes_graph, violation.sourceShape)) assert shape is not None, "Unable to map the violation to a shape" requirementCheck = SHACLCheck.get_instance(shape) assert requirementCheck is not None, "The requirement check cannot be None" From 8a87f0870cb124576482e5df958b32202fb2bf01 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 30 May 2024 12:48:57 +0200 Subject: [PATCH 522/902] feat(profiles/ro-crate): :sparkles: add initial sample for workflow ro-crate profile --- .../must/0_main-workflow.ttl | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 rocrate_validator/profiles/workflow-ro-crate/must/0_main-workflow.ttl 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 new file mode 100644 index 00000000..111b1d39 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/must/0_main-workflow.ttl @@ -0,0 +1,45 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix rocrate: . + +rocrate:FindMainWorkflow a sh:NodeShape ; + sh:name "Identify Main Workflow" ; + sh:description "Main Workflow" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?root schema:mainEntity ?this . + ?root a schema:Dataset . + ?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 rocrate:MainWorkflow ; + ] . + +ro:MainWorkflowRecommendedProperties a sh:NodeShape ; + sh:name "Main Workflow definition" ; + sh:description """The Main Workflow MUST have the properties defined in + """; + sh:targetClass rocrate:MainWorkflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "X" ; + sh:description "XXX" ; + sh:path schema_org:name ; + sh:minCount 1 ; + sh:nodeKind sh:Literal ; + ] . From 0d41dfe0c36777ecd71c76b079b3b08fb521f6ae Mon Sep 17 00:00:00 2001 From: simleo Date: Fri, 7 Jun 2024 15:26:25 +0200 Subject: [PATCH 523/902] add main workflow type check --- .../must/0_main-workflow.ttl | 17 ++- .../ro-crate-metadata.json | 113 ++++++++++++++++++ .../profiles/ro-crate/test_valid_ro-crate.py | 9 -- .../workflow-ro-crate/test_main_workflow.py | 22 ++++ .../workflow-ro-crate/test_valid_wroc.py | 17 +++ tests/ro_crates.py | 9 ++ tests/shared.py | 6 +- 7 files changed, 176 insertions(+), 17 deletions(-) create mode 100644 tests/data/crates/invalid/0_main_workflow/main_workflow_bad_type/ro-crate-metadata.json create mode 100644 tests/integration/profiles/workflow-ro-crate/test_main_workflow.py create mode 100644 tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py 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 111b1d39..fd3f37dc 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 @@ -1,13 +1,15 @@ @prefix ro: <./> . @prefix dct: . @prefix rdf: . -@prefix schema_org: . +@prefix schema: . @prefix sh: . @prefix xml1: . @prefix xsd: . @prefix rocrate: . +@prefix bioschemas: . -rocrate:FindMainWorkflow a sh:NodeShape ; + +ro:FindMainWorkflow a sh:NodeShape ; sh:name "Identify Main Workflow" ; sh:description "Main Workflow" ; sh:target [ @@ -37,9 +39,12 @@ ro:MainWorkflowRecommendedProperties a sh:NodeShape ; sh:targetClass rocrate:MainWorkflow ; sh:property [ a sh:PropertyShape ; - sh:name "X" ; - sh:description "XXX" ; - sh:path schema_org:name ; + sh:name "Main Workflow type" ; + sh:description "The Main Workflow must have types File, SoftwareSourceCode, ComputationalWorfklow" ; + sh:path rdf:type ; + sh:hasValue schema:MediaObject , + schema:SoftwareSourceCode , + bioschemas:ComputationalWorkflow ; sh:minCount 1 ; - sh:nodeKind sh:Literal ; + sh:message "The Main Workflow must have types File, SoftwareSourceCode, ComputationalWorfklow" ; ] . diff --git a/tests/data/crates/invalid/0_main_workflow/main_workflow_bad_type/ro-crate-metadata.json b/tests/data/crates/invalid/0_main_workflow/main_workflow_bad_type/ro-crate-metadata.json new file mode 100644 index 00000000..b6d3d9cb --- /dev/null +++ b/tests/data/crates/invalid/0_main_workflow/main_workflow_bad_type/ro-crate-metadata.json @@ -0,0 +1,113 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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" + } + ], + "license": "https://spdx.org/licenses/Apache-2.0.html", + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@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", + "Action", + "ComputationalWorkflow" + ], + "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" + } + ] +} \ No newline at end of file diff --git a/tests/integration/profiles/ro-crate/test_valid_ro-crate.py b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py index c6ea3657..a2f8515a 100644 --- a/tests/integration/profiles/ro-crate/test_valid_ro-crate.py +++ b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py @@ -35,12 +35,3 @@ def test_valid_roc_required_with_long_datetime(): Severity.REQUIRED, True ) - - -def test_valid_workflow_roc_required(): - """Test a valid RO-Crate.""" - do_entity_test( - ValidROC().workflow_roc, - Severity.REQUIRED, - True - ) diff --git a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py new file mode 100644 index 00000000..b5c8c04a --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py @@ -0,0 +1,22 @@ +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import InvalidMainWorkflow +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().main_workflow_bad_type, + Severity.REQUIRED, + False, + ["Main Workflow definition"], + ["The Main Workflow must have types File, SoftwareSourceCode, ComputationalWorfklow"], + profile_name="workflow-ro-crate" + ) diff --git a/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py b/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py new file mode 100644 index 00000000..7e22abc7 --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py @@ -0,0 +1,17 @@ +import logging + +from rocrate_validator.models import Severity +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, + Severity.REQUIRED, + True, + profile_name="workflow-ro-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index ab6d9761..e9585556 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -171,3 +171,12 @@ def valid_encoding_format_ctx_entity(self) -> Path: @property def valid_encoding_format_pronom(self) -> Path: return self.base_path / "valid_encoding_format_pronom" + + +class InvalidMainWorkflow: + + base_path = INVALID_CRATES_DATA_PATH / "0_main_workflow" + + @property + def main_workflow_bad_type(self) -> Path: + return self.base_path / "main_workflow_bad_type" diff --git a/tests/shared.py b/tests/shared.py index 815f488a..101f14d9 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -25,7 +25,8 @@ def do_entity_test( expected_validation_result: bool, expected_triggered_requirements: Optional[list[str]] = None, expected_triggered_issues: Optional[list[str]] = None, - abort_on_first: bool = True + abort_on_first: bool = True, + profile_name: str = "ro-crate" ): """ Shared function to test a RO-Crate entity @@ -54,7 +55,8 @@ def do_entity_test( services.validate(models.ValidationSettings(**{ "data_path": rocrate_path, "requirement_severity": requirement_severity, - "abort_on_first": abort_on_first + "abort_on_first": abort_on_first, + "profile_name": profile_name })) logger.debug("Expected validation result: %s", expected_validation_result) From f27bbbfb2ef18f1d68320d08356e01e32161aa28 Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 10 Jun 2024 11:05:10 +0200 Subject: [PATCH 524/902] add main workflow language check --- .../must/0_main-workflow.ttl | 9 ++ .../ro-crate-metadata.json | 99 +++++++++++++++++++ .../workflow-ro-crate/test_main_workflow.py | 15 +++ tests/ro_crates.py | 4 + 4 files changed, 127 insertions(+) create mode 100644 tests/data/crates/invalid/0_main_workflow/main_workflow_no_lang/ro-crate-metadata.json 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 fd3f37dc..babd0dd1 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 @@ -47,4 +47,13 @@ ro:MainWorkflowRecommendedProperties a sh:NodeShape ; bioschemas:ComputationalWorkflow ; sh:minCount 1 ; sh:message "The Main Workflow must have types File, SoftwareSourceCode, ComputationalWorfklow" ; + ] ; + 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/tests/data/crates/invalid/0_main_workflow/main_workflow_no_lang/ro-crate-metadata.json b/tests/data/crates/invalid/0_main_workflow/main_workflow_no_lang/ro-crate-metadata.json new file mode 100644 index 00000000..f9ca8cb3 --- /dev/null +++ b/tests/data/crates/invalid/0_main_workflow/main_workflow_no_lang/ro-crate-metadata.json @@ -0,0 +1,99 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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" + } + ], + "license": "https://spdx.org/licenses/Apache-2.0.html", + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@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" + ], + "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/" + } + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + } + ] +} \ No newline at end of file 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 b5c8c04a..461abde8 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py +++ b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py @@ -20,3 +20,18 @@ def test_main_workflow_bad_type(): ["The Main Workflow must have types File, SoftwareSourceCode, ComputationalWorfklow"], profile_name="workflow-ro-crate" ) + + +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().main_workflow_no_lang, + Severity.REQUIRED, + False, + ["Main Workflow definition"], + ["The Main Workflow must refer to its language via programmingLanguage"], + profile_name="workflow-ro-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index e9585556..5ef04741 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -180,3 +180,7 @@ class InvalidMainWorkflow: @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" From bc038d4427b2aeb23696be7871ae15ea58a1af72 Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 10 Jun 2024 11:54:46 +0200 Subject: [PATCH 525/902] add main workflow image check --- .../workflow-ro-crate/may/0_main-workflow.ttl | 25 +++++ .../must/0_main-workflow.ttl | 5 +- .../ro-crate-metadata.json | 103 ++++++++++++++++++ .../workflow-ro-crate/test_main_workflow.py | 15 +++ tests/ro_crates.py | 4 + 5 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl create mode 100644 tests/data/crates/invalid/0_main_workflow/main_workflow_no_image/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl new file mode 100644 index 00000000..f568f463 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl @@ -0,0 +1,25 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix rocrate: . +@prefix bioschemas: . + + +ro:MainWorkflowOptionalProperties a sh:NodeShape ; + sh:name "Main Workflow optional properties" ; + sh:description """Main Workflow properties defined as MAY"""; + sh:targetClass rocrate: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 ; + ] . 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 babd0dd1..3fca3eb0 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 @@ -32,10 +32,9 @@ ro:FindMainWorkflow a sh:NodeShape ; sh:object rocrate:MainWorkflow ; ] . -ro:MainWorkflowRecommendedProperties a sh:NodeShape ; +ro:MainWorkflowRequiredProperties a sh:NodeShape ; sh:name "Main Workflow definition" ; - sh:description """The Main Workflow MUST have the properties defined in - """; + sh:description """Main Workflow properties defined as MUST"""; sh:targetClass rocrate:MainWorkflow ; sh:property [ a sh:PropertyShape ; diff --git a/tests/data/crates/invalid/0_main_workflow/main_workflow_no_image/ro-crate-metadata.json b/tests/data/crates/invalid/0_main_workflow/main_workflow_no_image/ro-crate-metadata.json new file mode 100644 index 00000000..b746084b --- /dev/null +++ b/tests/data/crates/invalid/0_main_workflow/main_workflow_no_image/ro-crate-metadata.json @@ -0,0 +1,103 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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" + } + ], + "license": "https://spdx.org/licenses/Apache-2.0.html", + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@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" + ], + "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/" + } + }, + { + "@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": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + } + ] +} \ No newline at end of file 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 461abde8..470fe27c 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py +++ b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py @@ -35,3 +35,18 @@ def test_main_workflow_no_lang(): ["The Main Workflow must refer to its language via programmingLanguage"], profile_name="workflow-ro-crate" ) + + +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().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_name="workflow-ro-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 5ef04741..bcb75e8c 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -184,3 +184,7 @@ def main_workflow_bad_type(self) -> Path: @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" From d1e83d8eba993be9de71623acbc100b47b11a899 Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 10 Jun 2024 14:39:10 +0200 Subject: [PATCH 526/902] add main workflow cwl desc check --- .../workflow-ro-crate/may/0_main-workflow.ttl | 35 ++++++ .../ro-crate-metadata.json | 111 ++++++++++++++++++ .../ro-crate-metadata.json | 98 ++++++++++++++++ .../ro-crate-metadata.json | 86 ++++++++++++++ .../workflow-ro-crate/test_main_workflow.py | 45 +++++++ tests/ro_crates.py | 12 ++ 6 files changed, 387 insertions(+) create mode 100644 tests/data/crates/invalid/0_main_workflow/main_workflow_cwl_desc_bad_type/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/0_main_workflow/main_workflow_cwl_desc_no_lang/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/0_main_workflow/main_workflow_no_cwl_desc/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl index f568f463..d6dda4bc 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl @@ -7,6 +7,7 @@ @prefix xsd: . @prefix rocrate: . @prefix bioschemas: . +@prefix wroc: . ro:MainWorkflowOptionalProperties a sh:NodeShape ; @@ -22,4 +23,38 @@ ro:MainWorkflowOptionalProperties a sh:NodeShape ; 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 ro: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 ; + ] . + + +ro: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/tests/data/crates/invalid/0_main_workflow/main_workflow_cwl_desc_bad_type/ro-crate-metadata.json b/tests/data/crates/invalid/0_main_workflow/main_workflow_cwl_desc_bad_type/ro-crate-metadata.json new file mode 100644 index 00000000..51cee962 --- /dev/null +++ b/tests/data/crates/invalid/0_main_workflow/main_workflow_cwl_desc_bad_type/ro-crate-metadata.json @@ -0,0 +1,111 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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" + } + ], + "license": "https://spdx.org/licenses/Apache-2.0.html", + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@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" + ], + "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" + ], + "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" + } + ] +} \ No newline at end of file diff --git a/tests/data/crates/invalid/0_main_workflow/main_workflow_cwl_desc_no_lang/ro-crate-metadata.json b/tests/data/crates/invalid/0_main_workflow/main_workflow_cwl_desc_no_lang/ro-crate-metadata.json new file mode 100644 index 00000000..513e57ad --- /dev/null +++ b/tests/data/crates/invalid/0_main_workflow/main_workflow_cwl_desc_no_lang/ro-crate-metadata.json @@ -0,0 +1,98 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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" + } + ], + "license": "https://spdx.org/licenses/Apache-2.0.html", + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@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" + ], + "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" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + } + ] +} \ No newline at end of file diff --git a/tests/data/crates/invalid/0_main_workflow/main_workflow_no_cwl_desc/ro-crate-metadata.json b/tests/data/crates/invalid/0_main_workflow/main_workflow_no_cwl_desc/ro-crate-metadata.json new file mode 100644 index 00000000..60a60937 --- /dev/null +++ b/tests/data/crates/invalid/0_main_workflow/main_workflow_no_cwl_desc/ro-crate-metadata.json @@ -0,0 +1,86 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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" + } + ], + "license": "https://spdx.org/licenses/Apache-2.0.html", + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@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" + ], + "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/" + } + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + } + ] +} \ No newline at end of file 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 470fe27c..35267186 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py +++ b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py @@ -50,3 +50,48 @@ def test_main_workflow_no_image(): ["The Crate MAY contain a Main Workflow Diagram; if present it MUST be referred to via 'image'"], profile_name="workflow-ro-crate" ) + + +def test_main_workflow_no_cwl_desc(): + """\ + Test a Workflow RO-Crate where the main workflow does not have an + CWL description. + """ + do_entity_test( + InvalidMainWorkflow().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_name="workflow-ro-crate" + ) + + +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().main_workflow_cwl_desc_bad_type, + Severity.OPTIONAL, + False, + ["Main Workflow optional properties"], + ["The CWL Description type must be File, SoftwareSourceCode, HowTo"], + profile_name="workflow-ro-crate" + ) + + +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().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_name="workflow-ro-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index bcb75e8c..eed5b620 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -188,3 +188,15 @@ def main_workflow_no_lang(self) -> Path: @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" From 5438b1e06490e95657a9b3233965d46cacd6e75b Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 10 Jun 2024 17:07:49 +0200 Subject: [PATCH 527/902] add python checks for file existence --- .../workflow-ro-crate/may/0_main_workflow.py | 99 +++++++++++++++ .../no_files/ro-crate-metadata.json | 113 ++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 rocrate_validator/profiles/workflow-ro-crate/may/0_main_workflow.py create mode 100644 tests/data/crates/invalid/0_main_workflow/no_files/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/0_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/may/0_main_workflow.py new file mode 100644 index 00000000..a49462b6 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/may/0_main_workflow.py @@ -0,0 +1,99 @@ +import json +from typing import Optional + +import rocrate_validator.log as logging +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import (PyFunctionCheck, check, + requirement) + +# set up logging +logger = logging.getLogger(__name__) + + +def find_metadata_file_descriptor(entity_dict: dict): + for k, v in entity_dict.items(): + if k.endswith("ro-crate-metadata.json"): + return v + raise ValueError("no metadata file descriptor in crate") + + +def find_root_data_entity(entity_dict: dict): + metadata_file_descriptor = find_metadata_file_descriptor(entity_dict) + return entity_dict[metadata_file_descriptor["about"]["@id"]] + + +def find_main_workflow(entity_dict: dict): + root_data_entity = find_root_data_entity(entity_dict) + return entity_dict[root_data_entity["mainEntity"]["@id"]] + + +@requirement(name="Workflow diagram existence") +class WorkflowDiagramExistence(PyFunctionCheck): + """A Workflow diagram MAY be present in the RO-Crate.""" + + _json_dict_cache: Optional[dict] = None + + def get_json_dict(self, context: ValidationContext) -> dict: + if self._json_dict_cache is None or \ + self._json_dict_cache['file_descriptor_path'] != context.file_descriptor_path: + # invalid cache + try: + with open(context.file_descriptor_path, "r") as file: + self._json_dict_cache = dict( + json=json.load(file), + file_descriptor_path=context.file_descriptor_path) + except Exception as e: + context.result.add_error( + f'file descriptor "{context.rel_fd_path}" is not in the correct format', self) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return {} + return self._json_dict_cache['json'] + + @check(name="Workflow existence") + def check_workflow(self, validation_context: ValidationContext) -> bool: + """Check if the crate contains the workflow file.""" + json_dict = self.get_json_dict(validation_context) + entity_dict = {_["@id"]: _ for _ in json_dict["@graph"]} + main_workflow = find_main_workflow(entity_dict) + if not main_workflow: + validation_context.result.add_error(f"main workflow does not exist in metadata file", self) + return False + workflow_relpath = main_workflow["@id"] + workflow_path = validation_context.rocrate_path / workflow_relpath + if not workflow_path.is_file(): + validation_context.result.add_error(f"{workflow_path} not found in crate", self) + return False + return True + + @check(name="Workflow diagram existence") + def check_workflow_diagram(self, validation_context: ValidationContext) -> bool: + """Check if the crate contains the workflow diagram.""" + json_dict = self.get_json_dict(validation_context) + entity_dict = {_["@id"]: _ for _ in json_dict["@graph"]} + main_workflow = find_main_workflow(entity_dict) + diagram_relpath = main_workflow.get("image")["@id"] + if not diagram_relpath: + validation_context.result.add_error(f"main workflow does not have an 'image' property", self) + return False + diagram_path = validation_context.rocrate_path / diagram_relpath + if not diagram_path.is_file(): + validation_context.result.add_error(f"{diagram_path} not found in crate", self) + return False + return True + + @check(name="Workflow description existence") + def check_workflow_description(self, validation_context: ValidationContext) -> bool: + """Check if the crate contains the workflow CWL description.""" + json_dict = self.get_json_dict(validation_context) + entity_dict = {_["@id"]: _ for _ in json_dict["@graph"]} + main_workflow = find_main_workflow(entity_dict) + description_relpath = main_workflow.get("subjectOf")["@id"] + if not description_relpath: + validation_context.result.add_error(f"main workflow does not have a 'subjectOf' property", self) + return False + description_path = validation_context.rocrate_path / description_relpath + if not description_path.is_file(): + validation_context.result.add_error(f"{description_path} not found in crate", self) + return False + return True diff --git a/tests/data/crates/invalid/0_main_workflow/no_files/ro-crate-metadata.json b/tests/data/crates/invalid/0_main_workflow/no_files/ro-crate-metadata.json new file mode 100644 index 00000000..4bb00a12 --- /dev/null +++ b/tests/data/crates/invalid/0_main_workflow/no_files/ro-crate-metadata.json @@ -0,0 +1,113 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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" + } + ], + "license": "https://spdx.org/licenses/Apache-2.0.html", + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@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" + ], + "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" + } + ] +} \ No newline at end of file From ff02c4958fc5dc262b07f75f5632d30ba4031ae7 Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 10 Jun 2024 18:11:28 +0200 Subject: [PATCH 528/902] wroc: add checks for readme file --- .../workflow-ro-crate/should/1_wroc_crate.ttl | 33 +++++ .../ro-crate-metadata.json | 110 +++++++++++++++++ .../ro-crate-metadata.json | 113 ++++++++++++++++++ 3 files changed, 256 insertions(+) create mode 100644 rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl create mode 100644 tests/data/crates/invalid/1_wroc_crate/readme_not_about_crate/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/1_wroc_crate/readme_wrong_encoding_format/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl new file mode 100644 index 00000000..722c3bde --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl @@ -0,0 +1,33 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix rocrate: . +@prefix bioschemas: . + + +ro:FindReadme a sh:NodeShape ; + sh:name "Identify README.md" ; + 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 rocrate: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/tests/data/crates/invalid/1_wroc_crate/readme_not_about_crate/ro-crate-metadata.json b/tests/data/crates/invalid/1_wroc_crate/readme_not_about_crate/ro-crate-metadata.json new file mode 100644 index 00000000..ad81e806 --- /dev/null +++ b/tests/data/crates/invalid/1_wroc_crate/readme_not_about_crate/ro-crate-metadata.json @@ -0,0 +1,110 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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" + } + ], + "license": "https://spdx.org/licenses/Apache-2.0.html", + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@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" + ], + "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", + "encodingFormat": "text/markdown" + } + ] +} \ No newline at end of file diff --git a/tests/data/crates/invalid/1_wroc_crate/readme_wrong_encoding_format/ro-crate-metadata.json b/tests/data/crates/invalid/1_wroc_crate/readme_wrong_encoding_format/ro-crate-metadata.json new file mode 100644 index 00000000..2669fb9f --- /dev/null +++ b/tests/data/crates/invalid/1_wroc_crate/readme_wrong_encoding_format/ro-crate-metadata.json @@ -0,0 +1,113 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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" + } + ], + "license": "https://spdx.org/licenses/Apache-2.0.html", + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@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" + ], + "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/csv" + } + ] +} \ No newline at end of file From f793bda044128502c2f79e564741fa9ad09ef0b5 Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 11 Jun 2024 16:08:23 +0200 Subject: [PATCH 529/902] add tests for wroc files python checks --- .../workflow-ro-crate/may/0_main_workflow.py | 28 ++------ .../workflow-ro-crate/must/0_main_workflow.py | 67 +++++++++++++++++++ .../workflow-ro-crate/test_main_workflow.py | 46 +++++++++++++ tests/ro_crates.py | 4 ++ 4 files changed, 123 insertions(+), 22 deletions(-) create mode 100644 rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/0_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/may/0_main_workflow.py index a49462b6..706fe209 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/may/0_main_workflow.py +++ b/rocrate_validator/profiles/workflow-ro-crate/may/0_main_workflow.py @@ -27,9 +27,9 @@ def find_main_workflow(entity_dict: dict): return entity_dict[root_data_entity["mainEntity"]["@id"]] -@requirement(name="Workflow diagram existence") -class WorkflowDiagramExistence(PyFunctionCheck): - """A Workflow diagram MAY be present in the RO-Crate.""" +@requirement(name="Workflow-related files existence") +class WorkflowFilesExistence(PyFunctionCheck): + """Checks for workflow-related crate files existence.""" _json_dict_cache: Optional[dict] = None @@ -50,22 +50,6 @@ def get_json_dict(self, context: ValidationContext) -> dict: return {} return self._json_dict_cache['json'] - @check(name="Workflow existence") - def check_workflow(self, validation_context: ValidationContext) -> bool: - """Check if the crate contains the workflow file.""" - json_dict = self.get_json_dict(validation_context) - entity_dict = {_["@id"]: _ for _ in json_dict["@graph"]} - main_workflow = find_main_workflow(entity_dict) - if not main_workflow: - validation_context.result.add_error(f"main workflow does not exist in metadata file", self) - return False - workflow_relpath = main_workflow["@id"] - workflow_path = validation_context.rocrate_path / workflow_relpath - if not workflow_path.is_file(): - validation_context.result.add_error(f"{workflow_path} not found in crate", self) - return False - return True - @check(name="Workflow diagram existence") def check_workflow_diagram(self, validation_context: ValidationContext) -> bool: """Check if the crate contains the workflow diagram.""" @@ -78,7 +62,7 @@ def check_workflow_diagram(self, validation_context: ValidationContext) -> bool: return False diagram_path = validation_context.rocrate_path / diagram_relpath if not diagram_path.is_file(): - validation_context.result.add_error(f"{diagram_path} not found in crate", self) + validation_context.result.add_error(f"Workflow diagram {diagram_path} not found in crate", self) return False return True @@ -90,10 +74,10 @@ def check_workflow_description(self, validation_context: ValidationContext) -> b main_workflow = find_main_workflow(entity_dict) description_relpath = main_workflow.get("subjectOf")["@id"] if not description_relpath: - validation_context.result.add_error(f"main workflow does not have a 'subjectOf' property", self) + validation_context.result.add_error("main workflow does not have a 'subjectOf' property", self) return False description_path = validation_context.rocrate_path / description_relpath if not description_path.is_file(): - validation_context.result.add_error(f"{description_path} not found in crate", self) + validation_context.result.add_error(f"Workflow CWL description {description_path} not found in crate", self) return False return True 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 new file mode 100644 index 00000000..37934d34 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py @@ -0,0 +1,67 @@ +import json +from typing import Optional + +import rocrate_validator.log as logging +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import (PyFunctionCheck, check, + requirement) + +# set up logging +logger = logging.getLogger(__name__) + + +def find_metadata_file_descriptor(entity_dict: dict): + for k, v in entity_dict.items(): + if k.endswith("ro-crate-metadata.json"): + return v + raise ValueError("no metadata file descriptor in crate") + + +def find_root_data_entity(entity_dict: dict): + metadata_file_descriptor = find_metadata_file_descriptor(entity_dict) + return entity_dict[metadata_file_descriptor["about"]["@id"]] + + +def find_main_workflow(entity_dict: dict): + root_data_entity = find_root_data_entity(entity_dict) + return entity_dict[root_data_entity["mainEntity"]["@id"]] + + +@requirement(name="Main Workflow file existence") +class MainWorkflowFileExistence(PyFunctionCheck): + """Checks for main workflow file existence.""" + + _json_dict_cache: Optional[dict] = None + + def get_json_dict(self, context: ValidationContext) -> dict: + if self._json_dict_cache is None or \ + self._json_dict_cache['file_descriptor_path'] != context.file_descriptor_path: + # invalid cache + try: + with open(context.file_descriptor_path, "r") as file: + self._json_dict_cache = dict( + json=json.load(file), + file_descriptor_path=context.file_descriptor_path) + except Exception as e: + context.result.add_error( + f'file descriptor "{context.rel_fd_path}" is not in the correct format', self) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return {} + return self._json_dict_cache['json'] + + @check(name="Main Workflow file must exist") + def check_workflow(self, validation_context: ValidationContext) -> bool: + """Check if the crate contains the main workflow file.""" + json_dict = self.get_json_dict(validation_context) + entity_dict = {_["@id"]: _ for _ in json_dict["@graph"]} + main_workflow = find_main_workflow(entity_dict) + if not main_workflow: + validation_context.result.add_error(f"main workflow does not exist in metadata file", self) + return False + workflow_relpath = main_workflow["@id"] + workflow_path = validation_context.rocrate_path / workflow_relpath + if not workflow_path.is_file(): + validation_context.result.add_error(f"Main Workflow {workflow_path} not found in crate", self) + return False + return True 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 35267186..0155aacb 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py +++ b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py @@ -95,3 +95,49 @@ def test_main_workflow_cwl_desc_no_lang(): ["The CWL Description SHOULD have a language of https://w3id.org/workflowhub/workflow-ro-crate#cwl"], profile_name="workflow-ro-crate" ) + + +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().main_workflow_no_files, + Severity.REQUIRED, + False, + ["Main Workflow file existence"], + ["Main Workflow", "not found in crate"], + profile_name="workflow-ro-crate" + ) + + +# The following two tests pass only if run singularly + +# 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().main_workflow_no_files, +# Severity.OPTIONAL, +# False, +# ["Workflow-related files existence"], +# ["Workflow diagram", "not found in crate"], +# profile_name="workflow-ro-crate" +# ) + + +# 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().main_workflow_no_files, +# Severity.OPTIONAL, +# False, +# ["Workflow-related files existence"], +# ["Workflow CWL description", "not found in crate"], +# profile_name="workflow-ro-crate" +# ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index eed5b620..dffa7861 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -200,3 +200,7 @@ def main_workflow_cwl_desc_bad_type(self) -> Path: @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_files(self) -> Path: + return self.base_path / "no_files" From 8d134a77e4745d63cf68098f610ffed0316d456a Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 12 Jun 2024 16:16:23 +0200 Subject: [PATCH 530/902] add check for wroc metadata file descriptor --- .../workflow-ro-crate/should/1_wroc_crate.ttl | 15 +++ .../ro-crate-metadata.json | 108 ++++++++++++++++++ .../workflow-ro-crate/test_wroc_descriptor.py | 23 ++++ tests/ro_crates.py | 9 ++ 4 files changed, 155 insertions(+) create mode 100644 tests/data/crates/invalid/2_wroc_descriptor/wroc_descriptor_bad_conforms_to/ro-crate-metadata.json create mode 100644 tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl index 722c3bde..53e787b0 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl @@ -9,6 +9,21 @@ @prefix bioschemas: . +ro:DescriptorProperties a sh:NodeShape ; + sh:name "WROC Metadata File Descriptor properties" ; + sh:description "Properties of the WROC Metadata File Descriptor" ; + sh:targetNode ro:ro-crate-metadata.json ; + sh:property [ + a sh:propertyShape ; + sh:name "Metadata File Descriptor conformsTo" ; + sh:description "The Metadata File Descriptor conformsTo SHOULD contain https://w3id.org/ro/crate/1.1 and https://w3id.org/workflowhub/workflow-ro-crate/1.0" ; + sh:path dct:conformsTo ; + sh:hasValue , + ; + sh:minCount 1 ; + sh:message "The Metadata File Descriptor conformsTo SHOULD contain https://w3id.org/ro/crate/1.1 and https://w3id.org/workflowhub/workflow-ro-crate/1.0" ; + ] . + ro:FindReadme a sh:NodeShape ; sh:name "Identify README.md" ; sh:description "README file for the Workflow RO-Crate" ; diff --git a/tests/data/crates/invalid/2_wroc_descriptor/wroc_descriptor_bad_conforms_to/ro-crate-metadata.json b/tests/data/crates/invalid/2_wroc_descriptor/wroc_descriptor_bad_conforms_to/ro-crate-metadata.json new file mode 100644 index 00000000..70a0f622 --- /dev/null +++ b/tests/data/crates/invalid/2_wroc_descriptor/wroc_descriptor_bad_conforms_to/ro-crate-metadata.json @@ -0,0 +1,108 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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" + } + ], + "license": "https://spdx.org/licenses/Apache-2.0.html", + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.1" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "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" + } + ] +} \ No newline at end of file diff --git a/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py b/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py new file mode 100644 index 00000000..a92c044b --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py @@ -0,0 +1,23 @@ +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import WROCInvalidConformsTo +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_wroc_descriptor_bad_conforms_to(): + """\ + Test a Workflow RO-Crate where the metadata file descriptor does not + contain the required URIs. + """ + do_entity_test( + WROCInvalidConformsTo().wroc_descriptor_bad_conforms_to, + Severity.RECOMMENDED, + False, + ["WROC Metadata File Descriptor properties"], + ["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_name="workflow-ro-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index dffa7861..72e06caa 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -204,3 +204,12 @@ def main_workflow_cwl_desc_no_lang(self) -> Path: @property def main_workflow_no_files(self) -> Path: return self.base_path / "no_files" + + +class WROCInvalidConformsTo: + + base_path = INVALID_CRATES_DATA_PATH / "2_wroc_descriptor" + + @property + def wroc_descriptor_bad_conforms_to(self) -> Path: + return self.base_path / "wroc_descriptor_bad_conforms_to" From 71bb7cdcddd28fcb14df78023e5b6c2000d2b9d5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 12 Jun 2024 17:42:18 +0200 Subject: [PATCH 531/902] fix(profiles/workflow-rocrate): :bug: remote : from sh:name and sh:description --- .../profiles/workflow-ro-crate/may/0_main-workflow.ttl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl index d6dda4bc..78df89ab 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl @@ -37,8 +37,8 @@ ro:MainWorkflowOptionalProperties a sh:NodeShape ; ro:CWLDescriptionProperties a sh:NodeShape ; - sh:name: "CWL Description properties" ; - sh:description: "Main Workflow CWL Description properties" ; + sh:name "CWL Description properties" ; + sh:description "Main Workflow CWL Description properties" ; sh:property [ a sh:PropertyShape ; sh:name "CWL Description type" ; From 522e007d15980b2927eb98df48a5c5a9fbe2921b Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 13 Jun 2024 10:01:55 +0200 Subject: [PATCH 532/902] wroc: add tests for readme --- .../workflow-ro-crate/should/1_wroc_crate.ttl | 2 +- .../workflow-ro-crate/test_wroc_readme.py | 35 +++++++++++++++++++ tests/ro_crates.py | 13 +++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl index 53e787b0..26a05a23 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl @@ -25,7 +25,7 @@ ro:DescriptorProperties a sh:NodeShape ; ] . ro:FindReadme a sh:NodeShape ; - sh:name "Identify README.md" ; + sh:name "README.md properties" ; sh:description "README file for the Workflow RO-Crate" ; sh:targetNode ro:README.md ; sh:property [ diff --git a/tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py b/tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py new file mode 100644 index 00000000..46e5a212 --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py @@ -0,0 +1,35 @@ +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import WROCInvalidReadme +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().wroc_readme_not_about_crate, + Severity.RECOMMENDED, + False, + ["README.md properties"], + ["The README.md SHOULD be about the crate"], + profile_name="workflow-ro-crate" + ) + + +def test_wroc_readme_wrong_encoding_format(): + """\ + Test a Workflow RO-Crate where the README.md has the wrong encodingFormat.. + """ + do_entity_test( + WROCInvalidReadme().wroc_readme_wrong_encoding_format, + Severity.RECOMMENDED, + False, + ["README.md properties"], + ["The README.md SHOULD have text/markdown as its encodingFormat"], + profile_name="workflow-ro-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 72e06caa..01b40ca9 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -213,3 +213,16 @@ class WROCInvalidConformsTo: @property def wroc_descriptor_bad_conforms_to(self) -> Path: return self.base_path / "wroc_descriptor_bad_conforms_to" + + +class WROCInvalidReadme: + + base_path = INVALID_CRATES_DATA_PATH / "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" From e65399fc1a082a2d4c928d70be52ece04c7b0148 Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 13 Jun 2024 10:29:26 +0200 Subject: [PATCH 533/902] rename workflow-ro-crate/may/0_main_workflow.py --- ...{0_main_workflow.py => 1_main_workflow.py} | 0 .../workflow-ro-crate/test_main_workflow.py | 58 +++++++++---------- 2 files changed, 28 insertions(+), 30 deletions(-) rename rocrate_validator/profiles/workflow-ro-crate/may/{0_main_workflow.py => 1_main_workflow.py} (100%) diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/0_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/may/0_main_workflow.py rename to rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py 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 0155aacb..cc553717 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py +++ b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py @@ -111,33 +111,31 @@ def test_main_workflow_file_existence(): ) -# The following two tests pass only if run singularly - -# 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().main_workflow_no_files, -# Severity.OPTIONAL, -# False, -# ["Workflow-related files existence"], -# ["Workflow diagram", "not found in crate"], -# profile_name="workflow-ro-crate" -# ) - - -# 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().main_workflow_no_files, -# Severity.OPTIONAL, -# False, -# ["Workflow-related files existence"], -# ["Workflow CWL description", "not found in crate"], -# profile_name="workflow-ro-crate" -# ) +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().main_workflow_no_files, + Severity.OPTIONAL, + False, + ["Workflow-related files existence"], + ["Workflow diagram", "not found in crate"], + profile_name="workflow-ro-crate" + ) + + +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().main_workflow_no_files, + Severity.OPTIONAL, + False, + ["Workflow-related files existence"], + ["Workflow CWL description", "not found in crate"], + profile_name="workflow-ro-crate" + ) From c7c67c2cce13bf369df808a2a57172195cbb8524 Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 13 Jun 2024 15:12:03 +0200 Subject: [PATCH 534/902] add check for wroc license --- .../must/1_wroc_root_data_entity.ttl | 27 ++++ .../no_license/ro-crate-metadata.json | 112 +++++++++++++++++ .../no_license/sort-and-change-case.ga | 118 ++++++++++++++++++ .../ro-crate-metadata.json | 113 +++++++++++++++++ .../sort-and-change-case.ga | 118 ++++++++++++++++++ .../valid/workflow-roc/ro-crate-metadata.json | 9 +- .../workflow-ro-crate/test_valid_wroc.py | 6 + .../test_wroc_root_metadata.py | 21 ++++ tests/ro_crates.py | 13 ++ 9 files changed, 536 insertions(+), 1 deletion(-) create mode 100644 rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl create mode 100644 tests/data/crates/invalid/1_wroc_crate/no_license/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/1_wroc_crate/no_license/sort-and-change-case.ga create mode 100644 tests/data/crates/valid/workflow-roc-string-license/ro-crate-metadata.json create mode 100644 tests/data/crates/valid/workflow-roc-string-license/sort-and-change-case.ga create mode 100644 tests/integration/profiles/workflow-ro-crate/test_wroc_root_metadata.py diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl b/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl new file mode 100644 index 00000000..6a420419 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl @@ -0,0 +1,27 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix rocrate: . +@prefix bioschemas: . + + +ro:WROCRootDataEntityRequiredProperties a sh:NodeShape ; + sh:name "WROC Root Data Entity Required Properties" ; + sh:description """Root Data Entity properties defined as MUST""" ; + sh:targetClass rocrate: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/tests/data/crates/invalid/1_wroc_crate/no_license/ro-crate-metadata.json b/tests/data/crates/invalid/1_wroc_crate/no_license/ro-crate-metadata.json new file mode 100644 index 00000000..7f490d69 --- /dev/null +++ b/tests/data/crates/invalid/1_wroc_crate/no_license/ro-crate-metadata.json @@ -0,0 +1,112 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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": "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" + ], + "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" + } + ] +} \ No newline at end of file diff --git a/tests/data/crates/invalid/1_wroc_crate/no_license/sort-and-change-case.ga b/tests/data/crates/invalid/1_wroc_crate/no_license/sort-and-change-case.ga new file mode 100644 index 00000000..5a199969 --- /dev/null +++ b/tests/data/crates/invalid/1_wroc_crate/no_license/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" +} diff --git a/tests/data/crates/valid/workflow-roc-string-license/ro-crate-metadata.json b/tests/data/crates/valid/workflow-roc-string-license/ro-crate-metadata.json new file mode 100644 index 00000000..4e2e350f --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-string-license/ro-crate-metadata.json @@ -0,0 +1,113 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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" + } + ], + "license": "Apache-2.0", + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@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" + ], + "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" + } + ] +} \ No newline at end of file diff --git a/tests/data/crates/valid/workflow-roc-string-license/sort-and-change-case.ga b/tests/data/crates/valid/workflow-roc-string-license/sort-and-change-case.ga new file mode 100644 index 00000000..5a199969 --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-string-license/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" +} diff --git a/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json b/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json index 4bb00a12..98e4b387 100644 --- a/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json +++ b/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json @@ -19,11 +19,18 @@ "@id": "README.md" } ], - "license": "https://spdx.org/licenses/Apache-2.0.html", + "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", 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 7e22abc7..b5652ee6 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py +++ b/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py @@ -15,3 +15,9 @@ def test_valid_workflow_roc_required(): True, profile_name="workflow-ro-crate" ) + do_entity_test( + ValidROC().workflow_roc_string_license, + Severity.REQUIRED, + True, + profile_name="workflow-ro-crate" + ) 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 new file mode 100644 index 00000000..32d62c17 --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_root_metadata.py @@ -0,0 +1,21 @@ +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import WROCNoLicense +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().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_name="workflow-ro-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 01b40ca9..d44586ad 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -28,6 +28,10 @@ def wrroc_paper_long_date(self) -> Path: def workflow_roc(self) -> Path: return VALID_CRATES_DATA_PATH / "workflow-roc" + @property + def workflow_roc_string_license(self) -> Path: + return VALID_CRATES_DATA_PATH / "workflow-roc-string-license" + class InvalidFileDescriptor: @@ -226,3 +230,12 @@ def wroc_readme_not_about_crate(self) -> Path: @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/" + + @property + def wroc_no_license(self) -> Path: + return self.base_path / "no_license" From a2b38af5c46c6de576848453359647ed771f6d66 Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 13 Jun 2024 16:33:54 +0200 Subject: [PATCH 535/902] added check for tests and examples directories --- .../workflow-ro-crate/may/2_wrroc_crate.ttl | 37 +++++++++++++++++++ .../valid/workflow-roc/ro-crate-metadata.json | 8 ++++ .../workflow-ro-crate/test_wroc_crate.py | 35 ++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl create mode 100644 tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl new file mode 100644 index 00000000..10834cab --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl @@ -0,0 +1,37 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix rocrate: . +@prefix bioschemas: . + +ro: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" ; + ] . + +ro: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/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json b/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json index 98e4b387..6e3efb6e 100644 --- a/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json +++ b/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json @@ -115,6 +115,14 @@ "@id": "./" }, "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" } ] } \ No newline at end of file diff --git a/tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py b/tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py new file mode 100644 index 00000000..b3b9d523 --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py @@ -0,0 +1,35 @@ +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import WROCNoLicense +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().wroc_no_license, + Severity.OPTIONAL, + False, + ["test directory"], + ["The test/ dir should be a Dataset"], + profile_name="workflow-ro-crate" + ) + + +def test_wroc_no_examples(): + """\ + Test a Workflow RO-Crate with no examples/ Dataset. + """ + do_entity_test( + WROCNoLicense().wroc_no_license, + Severity.OPTIONAL, + False, + ["examples directory"], + ["The examples/ dir should be a Dataset"], + profile_name="workflow-ro-crate" + ) From 32667af07c8b635f8e9f91569683fe47958fe7de Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 17 Jun 2024 14:07:21 +0200 Subject: [PATCH 536/902] fix valid wroc --- tests/data/crates/valid/workflow-roc/ro-crate-metadata.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json b/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json index 6e3efb6e..9d0e4bec 100644 --- a/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json +++ b/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json @@ -17,6 +17,12 @@ }, { "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" } ], "license": { From e5b9ddb98e78d4957bd80fe67bcfca785902c8d4 Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 17 Jun 2024 14:31:00 +0200 Subject: [PATCH 537/902] wroc: add check for bioschemas conformancee --- .../should/2_main-workflow.ttl | 25 ++++ .../ro-crate-metadata.json | 137 ++++++++++++++++++ .../sort-and-change-case.ga | 118 +++++++++++++++ .../valid/workflow-roc/ro-crate-metadata.json | 3 + .../workflow-ro-crate/test_main_workflow.py | 15 ++ tests/ro_crates.py | 4 + 6 files changed, 302 insertions(+) create mode 100644 rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl create mode 100644 tests/data/crates/invalid/0_main_workflow/main_workflow_bad_conformsto/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/0_main_workflow/main_workflow_bad_conformsto/sort-and-change-case.ga diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl new file mode 100644 index 00000000..04712d32 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl @@ -0,0 +1,25 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix rocrate: . +@prefix bioschemas: . + + +ro:MainWorkflowRecommendedProperties a sh:NodeShape ; + sh:name "Main Workflow recommended properties" ; + sh:description """Main Workflow properties defined as SHOULD"""; + sh:targetClass rocrate: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/invalid/0_main_workflow/main_workflow_bad_conformsto/ro-crate-metadata.json b/tests/data/crates/invalid/0_main_workflow/main_workflow_bad_conformsto/ro-crate-metadata.json new file mode 100644 index 00000000..0f454e80 --- /dev/null +++ b/tests/data/crates/invalid/0_main_workflow/main_workflow_bad_conformsto/ro-crate-metadata.json @@ -0,0 +1,137 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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://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/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/" + } + }, + { + "@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" + } + ] +} \ No newline at end of file diff --git a/tests/data/crates/invalid/0_main_workflow/main_workflow_bad_conformsto/sort-and-change-case.ga b/tests/data/crates/invalid/0_main_workflow/main_workflow_bad_conformsto/sort-and-change-case.ga new file mode 100644 index 00000000..5a199969 --- /dev/null +++ b/tests/data/crates/invalid/0_main_workflow/main_workflow_bad_conformsto/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" +} diff --git a/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json b/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json index 9d0e4bec..22014d97 100644 --- a/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json +++ b/tests/data/crates/valid/workflow-roc/ro-crate-metadata.json @@ -59,6 +59,9 @@ "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" 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 cc553717..3b96ce11 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py +++ b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py @@ -139,3 +139,18 @@ def test_workflow_description_file_existence(): ["Workflow CWL description", "not found in crate"], profile_name="workflow-ro-crate" ) + + +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().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_name="workflow-ro-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index d44586ad..074923fd 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -209,6 +209,10 @@ def main_workflow_cwl_desc_no_lang(self) -> Path: def main_workflow_no_files(self) -> Path: return self.base_path / "no_files" + @property + def main_workflow_bad_conformsto(self) -> Path: + return self.base_path / "main_workflow_bad_conformsto" + class WROCInvalidConformsTo: From 880979035b08568fbd3313af507341e2d8d22076 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 17:57:48 +0200 Subject: [PATCH 538/902] feat(core): :sparkles: define the default filename for the profile specification --- rocrate_validator/constants.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index a4d53ca1..754da0d7 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -7,6 +7,9 @@ # Define RDF syntax namespace RDF_SYNTAX_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +# Define the file name for the profile specification conforms to the Profiles Vocabulary +PROFILE_SPECIFICATION_FILE = "profile.ttl" + # Define the rocrate-metadata.json file name ROCRATE_METADATA_FILE = "ro-crate-metadata.json" From 644f73ab62aed36dad402e6ce2954fffb94ca9d2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 18:00:15 +0200 Subject: [PATCH 539/902] feat(core): :sparkles: globally define the PROF and SCHEMA.org Namespaces --- rocrate_validator/constants.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index 754da0d7..a2ac7c86 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -1,9 +1,17 @@ # Define allowed RDF extensions and serialization formats as map import typing +from rdflib import Namespace + # Define SHACL namespace SHACL_NS = "http://www.w3.org/ns/shacl#" +# Define the Profiles Vocabulary namespace +PROF_NS = Namespace("http://www.w3.org/ns/dx/prof/") + +# Define the Schema.org namespace +SCHEMA_ORG_NS = Namespace("http://schema.org/") + # Define RDF syntax namespace RDF_SYNTAX_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" From 90b1d71dc5c8fad80766b71934639dd35df7738a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 18:12:49 +0200 Subject: [PATCH 540/902] feat(utils): :sparkles: add `MultiIndexMap` class --- rocrate_validator/utils.py | 75 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index dc8794c0..afec0d87 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -225,3 +225,78 @@ def to_camel_case(snake_str: str) -> str: """ components = re.split('_|-', snake_str) return components[0].capitalize() + ''.join(x.title() for x in components[1:]) + + +class MapIndex: + + def __init__(self, name: str, unique: bool = False): + self.name = name + self.unique = unique + + +class MultiIndexMap: + def __init__(self, key: str = "id", indexes: list[MapIndex] = None): + self._key = key + # initialize an empty dictionary to store the indexes + self._indices: list[MapIndex] = {} + if indexes: + for index in indexes: + self.add_index(index) + # initialize an empty dictionary to store the data + self._data = {} + + @property + def key(self) -> str: + return self._key + + @property + def keys(self) -> list[str]: + return list(self._data.keys()) + + @property + def indices(self) -> list[str]: + return list(self._indices.keys()) + + def add_index(self, index: MapIndex): + self._indices[index.name] = {"__meta__": index} + + def remove_index(self, index_name: str): + self._indices.pop(index_name) + + def get_index(self, index_name: str) -> MapIndex: + return self._indices.get(index_name)["__meta__"] + + def add(self, key, obj, **indices): + self._data[key] = obj + for index_name, index_value in indices.items(): + index = self.get_index(index_name) + assert isinstance(index, MapIndex), f"Index {index_name} does not exist" + if index_name in self._indices: + if index_value not in self._indices[index_name]: + self._indices[index_name][index_value] = set() if not index.unique else key + if not index.unique: + self._indices[index_name][index_value].add(key) + + def remove(self, key): + obj = self._data.pop(key) + for index_name, index in self._indices.items(): + index_value = getattr(obj, index_name) + if index_value in index: + index[index_value].remove(key) + + def values(self): + return self._data.values() + + def get_by_key(self, key): + return self._data.get(key) + + def get_by_index(self, index_name, index_value): + if index_name == self._key: + return self._data.get(index_value) + index = self.get_index(index_name) + assert isinstance(index, MapIndex), f"Index {index_name} does not exist" + if index.unique: + key = self._indices.get(index_name, {}).get(index_value) + return self._data.get(key) + keys = self._indices.get(index_name, {}).get(index_value, set()) + return [self._data[key] for key in keys] From 48f646170217ac7dbf382d2f08e5d73a3ce0a165 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 18:31:53 +0200 Subject: [PATCH 541/902] feat(core): :sparkles: load the `profile.ttl` specification when profile is instantiated --- rocrate_validator/errors.py | 50 +++++++++++++++++++++++++++++++++++++ rocrate_validator/models.py | 44 +++++++++++++++++++++++++++----- 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 81de638b..8c0b61b4 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -60,6 +60,56 @@ def __repr__(self): return f"ProfileNotFound({self._profile_name!r})" +class ProfileSpecificationNotFound(ROCValidatorError): + """Raised when the profile specification is not found.""" + + def __init__(self, profile_name: Optional[str] = None, spec_file: Optional[str] = None): + self._profile_name = profile_name + self._spec_file = spec_file + + @property + def profile_name(self) -> Optional[str]: + """The name of the profile.""" + return self._profile_name + + @property + def spec_file(self) -> Optional[str]: + """The name of the profile specification file.""" + return self._spec_file + + def __str__(self) -> str: + msg = f"Unable to find the `profile.ttl` specification for the profile \"{self._profile_name!r}\"" + if self._spec_file: + msg += f" in the file {self._spec_file!r}" + return msg + + def __repr__(self): + return f"ProfileSpecificationNotFound({self._profile_name!r})" + + +class ProfileSpecificationError(ROCValidatorError): + + def __init__(self, profile_name: Optional[str] = None, message: Optional[str] = None): + self._profile_name = profile_name + self._message = message + + @property + def profile_name(self) -> Optional[str]: + """The name of the profile.""" + return self._profile_name + + @property + def message(self) -> Optional[str]: + """The error message.""" + return self._message + + def __str__(self) -> str: + return f"Error in the `profile.ttl` specification for the profile \"{self._profile_name!r}\": {self._message!r}" + + def __repr__(self): + return f"ProfileSpecificationError({self._profile_name!r}, {self._message!r})" + + class DuplicateRequirementCheck(ROCValidatorError): """Raised when a duplicate requirement check is found.""" diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index b8142680..a3a79a43 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -4,27 +4,30 @@ import enum import inspect from abc import ABC, abstractmethod -from collections import OrderedDict from collections.abc import Collection from dataclasses import asdict, dataclass from functools import total_ordering from pathlib import Path from typing import Optional, Union -from rdflib import Graph +from rdflib import RDF, RDFS, Graph, Namespace, URIRef import rocrate_validator.log as logging from rocrate_validator.constants import (DEFAULT_ONTOLOGY_FILE, DEFAULT_PROFILE_NAME, DEFAULT_PROFILE_README_FILE, - IGNORED_PROFILE_DIRECTORIES, + IGNORED_PROFILE_DIRECTORIES, PROF_NS, PROFILE_FILE_EXTENSIONS, + PROFILE_SPECIFICATION_FILE, RDF_SERIALIZATION_FORMATS_TYPES, - ROCRATE_METADATA_FILE, + ROCRATE_METADATA_FILE, SCHEMA_ORG_NS, VALID_INFERENCE_OPTIONS_TYPES) from rocrate_validator.errors import (DuplicateRequirementCheck, - InvalidProfilePath, ProfileNotFound) -from rocrate_validator.utils import (get_profiles_path, + InvalidProfilePath, ProfileNotFound, + ProfileSpecificationError, + ProfileSpecificationNotFound) +from rocrate_validator.utils import (MapIndex, MultiIndexMap, + get_profiles_path, get_requirement_name_from_file) # set the default profiles path @@ -122,6 +125,10 @@ def get(name: str) -> RequirementLevel: @total_ordering class Profile: + + # store the map of profiles: profile URI -> Profile instance + __profiles_map: MultiIndexMap = MultiIndexMap("uri", indexes=[MapIndex("name"), MapIndex("token", unique=True)]) + def __init__(self, name: str, path: Path, requirements: Optional[list[Requirement]] = None, publicID: Optional[str] = None, @@ -133,6 +140,31 @@ def __init__(self, name: str, path: Path, self._publicID = publicID self._severity = severity + # init property to store the RDF node which is the root of the profile specification graph + self._profile_node = None + + # init property to store the RDF graph of the profile specification + self._profile_specification_graph = None + + # check if the profile specification file exists + spec_file = self.profile_specification_file_path + if not spec_file or not spec_file.exists(): + raise ProfileSpecificationNotFound(name, spec_file) + # load the profile specification expressed using the Profiles Vocabulary + profile = Graph() + profile.parse(str(spec_file), format="turtle") + # check that the specification Graph hosts only one profile + profiles = list(profile.subjects(predicate=RDF.type, object=PROF_NS.Profile)) + if len(profiles) == 1: + self._profile_node = profiles[0] + self._profile_specification_graph = profile + self.__profiles_map.add( + self._profile_node.toPython(), self, token=self.token, name=self.name) # add the profile to the profiles map + else: + raise ProfileSpecificationError( + profile_name=name, message=f"Profile specification file {spec_file} must contain exactly one profile") + + @property def path(self): return self._path From 7a99b21f122deb610d1584b2d02c413d3dfaf1bd Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 18:34:18 +0200 Subject: [PATCH 542/902] feat(core): :sparkles: add getters for the properties of the profile specification --- rocrate_validator/models.py | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index a3a79a43..db8bbaad 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -164,6 +164,13 @@ def __init__(self, name: str, path: Path, raise ProfileSpecificationError( profile_name=name, message=f"Profile specification file {spec_file} must contain exactly one profile") + def __get_specification_property__(self, property: str, namespace: Namespace, + pop_first: bool = True, as_Python_object: bool = True) -> Union[str, list[Union[str, URIRef]]]: + assert self._profile_specification_graph is not None, "Profile specification graph not loaded" + values = list(self._profile_specification_graph.objects(self._profile_node, namespace[property])) + if values and as_Python_object: + values = [v.toPython() for v in values] + return values[0] if values and len(values) >= 1 and pop_first else values @property def path(self): @@ -173,10 +180,46 @@ def path(self): def name(self): return self._name + @property + def profile_node(self): + return self._profile_node + + @property + def token(self): + return self.__get_specification_property__("hasToken", PROF_NS) or self._name.lower().replace(" ", "_") + + @property + def uri(self): + return self._profile_node.toPython() + + @property + def label(self): + return self.__get_specification_property__("label", RDFS) + + @property + def comment(self): + return self.__get_specification_property__("comment", RDFS) + + @property + def version(self): + return self.__get_specification_property__("version", SCHEMA_ORG_NS) + + @property + def is_profile_of(self) -> list[str]: + return self.__get_specification_property__("isProfileOf", PROF_NS, pop_first=False) + + @property + def is_transitive_profile_of(self) -> list[str]: + return self.__get_specification_property__("isTransitiveProfileOf", PROF_NS, pop_first=False) + @property def readme_file_path(self) -> Path: return self.path / DEFAULT_PROFILE_README_FILE + @property + def profile_specification_file_path(self) -> Path: + return self.path / PROFILE_SPECIFICATION_FILE + @property def publicID(self) -> Optional[str]: return self._publicID From 67356749837a8e7311ff63d72ad50887dc0f982c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 18:36:07 +0200 Subject: [PATCH 543/902] feat(core): :sparkles: compute profile dependencies according to the specs properties --- rocrate_validator/models.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index db8bbaad..1fbbe112 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -252,11 +252,31 @@ def get_requirements( if (not exact_match and requirement.severity >= severity) or (exact_match and requirement.severity == severity)] + @classmethod + def __get_nested_profiles__(cls, source: str) -> list[str]: + result = [] + visited = [] + queue = [source] + while len(queue) > 0: + p = queue.pop() + if not p in visited: + visited.append(p) + profile = cls.__profiles_map.get_by_key(p) + inherited_profiles = profile.is_profile_of + for p in sorted(inherited_profiles, reverse=True): + if not p in visited: + queue.append(p) + if not p in result: + result.insert(0, p) + return result + @property def inherited_profiles(self) -> list[Profile]: - profiles = [_ for _ in Profile.load_profiles(self.path.parent).values() if _ < self] - logger.debug("Inherited profiles: %s", profiles) - return profiles + inherited_profiles = self.is_transitive_profile_of + if not inherited_profiles or len(inherited_profiles) == 0: + inherited_profiles = Profile.__get_nested_profiles__(self.uri) + profile_keys = self.__profiles_map.keys + return [self.__profiles_map.get_by_key(_) for _ in inherited_profiles if _ in profile_keys] def add_requirement(self, requirement: Requirement): self._requirements.append(requirement) From ed8fe414c5b11bc5d007bc9faf9de5abd5b84ca2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 18:38:19 +0200 Subject: [PATCH 544/902] refactor(core): :recycle: refactor profiles loader --- rocrate_validator/models.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 1fbbe112..3364cfb8 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -323,7 +323,6 @@ def __str__(self) -> str: # return self.get_requirement(name) is not None # def get_requirements_by_type(self, type: RequirementLevel) -> list[Requirement]: - # return [requirement for requirement in self.requirements if requirement.severity == type] @staticmethod def load(path: Union[str, Path], @@ -343,22 +342,24 @@ def load(path: Union[str, Path], @staticmethod def load_profiles(profiles_path: Union[str, Path], publicID: Optional[str] = None, - severity: Severity = Severity.REQUIRED, - reverse_order: bool = False) -> OrderedDict[str, Profile]: + severity: Severity = Severity.REQUIRED) -> list[Profile]: # if the path is a string, convert it to a Path if isinstance(profiles_path, str): profiles_path = Path(profiles_path) # check if the path is a directory - assert profiles_path.is_dir(), f"Invalid profiles path: {profiles_path}" - # initialize the profiles - profiles = {} - # iterate through the directories + if not profiles_path.is_dir(): + raise InvalidProfilePath(profiles_path) + # initialize the profiles list + profiles = [] + # iterate through the directories and load the profiles for profile_path in profiles_path.iterdir(): logger.debug("Checking profile path: %s %s %r", profile_path, profile_path.is_dir(), IGNORED_PROFILE_DIRECTORIES) if profile_path.is_dir() and profile_path not in IGNORED_PROFILE_DIRECTORIES: profile = Profile.load(profile_path, publicID=publicID, severity=severity) - profiles[profile.name] = profile + profiles.append(profile) + # order profiles according to the dependencies between them: first the profiles that do not depend on ??? + return profiles return OrderedDict(sorted(profiles.items(), key=lambda x: x, reverse=reverse_order)) From b0b30f55b349b562881827f6c96f46bbfa471936 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 18:41:05 +0200 Subject: [PATCH 545/902] refactor(core): :fire: clean up --- rocrate_validator/models.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 3364cfb8..0f936da7 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -308,22 +308,6 @@ def __repr__(self) -> str: def __str__(self) -> str: return self.name - # def get_requirement(self, name: str) -> Requirement: - # for requirement in self.requirements: - # if requirement.name == name: - # return requirement - # return None - - # @property - # def requirements_by_severity_map(self) -> dict[Severity, list[Requirement]]: - # return {level.severity: self.get_requirements_by_type(level.severity) - # for level in LevelCollection.all()} - - # def has_requirement(self, name: str) -> bool: - # return self.get_requirement(name) is not None - - # def get_requirements_by_type(self, type: RequirementLevel) -> list[Requirement]: - @staticmethod def load(path: Union[str, Path], publicID: Optional[str] = None, From cee59fef3d263189a611e516ee95e10689183f86 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 18:42:20 +0200 Subject: [PATCH 546/902] feat(core): :sparkles: add methods to get profiles by different criteria --- rocrate_validator/models.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 0f936da7..2697b632 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -345,7 +345,21 @@ def load_profiles(profiles_path: Union[str, Path], # order profiles according to the dependencies between them: first the profiles that do not depend on ??? return profiles - return OrderedDict(sorted(profiles.items(), key=lambda x: x, reverse=reverse_order)) + @classmethod + def get_by_uri(cls, uri: str) -> Profile: + return cls.__profiles_map.get_by_key(uri) + + @classmethod + def get_by_name(cls, name: str) -> list[Profile]: + return cls.__profiles_map.get_by_index("name", name) + + @classmethod + def get_by_token(cls, token: str) -> Profile: + return cls.__profiles_map.get_by_index("token", token) + + @classmethod + def all(cls) -> list[Profile]: + return cls.__profiles_map.values() @total_ordering From 9f6b32e4b09b3e6a8c09b6726b1bf6c32029a2e2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 18:43:37 +0200 Subject: [PATCH 547/902] fix(core): :bug: skip `profile.ttl` during the shape parsing --- rocrate_validator/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 2697b632..6356f936 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -565,6 +565,7 @@ def ok_file(p: Path) -> bool: return p.is_file() \ and p.suffix in PROFILE_FILE_EXTENSIONS \ and not p.name == DEFAULT_ONTOLOGY_FILE \ + and not p.name == PROFILE_SPECIFICATION_FILE \ and not p.name.startswith('.') \ and not p.name.startswith('_') From acbb2018a36d0b1787dcf397d2e1223763280625 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 18:47:38 +0200 Subject: [PATCH 548/902] feat(core): :sparkles: add setting to disable check for duplicates --- rocrate_validator/models.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 6356f936..a849f7a5 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -904,6 +904,7 @@ class ValidationSettings: profile_name: str = DEFAULT_PROFILE_NAME inherit_profiles: bool = True allow_shapes_override: bool = True + disable_check_for_duplicates: bool = False # Ontology and inference settings ontology_path: Optional[Path] = None inference: Optional[VALID_INFERENCE_OPTIONS_TYPES] = None @@ -1107,7 +1108,14 @@ def inheritance_enabled(self) -> bool: def profile_name(self) -> str: return self.settings.get("profile_name") - def __load_profiles__(self) -> OrderedDict[str, Profile]: + @property + def allow_shapes_override(self) -> bool: + return self.settings.get("allow_shapes_override", True) + + @property + def disable_check_for_duplicates(self) -> bool: + return self.settings.get("disable_check_for_duplicates", False) + if not self.inheritance_enabled: profile = Profile.load( self.profiles_path / self.profile_name, From fe968e4ca5813358b6a4d81ff3bf87cced5a8f99 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 19:01:06 +0200 Subject: [PATCH 549/902] refactor(core): :recycle: refactor profile loading --- rocrate_validator/models.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index a849f7a5..32344bb1 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1116,21 +1116,34 @@ def allow_shapes_override(self) -> bool: def disable_check_for_duplicates(self) -> bool: return self.settings.get("disable_check_for_duplicates", False) + def __load_profiles__(self) -> list[Profile]: + + # if the inheritance is disabled, load only the target profile if not self.inheritance_enabled: profile = Profile.load( self.profiles_path / self.profile_name, publicID=self.publicID, severity=self.requirement_severity) - return {profile.name: profile} - profiles = {pn: p for pn, p in Profile.load_profiles( + return [profile] + + # load all profiles + profiles = Profile.load_profiles( self.profiles_path, publicID=self.publicID, - severity=self.requirement_severity, - reverse_order=False).items() if pn <= self.profile_name} + severity=self.requirement_severity) + # Check if the target profile is in the list of profiles - if self.profile_name not in profiles: + profile = Profile.get_by_token(self.profile_name) or Profile.get_by_name(self.profile_name)[0] + if profile is None: raise ProfileNotFound(f"Profile '{self.profile_name}' not found in '{self.profiles_path}'") + # Set the profiles to validate against as the target profile and its inherited profiles + profiles = profile.inherited_profiles + [profile] + + # if the check for duplicates is disabled, return the profiles + if self.disable_check_for_duplicates: + return profiles + # navigate the profiles and check for overridden checks # if the override is enabled in the settings # overridden checks should be marked as such @@ -1138,12 +1151,12 @@ def disable_check_for_duplicates(self) -> bool: profiles_checks = {} # visit the profiles in reverse order # (the order is important to visit the most specific profiles first) - for profile in sorted(profiles.values(), reverse=True): + for profile in sorted(profiles, reverse=True): profile_checks = [_ for r in profile.get_requirements() for _ in r.get_checks()] profile_check_names = [] for check in profile_checks: # ย find duplicated checks and raise an error - if check.name in profile_check_names: + if check.name in profile_check_names and not self.allow_shapes_override: raise DuplicateRequirementCheck(check.name, profile.name) # ย add check to the list profile_check_names.append(check.name) @@ -1160,7 +1173,7 @@ def disable_check_for_duplicates(self) -> bool: return profiles @property - def profiles(self) -> OrderedDict[str, Profile]: + def profiles(self) -> list[Profile]: if not self._profiles: self._profiles = self.__load_profiles__() return self._profiles.copy() From 52b9abac76b23ebfb5935004f462c499243ffb37 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 19:03:13 +0200 Subject: [PATCH 550/902] feat(core): :sparkles: allow to get profile by token --- rocrate_validator/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 32344bb1..bc5add1f 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1177,3 +1177,9 @@ def profiles(self) -> list[Profile]: if not self._profiles: self._profiles = self.__load_profiles__() return self._profiles.copy() + + def get_profile_by_token(self, token: str) -> Profile: + for p in self.profiles: + if p.token == token: + return p + raise ProfileNotFound(f"Profile with token '{token}' not found") From cd1be0f0a7959fc84b8eee6fc342d6783ae71434 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 19:05:28 +0200 Subject: [PATCH 551/902] refactor(core): :recycle: update initialisation of profiles before validation --- rocrate_validator/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index bc5add1f..a239d6b9 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -989,11 +989,11 @@ def __do_validate__(self, context = ValidationContext(self, self.validation_settings.to_dict()) # set the profiles to validate against - profiles = context.profiles.values() - logger.debug("Profiles to validate: %r", profiles) + profiles = context.profiles + assert len(profiles) > 0, "No profiles to validate" for profile in profiles: - logger.debug("Validating profile %s", profile.name) + logger.debug("Validating profile %s (id: %s)", profile.name, profile.token) # perform the requirements validation requirements = profile.get_requirements( context.requirement_severity, exact_match=context.requirement_severity_only) From 6d8a884b29d13aa4e537b41e4b981840d0881ccd Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 19:09:18 +0200 Subject: [PATCH 552/902] refactor(core): :recycle: remove hardwired default profile name --- rocrate_validator/services.py | 3 ++- tests/shared.py | 3 ++- tests/unit/requirements/test_profiles.py | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index f164e435..4b084f96 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -2,6 +2,7 @@ from typing import Union import rocrate_validator.log as logging +from rocrate_validator.constants import DEFAULT_PROFILE_NAME from .models import (Profile, Severity, ValidationResult, ValidationSettings, Validator) @@ -40,7 +41,7 @@ def get_profiles(profiles_path: Path = DEFAULT_PROFILES_PATH, publicID: str = No def get_profile(profiles_path: Path = DEFAULT_PROFILES_PATH, - profile_name: str = "ro-crate", publicID: str = None) -> Profile: + profile_name: str = DEFAULT_PROFILE_NAME, publicID: str = None) -> Profile: """ Load the profiles from the given path """ diff --git a/tests/shared.py b/tests/shared.py index 101f14d9..f1c46621 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -8,6 +8,7 @@ from typing import Optional, TypeVar, Union from rocrate_validator import models, services +from rocrate_validator.constants import DEFAULT_PROFILE_NAME logger = logging.getLogger(__name__) @@ -26,7 +27,7 @@ def do_entity_test( expected_triggered_requirements: Optional[list[str]] = None, expected_triggered_issues: Optional[list[str]] = None, abort_on_first: bool = True, - profile_name: str = "ro-crate" + profile_name: str = DEFAULT_PROFILE_NAME ): """ Shared function to test a RO-Crate entity diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 5a6a37e3..bdc5141e 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -3,6 +3,7 @@ import pytest +from rocrate_validator.constants import DEFAULT_PROFILE_NAME from rocrate_validator.errors import DuplicateRequirementCheck, InvalidProfilePath from rocrate_validator.models import (Profile, ValidationContext, ValidationSettings, Validator) @@ -37,8 +38,8 @@ def test_order_of_loaded_profiles(profiles_path: str): def test_load_invalid_profile_from_validation_context(fake_profiles_path: str): """Test the loaded profiles from the validator context.""" settings = { - "profiles_path": fake_profiles_path, - "profile_name": "ro-crate", + "profiles_path": "/tmp/random_path_xxx", + "profile_name": DEFAULT_PROFILE_NAME, "data_path": "/tmp/random_path", "inherit_profiles": False } From 2089e0ef6bad9efcb3f9176730f7151a3b925015 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 19:11:08 +0200 Subject: [PATCH 553/902] fix(services): :bug: fix missing severity of the `get_profiles` service method --- rocrate_validator/services.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 4b084f96..345ed7dc 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -31,11 +31,11 @@ def validate(settings: Union[dict, ValidationSettings]) -> ValidationResult: return result -def get_profiles(profiles_path: Path = DEFAULT_PROFILES_PATH, publicID: str = None) -> dict[str, Profile]: +def get_profiles(profiles_path: Path = DEFAULT_PROFILES_PATH, publicID: str = None, severity=Severity.OPTIONAL) -> list: """ Load the profiles from the given path """ - profiles = Profile.load_profiles(profiles_path, publicID=publicID) + profiles = Profile.load_profiles(profiles_path, publicID=publicID, severity=severity) logger.debug("Profiles loaded: %s", profiles) return profiles From da2cb6dcc0b3d5ae806823c8ed945292fc78460d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 19:13:26 +0200 Subject: [PATCH 554/902] refactor(core): :recycle: remove hardwired default profile name --- 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 1ecfd8e9..2c0efebb 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -85,7 +85,7 @@ @click.pass_context def validate(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH, - profile_name: str = "ro-crate", + profile_name: str = DEFAULT_PROFILE_NAME, disable_profile_inheritance: bool = False, requirement_severity: str = Severity.REQUIRED.name, requirement_severity_only: bool = False, From f99bb1551c6344ee08fe983ce3451780f20df3be Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 19:18:52 +0200 Subject: [PATCH 555/902] feat(shacl): :sparkles: add time evaluation to some steps of the validation procedure --- .../requirements/shacl/checks.py | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 90b237e4..38f4a3b7 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -1,3 +1,4 @@ +from timeit import default_timer as timer from typing import Optional import rocrate_validator.log as logging @@ -61,30 +62,50 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): shapes_registry = shacl_context.shapes_registry # set up the input data for the validator + start_time = timer() ontology_graph = shacl_context.ontology_graph + end_time = timer() + logger.debug(f"Execution time for getting ontology graph: {end_time - start_time} seconds") + + start_time = timer() data_graph = shacl_context.data_graph + end_time = timer() + logger.debug(f"Execution time for getting data graph: {end_time - start_time} seconds") + + # Begin the timer + start_time = timer() shapes_graph = shapes_registry.shapes_graph + end_time = timer() + logger.debug(f"Execution time for getting shapes: {end_time - start_time} seconds") - # uncomment to save the graphs to the logs folder (for debugging purposes) + # # uncomment to save the graphs to the logs folder (for debugging purposes) + # start_time = timer() # data_graph.serialize("logs/data_graph.ttl", format="turtle") # shapes_graph.serialize("logs/shapes_graph.ttl", format="turtle") # if ontology_graph: # ontology_graph.serialize("logs/ontology_graph.ttl", format="turtle") + # end_time = timer() + # logger.debug(f"Execution time for saving graphs: {end_time - start_time} seconds") # validate the data graph + start_time = timer() shacl_validator = SHACLValidator(shapes_graph=shapes_graph, ont_graph=ontology_graph) shacl_result = shacl_validator.validate( data_graph=data_graph, ontology_graph=ontology_graph, **shacl_context.settings) # parse the validation result + end_time = timer() logger.debug("Validation '%s' conforms: %s", self.name, shacl_result.conforms) + logger.debug(f"Execution time for validating the data graph: {end_time - start_time} seconds") + # store the validation result in the context + start_time = timer() result = shacl_result.conforms # if the validation failed, add the issues to the context if not shacl_result.conforms: logger.debug("Validation failed") logger.debug("Parsing Validation result: %s", result) for violation in shacl_result.violations: - shape = shapes_registry.get_shape(Shape.compute_key(shacl_context.shapes_graph, violation.sourceShape)) + shape = shapes_registry.get_shape(Shape.compute_key(shapes_graph, violation.sourceShape)) assert shape is not None, "Unable to map the violation to a shape" requirementCheck = SHACLCheck.get_instance(shape) assert requirementCheck is not None, "The requirement check cannot be None" @@ -102,6 +123,8 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): logger.debug("Added validation issue to the context: %s", c) if shacl_context.base_context.fail_fast: break + end_time = timer() + logger.debug(f"Execution time for parsing the validation result: {end_time - start_time} seconds") return result From 89115cd5ad7c52bd889f06b69504c9bb352e25e0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 19:19:41 +0200 Subject: [PATCH 556/902] fix(shacl): --- rocrate_validator/requirements/shacl/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index ee53f240..d7b784a8 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -22,6 +22,7 @@ class SHACLNode: # define default values name: str = None description: str = None + severity: str = None def __init__(self, node: Node, graph: Graph, parent: Optional[SHACLNode] = None): From f9c1b286869db142deb9fda2bda7f4418f75e42d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 19:21:27 +0200 Subject: [PATCH 557/902] refactor(shacl): :recycle: update SHACL context initialisation --- .../requirements/shacl/validator.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index c5096c96..df95a787 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -49,10 +49,12 @@ def __enter__(self) -> SHACLValidationContext: if not self._shacl_context.__set_current_validation_profile__(self._profile): raise SHACLValidationAlreadyProcessed( self._profile.name, self._shacl_context.get_validation_result(self._profile)) + logger.debug("Processing profile: %s (id: %s)", self._profile.name, self._profile.token) if self._context.settings.get("target_only_validation", False) and \ - self._profile.name != self._context.settings.get("profile_name", None): + self._profile.token != self._context.settings.get("profile_name", None): logger.debug("Skipping validation of profile %s", self._profile.name) raise SHACLValidationSkip(f"Skipping validation of profile {self._profile.name}") + logger.debug("ValidationContext of profile %s initialized", self._profile.name) return self._shacl_context def __exit__(self, exc_type, exc_val, exc_tb) -> None: @@ -96,13 +98,16 @@ def __init__(self, context: ValidationContext): self._ontology_graph: Graph = Graph() def __set_current_validation_profile__(self, profile: Profile) -> bool: - if not profile.name in self._processed_profiles: + if not profile.token in self._processed_profiles: # augment the ontology graph with the profile ontology - self._ontology_graph += self.__load_ontology_graph__(profile.name) + 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() profile_shapes_graph = profile_registry.shapes_graph + logger.debug("Loaded shapes: %s", profile_shapes) # enable overriding of checks if self.settings.get("override_checks", False): @@ -147,11 +152,11 @@ def current_validation_result(self, result: ValidationResult): # store the validation result self._validation_result = result # mark the profile as processed and store the result - self._processed_profiles[self._current_validation_profile.name] = result + self._processed_profiles[self._current_validation_profile.token] = result def get_validation_result(self, profile: Profile) -> Optional[bool]: assert profile is not None, "Invalid profile" - return self._processed_profiles.get(profile.name, None) + return self._processed_profiles.get(profile.token, None) @property def result(self) -> ValidationResult: @@ -165,9 +170,9 @@ def shapes_registry(self) -> ShapesRegistry: def shapes_graph(self) -> Graph: return self.shapes_registry.shapes_graph - def __get_ontology_path__(self, profile_name: str, ontology_filename: str = DEFAULT_ONTOLOGY_FILE) -> Path: + def __get_ontology_path__(self, profile_path: Path, ontology_filename: str = DEFAULT_ONTOLOGY_FILE) -> Path: if not self._ontology_path: - supported_path = f"{self.profiles_path}/{profile_name}/{ontology_filename}" + supported_path = f"{profile_path}/{ontology_filename}" if self.settings.get("ontology_path", None): logger.warning("Detected an ontology path. Custom ontology file is not yet supported." f"Use {supported_path} to provide an ontology for your profile.") @@ -175,15 +180,16 @@ def __get_ontology_path__(self, profile_name: str, ontology_filename: str = DEFA self._ontology_path = Path(supported_path) return self._ontology_path - def __load_ontology_graph__(self, profile_name: str, ontology_filename: Optional[str] = DEFAULT_ONTOLOGY_FILE) -> Graph: + def __load_ontology_graph__(self, profile_path: Path, ontology_filename: Optional[str] = DEFAULT_ONTOLOGY_FILE) -> Graph: # load the graph of ontologies ontology_graph = None - ontology_path = self.__get_ontology_path__(profile_name, ontology_filename) + ontology_path = self.__get_ontology_path__(profile_path, ontology_filename) if os.path.exists(ontology_path): logger.debug("Loading ontologies: %s", ontology_path) ontology_graph = Graph() ontology_graph.parse(ontology_path, format="ttl", publicID=self.publicID) + logger.debug("Ontologies loaded: %s", ontology_graph) return ontology_graph @property From da3a5d90861b8b7d71b938ed88be912a89f88bbf Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 19:22:32 +0200 Subject: [PATCH 558/902] test(profiles): :white_check_mark: add/update tests for the profiles loader --- tests/unit/requirements/test_profiles.py | 76 +++++++++++++++++++----- 1 file changed, 60 insertions(+), 16 deletions(-) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index bdc5141e..9910a7e8 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -25,7 +25,7 @@ def test_order_of_loaded_profiles(profiles_path: str): assert len(profiles) > 0 # Extract the profile names - profile_names = sorted([profile for profile in profiles]) + profile_names = sorted([profile.name for profile in profiles]) logger.debug("The profile names: %r", profile_names) # The order of the profiles should be the same as the order of the directories @@ -81,14 +81,49 @@ def test_load_valid_profile_without_inheritance_from_validation_context(fake_pro assert len(profiles) == 1, "The number of profiles should be 1" +def test_profile_spec_properties(fake_profiles_path: str): + """Test the loaded profiles from the validator context.""" + settings = { + "profiles_path": fake_profiles_path, + "profile_name": "c", + "data_path": "/tmp/random_path", + "inherit_profiles": True, + "disable_check_for_duplicates": True, + } + + settings = ValidationSettings(**settings) + assert settings.inherit_profiles, "The inheritance mode should be set to True" + + validator = Validator(settings) + # initialize the validation context + context = ValidationContext(validator, validator.validation_settings.to_dict()) + + # Load the profiles + profiles = context.profiles + logger.debug("The profiles: %r", profiles) + + # The number of profiles should be 1 + assert len(profiles) == 2, "The number of profiles should be 2" + + # Get the profile + profile = context.get_profile_by_token("c") + assert profile.name == "c", "The profile name should be c" + assert profile.comment == "Comment for the Profile C.", "The profile comment should be 'Comment for the Profile C.'" + assert profile.version == "1.0.0", "The profile version should be 1.0.0" + assert profile.is_profile_of == ["https://w3id.org/a"], "The profileOf property should be ['a']" + assert profile.is_transitive_profile_of == [ + "https://w3id.org/a"], "The transitiveProfileOf property should be ['a']" + + def test_loaded_valid_profile_with_inheritance_from_validator_context(fake_profiles_path: str): """Test the loaded profiles from the validator context.""" - def __perform_test__(profile_name: str, expected_profiles: int): + def __perform_test__(profile_name: str, expected_inherited_profiles: list[str]): settings = { "profiles_path": fake_profiles_path, "profile_name": profile_name, "data_path": "/tmp/random_path", + "disable_check_for_duplicates": True, } validator = Validator(settings) @@ -101,15 +136,24 @@ def __perform_test__(profile_name: str, expected_profiles: int): profiles = context.profiles logger.debug("The profiles: %r", profiles) + # get and check the profile + profile = context.get_profile_by_token(profile_name) + assert profile.name == profile_name, f"The profile name should be {profile_name}" + # The number of profiles should be 1 - assert len(profiles) == expected_profiles, f"The number of profiles should be {expected_profiles}" + profiles_names = [_.name for _ in profile.inherited_profiles] + assert profiles_names == expected_inherited_profiles, f"The number of profiles should be {expected_inherited_profiles}" # Test the inheritance mode with 1 profile - __perform_test__("a", 1) + __perform_test__("a", []) + # Test the inheritance mode with 2 profiles + __perform_test__("b", ["a"]) # Test the inheritance mode with 2 profiles - __perform_test__("b", 2) - # Test the inheritance mode with 3 profiles - __perform_test__("c", 3) + __perform_test__("c", ["a"]) + # Test the inheritance mode with 4 profiles: using the profileOf property + __perform_test__("d1", ["a", "b", "c"]) + # Test the inheritance mode with 4 profiles: using the transitiveProfileOf property + __perform_test__("d2", ["a", "b", "c"]) def test_load_invalid_profile_no_override_enabled(fake_profiles_path: str): @@ -119,12 +163,12 @@ def test_load_invalid_profile_no_override_enabled(fake_profiles_path: str): "profile_name": "invalid-duplicated-shapes", "data_path": "/tmp/random_path", "inherit_profiles": True, - "override_profiles": False + "allow_shapes_override": False, } settings = ValidationSettings(**settings) assert settings.inherit_profiles, "The inheritance mode should be set to True" - assert not settings.override_profiles, "The override mode should be set to False" + assert not settings.allow_shapes_override, "The override mode should be set to False" validator = Validator(settings) # initialize the validation context @@ -143,12 +187,12 @@ def test_load_invalid_profile_with_override_on_same_profile(fake_profiles_path: "profile_name": "invalid-duplicated-shapes", "data_path": "/tmp/random_path", "inherit_profiles": True, - "override_profiles": True + "allow_shapes_override": False } settings = ValidationSettings(**settings) assert settings.inherit_profiles, "The inheritance mode should be set to True" - assert settings.override_profiles, "The override mode should be set to `True`" + assert not settings.allow_shapes_override, "The override mode should be set to `True`" validator = Validator(settings) # initialize the validation context context = ValidationContext(validator, validator.validation_settings.to_dict()) @@ -166,12 +210,12 @@ def test_load_valid_profile_with_override_on_inherited_profile(fake_profiles_pat "profile_name": "c-overridden", "data_path": "/tmp/random_path", "inherit_profiles": True, - "override_profiles": True + "allow_shapes_override": True } settings = ValidationSettings(**settings) assert settings.inherit_profiles, "The inheritance mode should be set to True" - assert settings.override_profiles, "The override mode should be set to `True`" + assert settings.allow_shapes_override, "The override mode should be set to `True`" validator = Validator(settings) # initialize the validation context context = ValidationContext(validator, validator.validation_settings.to_dict()) @@ -181,8 +225,8 @@ def test_load_valid_profile_with_override_on_inherited_profile(fake_profiles_pat logger.debug("The profiles: %r", profiles) # The number of profiles should be 2 - assert len(profiles) == 4, "The number of profiles should be 2" + assert len(profiles) == 3, "The number of profiles should be 3" # the number of checks should be 2 - requirements_checks = [requirement for profile in profiles.values() for requirement in profile.requirements] - assert len(requirements_checks) == 4, "The number of requirements should be 2" + requirements_checks = [requirement for profile in profiles for requirement in profile.requirements] + assert len(requirements_checks) == 3, "The number of requirements should be 2" From e9f99af54e4ad3b9cadf6adfd03053e812829bf1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 19:23:29 +0200 Subject: [PATCH 559/902] feat(cli): :sparkles: update the `profiles list` output --- rocrate_validator/cli/commands/profiles.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 6f28e7e4..404ea4fe 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -33,15 +33,19 @@ def profiles(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH): """ [magenta]rocrate-validator:[/magenta] Manage profiles """ + logger.debug("Profiles path: %s", profiles_path) + ctx.obj['profiles_path'] = profiles_path @profiles.command("list") @click.pass_context -def list_profiles(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH): +def list_profiles(ctx): # , profiles_path: Path = DEFAULT_PROFILES_PATH): """ List available profiles """ + profiles_path = ctx.obj['profiles_path'] console = ctx.obj['console'] + # Get the profiles profiles = services.get_profiles(profiles_path=profiles_path) # console.print("\nAvailable profiles:", style="white bold") console.print("\n", style="white bold") @@ -54,12 +58,15 @@ def list_profiles(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH): caption="[cyan](*)[/cyan] Number of requirements by severity") # Define columns - table.add_column("Name", style="magenta bold", justify="right") + table.add_column("ID", style="magenta bold", justify="center") + table.add_column("URI", style="yellow bold", justify="center") + table.add_column("Name", style="white bold", justify="center") table.add_column("Description", style="white italic") + table.add_column("based on", style="white", justify="center") table.add_column("Requirements (*)", style="white", justify="center") # Add data to the table - for profile_name, profile in profiles.items(): + for profile in profiles: # Count requirements by severity requirements = {} logger.debug("Requirements: %s", requirements) @@ -73,7 +80,10 @@ def list_profiles(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH): for severity, count in requirements.items() if count > 0]) # Add the row to the table - table.add_row(profile_name, Markdown(profile.description.strip()), requirements) + table.add_row(profile.token, profile.uri, + profile.label, Markdown(profile.description.strip()), + ", ".join([p.token for p in profile.inherited_profiles]), + requirements) table.add_row() # Print the table From 8339c8b764ffb94dd94c5ae89f0a20153feff456 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 19:26:35 +0200 Subject: [PATCH 560/902] perf(profiles/ro-crate): :zap: simplify base ontology on the RO-Crate profile --- .../profiles/ro-crate/ontology.ttl | 129 +----------------- 1 file changed, 1 insertion(+), 128 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/ontology.ttl b/rocrate_validator/profiles/ro-crate/ontology.ttl index 5e5f6112..8dd30ab6 100644 --- a/rocrate_validator/profiles/ro-crate/ontology.ttl +++ b/rocrate_validator/profiles/ro-crate/ontology.ttl @@ -12,32 +12,6 @@ rdf:type owl:Ontology ; owl:versionIRI . -# # ################################################################ -# # # Object Properties -# # ################################################################ - -# # ### http://schema.org/about -# # schema:about rdf:type owl:ObjectProperty , -# # owl:FunctionalProperty ; -# # rdfs:domain rocrate:ROCrateMetadataFileDescriptor ; -# # rdfs:range rocrate:RootDataEntity ; -# # rdfs:label "about"@en . - - -# # ### http://schema.org/hasPart -# # schema:hasPart rdf:type owl:ObjectProperty ; -# # rdfs:domain rocrate:RootDataEntity ; -# # rdfs:range rocrate:DataEntity ; -# # rdfs:label "hasPart"@en . - - -# # ### http://schema.org/mainEntity -# # schema:mainEntity rdf:type owl:ObjectProperty , -# # owl:FunctionalProperty ; -# # rdfs:domain rocrate:RootDataEntity ; -# # rdfs:range rocrate:MainWorkflow . - - # # ################################################################# # # # Classes # # ################################################################# @@ -68,112 +42,11 @@ bioschemas:ComputationalWorkflow rdf:type owl:Class . ### https://w3id.org/ro/crate/1.1/DataEntity rocrate:DataEntity rdf:type owl:Class ; - owl:equivalentClass [ rdf:type owl:Class ; - owl:unionOf ( rocrate:Directory - rocrate:File - ) - ] ; rdfs:subClassOf schema:CreativeWork ; rdfs:label "DataEntity"@en . # # ### https://w3id.org/ro/crate/1.1/Directory rocrate:Directory rdf:type owl:Class ; - owl:equivalentClass [ - rdf:type owl:Class ; - owl:intersectionOf ( - schema:Dataset - [ - rdf:type owl:Class ; - owl:complementOf rocrate:RootDataEntity - ] - ) ; - ] ; + rdfs:subClassOf schema:Dataset ; rdfs:label "Directory"@en . - - -# ### https://w3id.org/ro/crate/1.1/File -rocrate:File rdf:type owl:Class ; - owl:equivalentClass schema:MediaObject ; - owl:disjointWith rocrate:Directory , - rocrate:RootDataEntity ; - rdfs:label "File"@en . - - -# # ### https://w3id.org/ro/crate/1.1/MainWorkflow -# # rocrate:MainWorkflow rdf:type owl:Class ; -# # rdfs:subClassOf rocrate:Workflow . - - -# # ### https://w3id.org/ro/crate/1.1/ROCrateMetadataFileDescriptor -# rocrate:ROCrateMetadataFileDescriptor rdf:type owl:Class ; -# owl:equivalentClass [ rdf:type owl:Class ; -# owl:oneOf ( ro:ro-crate-metadata.json -# ) -# ] ; -# rdfs:label "ROCrateMetadataFileDescriptor"@en . -# # [ rdf:type owl:Restriction ; -# # owl:onProperty schema:about ; -# # owl:someValuesFrom owl:Thing -# # ] ; -# # rdfs:label "ROCrateMetadataFileDescriptor"@en . - - -# # ### https://w3id.org/ro/crate/1.1/RootDataEntity -# rocrate:RootDataEntity rdf:type owl:Class ; -# owl:equivalentClass [ rdf:type owl:Class ; -# owl:oneOf ( ro: -# ) -# ] . -# # [ rdf:type owl:Restriction ; -# # owl:onProperty schema:mainEntity ; -# # owl:someValuesFrom owl:Thing -# # ] ; -# # rdfs:subClassOf [ rdf:type owl:Restriction ; -# # owl:onProperty schema:hasPart ; -# # owl:minCardinality "1"^^xsd:nonNegativeInteger -# # ] ; -# # rdfs:label "RootDataEntity"@en . - - -# # ### https://w3id.org/ro/crate/1.1/Workflow -# # rocrate:Workflow rdf:type owl:Class ; -# # rdfs:subClassOf schema:SoftwareSourceCode , -# # bioschemas:ComputationalWorkflow , -# # rocrate:File . - - -# # ################################################################# -# # # Individuals -# # ################################################################# - -# # ### ./ -# ro: rdf:type owl:NamedIndividual , -# schema:Dataset , -# rocrate:RootDataEntity . - - -# ### ./ro-crate-metadata.json -# ro:ro-crate-metadata.json rdf:type owl:NamedIndividual , -# rocrate:ROCrateMetadataFileDescriptor . - - -# # ################################################################# -# # # General axioms -# # ################################################################# - -[ rdf:type owl:Class ; - owl:unionOf ( schema:Dataset - schema:MediaObject - rocrate:ROCrateMetadataFileDescriptor - ) ; - rdfs:subClassOf schema:CreativeWork -] . - - -[ rdf:type owl:AllDisjointClasses ; - owl:members ( schema:Dataset - schema:MediaObject - rocrate:ROCrateMetadataFileDescriptor - ) -] . From 132ada76547fb32d99134892130d7a0221f4b89c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 19:31:22 +0200 Subject: [PATCH 561/902] feat(profiles): :sparkles: add profile spec for the RO-Crate and Workflow RO-Crate profiles --- .../profiles/ro-crate/profile.ttl | 60 +++++++++++++++++ .../profiles/workflow-ro-crate/profile.ttl | 67 +++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 rocrate_validator/profiles/ro-crate/profile.ttl create mode 100644 rocrate_validator/profiles/workflow-ro-crate/profile.ttl diff --git a/rocrate_validator/profiles/ro-crate/profile.ttl b/rocrate_validator/profiles/ro-crate/profile.ttl new file mode 100644 index 00000000..eda8a696 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/profile.ttl @@ -0,0 +1,60 @@ +@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.1" ; + + # 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 refered 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 refered 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/workflow-ro-crate/profile.ttl b/rocrate_validator/profiles/workflow-ro-crate/profile.ttl new file mode 100644 index 00000000..39a22e92 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/profile.ttl @@ -0,0 +1,67 @@ +@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 """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.1 profile + prof:isProfileOf ; + + # Explicitly state that this profile is a transitive profile of the RO-Crate Metadata Specification 1.1 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 refered 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 refered 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.1 + prof:isInheritedFrom ; + ] ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "workflow-ro-crate" ; +. From a06f74a84440cc2fed011289c2215dc80b5285aa Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 19:37:37 +0200 Subject: [PATCH 562/902] test(test-conf): :card_file_box: add fake profiles for testing --- tests/data/profiles/fake/a/profile.ttl | 28 +++++++++++++++++ tests/data/profiles/fake/b/profile.ttl | 27 ++++++++++++++++ .../profiles/fake/c-overridden/profile.ttl | 14 +++++++++ tests/data/profiles/fake/c/profile.ttl | 31 +++++++++++++++++++ tests/data/profiles/fake/d1/profile.ttl | 27 ++++++++++++++++ tests/data/profiles/fake/d2/profile.ttl | 27 ++++++++++++++++ .../invalid-duplicated-shapes/profile.ttl | 28 +++++++++++++++++ 7 files changed, 182 insertions(+) create mode 100644 tests/data/profiles/fake/a/profile.ttl create mode 100644 tests/data/profiles/fake/b/profile.ttl create mode 100644 tests/data/profiles/fake/c-overridden/profile.ttl create mode 100644 tests/data/profiles/fake/c/profile.ttl create mode 100644 tests/data/profiles/fake/d1/profile.ttl create mode 100644 tests/data/profiles/fake/d2/profile.ttl create mode 100644 tests/data/profiles/fake/invalid-duplicated-shapes/profile.ttl diff --git a/tests/data/profiles/fake/a/profile.ttl b/tests/data/profiles/fake/a/profile.ttl new file mode 100644 index 00000000..8f7739db --- /dev/null +++ b/tests/data/profiles/fake/a/profile.ttl @@ -0,0 +1,28 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Profile A" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Comment for the Profile A."""@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.1 profile + # prof:isProfileOf ; + + # # Explicitly state that this profile is a transitive profile of the RO-Crate Metadata Specification 1.1 profile + # prof:isTransitiveProfileOf , ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "a" ; +. diff --git a/tests/data/profiles/fake/b/profile.ttl b/tests/data/profiles/fake/b/profile.ttl new file mode 100644 index 00000000..707e55e7 --- /dev/null +++ b/tests/data/profiles/fake/b/profile.ttl @@ -0,0 +1,27 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Profile B" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Comment for the Profile B."""@en ; + + # URI of the publisher of the profile B + dct:publisher ; + + # This profile is an extension of the profile A + prof:isProfileOf ; + + # Explicitly state that this profile is a transitive profile of the profile A + prof:isTransitiveProfileOf ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "b" ; +. diff --git a/tests/data/profiles/fake/c-overridden/profile.ttl b/tests/data/profiles/fake/c-overridden/profile.ttl new file mode 100644 index 00000000..26992dbc --- /dev/null +++ b/tests/data/profiles/fake/c-overridden/profile.ttl @@ -0,0 +1,14 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + a prof:Profile ; + rdfs:label "Profile C2" ; + rdfs:comment """Comment for Profile C2."""@en ; + dct:publisher ; + prof:isProfileOf ; + prof:isTransitiveProfileOf , ; + prof:hasToken "c-overridden" ; +. diff --git a/tests/data/profiles/fake/c/profile.ttl b/tests/data/profiles/fake/c/profile.ttl new file mode 100644 index 00000000..c622c862 --- /dev/null +++ b/tests/data/profiles/fake/c/profile.ttl @@ -0,0 +1,31 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . +@prefix schema: . + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Profile C" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Comment for the Profile C."""@en ; + + # the version of the profile + schema:version "1.0.0" ; + + # URI of the publisher of the profile C + dct:publisher ; + + # This profile is an extension of the profile A + prof:isProfileOf ; + + # Explicitly state that this profile is a transitive profile of the profile A + prof:isTransitiveProfileOf ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "c" ; +. diff --git a/tests/data/profiles/fake/d1/profile.ttl b/tests/data/profiles/fake/d1/profile.ttl new file mode 100644 index 00000000..5b6eec19 --- /dev/null +++ b/tests/data/profiles/fake/d1/profile.ttl @@ -0,0 +1,27 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Profile D1" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Comment for the Profile D1."""@en ; + + # URI of the publisher of the profile D1 + dct:publisher ; + + # This profile is an extension of the profile A + prof:isProfileOf , ; + + # # Explicitly state that this profile is a transitive profile of the profile A + # prof:isTransitiveProfileOf , ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "d1" ; +. diff --git a/tests/data/profiles/fake/d2/profile.ttl b/tests/data/profiles/fake/d2/profile.ttl new file mode 100644 index 00000000..6fee05b6 --- /dev/null +++ b/tests/data/profiles/fake/d2/profile.ttl @@ -0,0 +1,27 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Profile D2" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Comment for the Profile D2."""@en ; + + # URI of the publisher of the profile D2 + dct:publisher ; + + # This profile is an extension of the profile A + prof:isProfileOf , ; + + # Explicitly state that this profile is a transitive profile of the profile A + prof:isTransitiveProfileOf , , ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "d2" ; +. diff --git a/tests/data/profiles/fake/invalid-duplicated-shapes/profile.ttl b/tests/data/profiles/fake/invalid-duplicated-shapes/profile.ttl new file mode 100644 index 00000000..de2ec683 --- /dev/null +++ b/tests/data/profiles/fake/invalid-duplicated-shapes/profile.ttl @@ -0,0 +1,28 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "A fake profile with duplicated shapes" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Comment for the fake profile."""@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.1 profile + # prof:isProfileOf ; + + # # Explicitly state that this profile is a transitive profile of the RO-Crate Metadata Specification 1.1 profile + # prof:isTransitiveProfileOf , ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "invalid-duplicated-shapes" ; +. From fff03868a6883af6bdd16b0b5902ad19a04e636c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 26 Jun 2024 19:40:41 +0200 Subject: [PATCH 563/902] refactor(profiles/workflow-ro-crate): :bug: safe get of entity @id --- .../profiles/workflow-ro-crate/may/1_main_workflow.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 706fe209..8f687746 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 @@ -56,7 +56,8 @@ def check_workflow_diagram(self, validation_context: ValidationContext) -> bool: json_dict = self.get_json_dict(validation_context) entity_dict = {_["@id"]: _ for _ in json_dict["@graph"]} main_workflow = find_main_workflow(entity_dict) - diagram_relpath = main_workflow.get("image")["@id"] + image = main_workflow.get("image") + diagram_relpath = image["@id"] if image else None if not diagram_relpath: validation_context.result.add_error(f"main workflow does not have an 'image' property", self) return False @@ -72,7 +73,8 @@ def check_workflow_description(self, validation_context: ValidationContext) -> b json_dict = self.get_json_dict(validation_context) entity_dict = {_["@id"]: _ for _ in json_dict["@graph"]} main_workflow = find_main_workflow(entity_dict) - description_relpath = main_workflow.get("subjectOf")["@id"] + main_workflow = main_workflow.get("subjectOf") + description_relpath = main_workflow["@id"] if main_workflow else None if not description_relpath: validation_context.result.add_error("main workflow does not have a 'subjectOf' property", self) return False From 2d819b06b443da3c3acd77b77deb7db378b1afa8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 09:24:23 +0200 Subject: [PATCH 564/902] fix(profiles/workflow-ro-crate): :bug: fix profile label --- rocrate_validator/profiles/workflow-ro-crate/profile.ttl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/profiles/workflow-ro-crate/profile.ttl b/rocrate_validator/profiles/workflow-ro-crate/profile.ttl index 39a22e92..99fec7cf 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/profile.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/profile.ttl @@ -9,7 +9,7 @@ a prof:Profile ; # the Profile's label - rdfs:label "Workflow RO-Crate Metadata Specification 1.1" ; + rdfs:label "Workflow RO-Crate Metadata Specification 1.0" ; # regular metadata, a basic description of the Profile rdfs:comment """RO-Crate Metadata Specification."""@en ; From 3a2d169501f32be42b20765e86c64983873ef024 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 27 Jun 2024 14:36:03 +0200 Subject: [PATCH 565/902] Add pytest-xdist to test depedencies --- poetry.lock | 319 +++++++++++++++++++++++++++---------------------- pyproject.toml | 1 + 2 files changed, 178 insertions(+), 142 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7ce5ca3c..c4b54665 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "appnope" @@ -289,63 +289,63 @@ test = ["pytest"] [[package]] name = "coverage" -version = "7.5.3" +version = "7.5.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"}, - {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"}, - {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"}, - {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"}, - {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"}, - {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"}, - {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"}, - {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"}, - {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"}, - {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"}, - {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"}, - {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"}, - {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"}, - {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"}, - {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"}, - {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"}, - {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"}, - {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"}, - {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"}, - {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"}, - {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"}, - {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"}, + {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"}, + {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"}, + {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"}, + {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"}, + {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"}, + {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"}, + {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"}, + {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"}, + {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"}, + {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"}, + {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"}, + {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"}, + {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"}, + {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"}, + {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"}, + {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"}, + {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"}, + {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"}, + {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"}, + {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, ] [package.dependencies] @@ -356,33 +356,33 @@ toml = ["tomli"] [[package]] name = "debugpy" -version = "1.8.1" +version = "1.8.2" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" files = [ - {file = "debugpy-1.8.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:3bda0f1e943d386cc7a0e71bfa59f4137909e2ed947fb3946c506e113000f741"}, - {file = "debugpy-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda73bf69ea479c8577a0448f8c707691152e6c4de7f0c4dec5a4bc11dee516e"}, - {file = "debugpy-1.8.1-cp310-cp310-win32.whl", hash = "sha256:3a79c6f62adef994b2dbe9fc2cc9cc3864a23575b6e387339ab739873bea53d0"}, - {file = "debugpy-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:7eb7bd2b56ea3bedb009616d9e2f64aab8fc7000d481faec3cd26c98a964bcdd"}, - {file = "debugpy-1.8.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:016a9fcfc2c6b57f939673c874310d8581d51a0fe0858e7fac4e240c5eb743cb"}, - {file = "debugpy-1.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd97ed11a4c7f6d042d320ce03d83b20c3fb40da892f994bc041bbc415d7a099"}, - {file = "debugpy-1.8.1-cp311-cp311-win32.whl", hash = "sha256:0de56aba8249c28a300bdb0672a9b94785074eb82eb672db66c8144fff673146"}, - {file = "debugpy-1.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a9fe0829c2b854757b4fd0a338d93bc17249a3bf69ecf765c61d4c522bb92a8"}, - {file = "debugpy-1.8.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3ebb70ba1a6524d19fa7bb122f44b74170c447d5746a503e36adc244a20ac539"}, - {file = "debugpy-1.8.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2e658a9630f27534e63922ebf655a6ab60c370f4d2fc5c02a5b19baf4410ace"}, - {file = "debugpy-1.8.1-cp312-cp312-win32.whl", hash = "sha256:caad2846e21188797a1f17fc09c31b84c7c3c23baf2516fed5b40b378515bbf0"}, - {file = "debugpy-1.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:edcc9f58ec0fd121a25bc950d4578df47428d72e1a0d66c07403b04eb93bcf98"}, - {file = "debugpy-1.8.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:7a3afa222f6fd3d9dfecd52729bc2e12c93e22a7491405a0ecbf9e1d32d45b39"}, - {file = "debugpy-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d915a18f0597ef685e88bb35e5d7ab968964b7befefe1aaea1eb5b2640b586c7"}, - {file = "debugpy-1.8.1-cp38-cp38-win32.whl", hash = "sha256:92116039b5500633cc8d44ecc187abe2dfa9b90f7a82bbf81d079fcdd506bae9"}, - {file = "debugpy-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:e38beb7992b5afd9d5244e96ad5fa9135e94993b0c551ceebf3fe1a5d9beb234"}, - {file = "debugpy-1.8.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:bfb20cb57486c8e4793d41996652e5a6a885b4d9175dd369045dad59eaacea42"}, - {file = "debugpy-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd3fdd3f67a7e576dd869c184c5dd71d9aaa36ded271939da352880c012e703"}, - {file = "debugpy-1.8.1-cp39-cp39-win32.whl", hash = "sha256:58911e8521ca0c785ac7a0539f1e77e0ce2df753f786188f382229278b4cdf23"}, - {file = "debugpy-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:6df9aa9599eb05ca179fb0b810282255202a66835c6efb1d112d21ecb830ddd3"}, - {file = "debugpy-1.8.1-py2.py3-none-any.whl", hash = "sha256:28acbe2241222b87e255260c76741e1fbf04fdc3b6d094fcf57b6c6f75ce1242"}, - {file = "debugpy-1.8.1.zip", hash = "sha256:f696d6be15be87aef621917585f9bb94b1dc9e8aced570db1b8a6fc14e8f9b42"}, + {file = "debugpy-1.8.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:7ee2e1afbf44b138c005e4380097d92532e1001580853a7cb40ed84e0ef1c3d2"}, + {file = "debugpy-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f8c3f7c53130a070f0fc845a0f2cee8ed88d220d6b04595897b66605df1edd6"}, + {file = "debugpy-1.8.2-cp310-cp310-win32.whl", hash = "sha256:f179af1e1bd4c88b0b9f0fa153569b24f6b6f3de33f94703336363ae62f4bf47"}, + {file = "debugpy-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:0600faef1d0b8d0e85c816b8bb0cb90ed94fc611f308d5fde28cb8b3d2ff0fe3"}, + {file = "debugpy-1.8.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8a13417ccd5978a642e91fb79b871baded925d4fadd4dfafec1928196292aa0a"}, + {file = "debugpy-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acdf39855f65c48ac9667b2801234fc64d46778021efac2de7e50907ab90c634"}, + {file = "debugpy-1.8.2-cp311-cp311-win32.whl", hash = "sha256:2cbd4d9a2fc5e7f583ff9bf11f3b7d78dfda8401e8bb6856ad1ed190be4281ad"}, + {file = "debugpy-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:d3408fddd76414034c02880e891ea434e9a9cf3a69842098ef92f6e809d09afa"}, + {file = "debugpy-1.8.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:5d3ccd39e4021f2eb86b8d748a96c766058b39443c1f18b2dc52c10ac2757835"}, + {file = "debugpy-1.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62658aefe289598680193ff655ff3940e2a601765259b123dc7f89c0239b8cd3"}, + {file = "debugpy-1.8.2-cp312-cp312-win32.whl", hash = "sha256:bd11fe35d6fd3431f1546d94121322c0ac572e1bfb1f6be0e9b8655fb4ea941e"}, + {file = "debugpy-1.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:15bc2f4b0f5e99bf86c162c91a74c0631dbd9cef3c6a1d1329c946586255e859"}, + {file = "debugpy-1.8.2-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:5a019d4574afedc6ead1daa22736c530712465c0c4cd44f820d803d937531b2d"}, + {file = "debugpy-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40f062d6877d2e45b112c0bbade9a17aac507445fd638922b1a5434df34aed02"}, + {file = "debugpy-1.8.2-cp38-cp38-win32.whl", hash = "sha256:c78ba1680f1015c0ca7115671fe347b28b446081dada3fedf54138f44e4ba031"}, + {file = "debugpy-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:cf327316ae0c0e7dd81eb92d24ba8b5e88bb4d1b585b5c0d32929274a66a5210"}, + {file = "debugpy-1.8.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1523bc551e28e15147815d1397afc150ac99dbd3a8e64641d53425dba57b0ff9"}, + {file = "debugpy-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e24ccb0cd6f8bfaec68d577cb49e9c680621c336f347479b3fce060ba7c09ec1"}, + {file = "debugpy-1.8.2-cp39-cp39-win32.whl", hash = "sha256:7f8d57a98c5a486c5c7824bc0b9f2f11189d08d73635c326abef268f83950326"}, + {file = "debugpy-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:16c8dcab02617b75697a0a925a62943e26a0330da076e2a10437edd9f0bf3755"}, + {file = "debugpy-1.8.2-py2.py3-none-any.whl", hash = "sha256:16e16df3a98a35c63c3ab1e4d19be4cbc7fdda92d9ddc059294f18910928e0ca"}, + {file = "debugpy-1.8.2.zip", hash = "sha256:95378ed08ed2089221896b9b3a8d021e642c24edc8fef20e5d4342ca8be65c00"}, ] [[package]] @@ -425,6 +425,20 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "execnet" +version = "2.1.1" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +files = [ + {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, + {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + [[package]] name = "executing" version = "2.0.1" @@ -489,22 +503,22 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.1.0" +version = "8.0.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, - {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, + {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, + {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "iniconfig" @@ -766,13 +780,13 @@ rdflib = ">=6.0.2" [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] @@ -865,13 +879,13 @@ tests = ["pytest", "pytest-cov", "pytest-lazy-fixtures"] [[package]] name = "prompt-toolkit" -version = "3.0.45" +version = "3.0.47" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.45-py3-none-any.whl", hash = "sha256:a29b89160e494e3ea8622b09fa5897610b437884dcdcd054fdc1308883326c2a"}, - {file = "prompt_toolkit-3.0.45.tar.gz", hash = "sha256:07c60ee4ab7b7e90824b61afa840c8f5aad2d46b3e2e10acc33d8ecc94a49089"}, + {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, + {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, ] [package.dependencies] @@ -879,27 +893,28 @@ wcwidth = "*" [[package]] name = "psutil" -version = "5.9.8" +version = "6.0.0" description = "Cross-platform lib for process and system monitoring in Python." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -files = [ - {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, - {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, - {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, - {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, - {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, - {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, - {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, - {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, - {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, - {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, - {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, - {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, - {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, - {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, - {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, - {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, + {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, + {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, + {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, + {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, + {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, + {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, + {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, + {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, + {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, + {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, + {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, + {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, + {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, ] [package.extras] @@ -979,13 +994,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "3.2.2" +version = "3.2.4" description = "python code static checker" optional = false python-versions = ">=3.8.0" files = [ - {file = "pylint-3.2.2-py3-none-any.whl", hash = "sha256:3f8788ab20bb8383e06dd2233e50f8e08949cfd9574804564803441a4946eab4"}, - {file = "pylint-3.2.2.tar.gz", hash = "sha256:d068ca1dfd735fb92a07d33cb8f288adc0f6bc1287a139ca2425366f7cbe38f8"}, + {file = "pylint-3.2.4-py3-none-any.whl", hash = "sha256:43b8ffdf1578e4e4439fa1f6ace402281f5dd61999192280fa12fe411bef2999"}, + {file = "pylint-3.2.4.tar.gz", hash = "sha256:5753d27e49a658b12a48c2883452751a2ecfc7f38594e0980beb03a6e77e6f86"}, ] [package.dependencies] @@ -993,8 +1008,8 @@ astroid = ">=3.2.2,<=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.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, ] isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" mccabe = ">=0.6,<0.8" @@ -1067,13 +1082,13 @@ js = ["pyduktape2 (>=0.4.6,<0.5.0)"] [[package]] name = "pytest" -version = "8.2.1" +version = "8.2.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, - {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, ] [package.dependencies] @@ -1105,6 +1120,26 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] +[[package]] +name = "pytest-xdist" +version = "3.6.1" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, + {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, +] + +[package.dependencies] +execnet = ">=2.1" +pytest = ">=7.0.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1305,13 +1340,13 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rich-click" -version = "1.8.2" +version = "1.8.3" description = "Format click help output nicely with rich" optional = false python-versions = ">=3.7" files = [ - {file = "rich_click-1.8.2-py3-none-any.whl", hash = "sha256:b57856f304e4fe0394b82d7ce0784450758f8c8b4e201ccc4320501cc201806b"}, - {file = "rich_click-1.8.2.tar.gz", hash = "sha256:8e29bdede858b59aa2859a1ab1c4ccbd39ed7ed5870262dae756fba6b5dc72e8"}, + {file = "rich_click-1.8.3-py3-none-any.whl", hash = "sha256:636d9c040d31c5eee242201b5bf4f2d358bfae4db14bb22ec1cafa717cfd02cd"}, + {file = "rich_click-1.8.3.tar.gz", hash = "sha256:6d75bdfa7aa9ed2c467789a0688bc6da23fbe3a143e19aa6ad3f8bac113d2ab3"}, ] [package.dependencies] @@ -1388,22 +1423,22 @@ files = [ [[package]] name = "tornado" -version = "6.4" +version = "6.4.1" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = false -python-versions = ">= 3.8" +python-versions = ">=3.8" files = [ - {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"}, - {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"}, - {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"}, - {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"}, - {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"}, - {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"}, - {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"}, - {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"}, - {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"}, - {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"}, - {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, + {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, + {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, + {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, ] [[package]] @@ -1423,24 +1458,24 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0, [[package]] name = "typing-extensions" -version = "4.12.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, - {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] @@ -1473,20 +1508,20 @@ files = [ [[package]] name = "zipp" -version = "3.19.1" +version = "3.19.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, - {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, + {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, + {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, ] [package.extras] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "440400ea408f3383bedded6191b9298acf6192ed4738b1642cfa06adacba25cb" +content-hash = "1b88cdfd9d1197574df9aa07fece4cab40064dedce4d1830be6c0d0cb96d5506" diff --git a/pyproject.toml b/pyproject.toml index 28f58dc0..644d6099 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ ipykernel = "^6.29.3" [tool.poetry.group.test.dependencies] pytest-cov = "^5.0.0" +pytest-xdist = "^3.6.1" [tool.flake8] max-line-length = 120 From cc84712368fcb59d107a99ac42ef065a4b38f686 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 15:02:17 +0200 Subject: [PATCH 566/902] use pytest-xdist by default for running tests --- tests/pytest.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/pytest.ini b/tests/pytest.ini index a1080c1e..f023c908 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -2,4 +2,5 @@ # markers = sources # log_cli=true # log_level=INFO +addopts = -n auto ; filterwarnings = From a27ecb6d5674619f38ac7ae173de53fe596ecd13 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 16:51:54 +0200 Subject: [PATCH 567/902] refactor(core): :recycle: restructure ProfileSpecification errors --- rocrate_validator/errors.py | 25 +++++++------------------ rocrate_validator/models.py | 4 ++-- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 8c0b61b4..4ae735f5 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -63,51 +63,40 @@ def __repr__(self): class ProfileSpecificationNotFound(ROCValidatorError): """Raised when the profile specification is not found.""" - def __init__(self, profile_name: Optional[str] = None, spec_file: Optional[str] = None): - self._profile_name = profile_name + def __init__(self, spec_file: Optional[str] = None): self._spec_file = spec_file - @property - def profile_name(self) -> Optional[str]: - """The name of the profile.""" - return self._profile_name - @property def spec_file(self) -> Optional[str]: """The name of the profile specification file.""" return self._spec_file def __str__(self) -> str: - msg = f"Unable to find the `profile.ttl` specification for the profile \"{self._profile_name!r}\"" + msg = "Unable to find the `profile.ttl` specification" if self._spec_file: msg += f" in the file {self._spec_file!r}" return msg def __repr__(self): - return f"ProfileSpecificationNotFound({self._profile_name!r})" + return f"ProfileSpecificationNotFound()" class ProfileSpecificationError(ROCValidatorError): + """Raised when an error occurs in the profile specification.""" - def __init__(self, profile_name: Optional[str] = None, message: Optional[str] = None): - self._profile_name = profile_name + def __init__(self, message: Optional[str] = None): self._message = message - @property - def profile_name(self) -> Optional[str]: - """The name of the profile.""" - return self._profile_name - @property def message(self) -> Optional[str]: """The error message.""" return self._message def __str__(self) -> str: - return f"Error in the `profile.ttl` specification for the profile \"{self._profile_name!r}\": {self._message!r}" + return f"Error in the `profile.ttl` specification: {self._message!r}" def __repr__(self): - return f"ProfileSpecificationError({self._profile_name!r}, {self._message!r})" + return f"ProfileSpecificationError({self._message!r})" class DuplicateRequirementCheck(ROCValidatorError): diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index a239d6b9..a898b69b 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -149,7 +149,7 @@ def __init__(self, name: str, path: Path, # check if the profile specification file exists spec_file = self.profile_specification_file_path if not spec_file or not spec_file.exists(): - raise ProfileSpecificationNotFound(name, spec_file) + raise ProfileSpecificationNotFound(spec_file) # load the profile specification expressed using the Profiles Vocabulary profile = Graph() profile.parse(str(spec_file), format="turtle") @@ -162,7 +162,7 @@ def __init__(self, name: str, path: Path, self._profile_node.toPython(), self, token=self.token, name=self.name) # add the profile to the profiles map else: raise ProfileSpecificationError( - profile_name=name, message=f"Profile specification file {spec_file} must contain exactly one profile") + message=f"Profile specification file {spec_file} must contain exactly one profile") def __get_specification_property__(self, property: str, namespace: Namespace, pop_first: bool = True, as_Python_object: bool = True) -> Union[str, list[Union[str, URIRef]]]: From 0310c61020aece18e5387b2439529e7af5ee7334 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 16:57:42 +0200 Subject: [PATCH 568/902] refactor(core): :recycle: add profiles base path to the init of Profile class --- rocrate_validator/models.py | 9 +++++---- rocrate_validator/services.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index a898b69b..eba0a58b 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -129,12 +129,12 @@ class Profile: # store the map of profiles: profile URI -> Profile instance __profiles_map: MultiIndexMap = MultiIndexMap("uri", indexes=[MapIndex("name"), MapIndex("token", unique=True)]) - def __init__(self, name: str, path: Path, + profiles_base_path: Path, requirements: Optional[list[Requirement]] = None, publicID: Optional[str] = None, severity: Severity = Severity.REQUIRED): - self._path = path - self._name = name + self._profiles_base_path = profiles_base_path + self._profile_path = profile_path self._description: Optional[str] = None self._requirements: list[Requirement] = requirements if requirements is not None else [] self._publicID = publicID @@ -174,7 +174,7 @@ def __get_specification_property__(self, property: str, namespace: Namespace, @property def path(self): - return self._path + return self._profile_path @property def name(self): @@ -1121,6 +1121,7 @@ def __load_profiles__(self) -> list[Profile]: # if the inheritance is disabled, load only the target profile if not self.inheritance_enabled: profile = Profile.load( + self.profiles_path, self.profiles_path / self.profile_name, publicID=self.publicID, severity=self.requirement_severity) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 345ed7dc..8ca09bf8 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -48,7 +48,7 @@ def get_profile(profiles_path: Path = DEFAULT_PROFILES_PATH, profile_path = profiles_path / profile_name if not Path(profiles_path).exists(): raise FileNotFoundError(f"Profile not found: {profile_path}") - profile = Profile.load(f"{profiles_path}/{profile_name}", + profile = Profile.load(profiles_path, f"{profiles_path}/{profile_name}", publicID=publicID, severity=Severity.OPTIONAL) logger.debug("Profile loaded: %s", profile) return profile From a065835309d44fd1656e3feffb058813a88d97ef Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:01:20 +0200 Subject: [PATCH 569/902] feat(core): :sparkles: allow to extract profile version according to different criteria --- rocrate_validator/models.py | 58 +++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index eba0a58b..a58d1a9c 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -158,6 +158,8 @@ class Profile: if len(profiles) == 1: self._profile_node = profiles[0] self._profile_specification_graph = profile + # initialize the token and version + self._token, self._version = self.__init_token_version__() self.__profiles_map.add( self._profile_node.toPython(), self, token=self.token, name=self.name) # add the profile to the profiles map else: @@ -186,7 +188,7 @@ def profile_node(self): @property def token(self): - return self.__get_specification_property__("hasToken", PROF_NS) or self._name.lower().replace(" ", "_") + return self._token @property def uri(self): @@ -202,7 +204,7 @@ def comment(self): @property def version(self): - return self.__get_specification_property__("version", SCHEMA_ORG_NS) + return self._version @property def is_profile_of(self) -> list[str]: @@ -309,7 +311,57 @@ def __str__(self) -> str: return self.name @staticmethod - def load(path: Union[str, Path], + def __extract_version_from_token__(token: str) -> Optional[str]: + if not token: + return None + pattern = r"\Wv?(\d+(\.\d+(\.\d+)?)?)" + matches = re.findall(pattern, token) + if matches: + return matches[-1][0] + return None + + def __get_consistent_version__(self, candidate_token: str) -> str: + candidates = {_ for _ in [ + self.__get_specification_property__("version", SCHEMA_ORG_NS), + self.__extract_version_from_token__(candidate_token), + self.__extract_version_from_token__(str(self.path.relative_to(self._profiles_base_path))), + self.__extract_version_from_token__(str(self.uri)) + ] if _ is not None} + if len(candidates) > 1: + raise ProfileSpecificationError(f"Inconsistent versions found: {candidates}") + logger.debug("Candidate versions: %s", candidates) + return candidates.pop() if len(candidates) == 1 else None + + def __extract_token_from_path__(self) -> str: + base_path = str(self._profiles_base_path.absolute()) + identifier = str(self.path.absolute()) + # Check if the path starts with the base path + if not identifier.startswith(base_path): + raise ValueError("Path does not start with the base path") + # Remove the base path from the identifier + identifier = identifier.replace(f"{base_path}/", "") + # Replace slashes with hyphens + identifier = identifier.replace('/', '-') + return identifier + + def __init_token_version__(self) -> Tuple[str, str, str]: + # try to extract the token from the specs or the path + candidate_token = self.__get_specification_property__("hasToken", PROF_NS) + if not candidate_token: + candidate_token = self.__extract_token_from_path__() + logger.debug("Candidate token: %s", candidate_token) + + # try to extract the version from the specs or the token or the path or the URI + version = self.__get_consistent_version__(candidate_token) + logger.debug("Extracted version: %s", version) + + # remove the version from the token if it is present + if version: + candidate_token = re.sub(r"[\W|_]+" + re.escape(version) + r"$", "", candidate_token) + + # return the candidate token and version + return candidate_token, version + publicID: Optional[str] = None, severity: Severity = Severity.REQUIRED) -> Profile: # if the path is a string, convert it to a Path From 8f78e15963f480797bde41880d0747588446362b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:03:21 +0200 Subject: [PATCH 570/902] feat(core): :sparkles: add explicit `identifier` property to the Profile class --- rocrate_validator/models.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index a58d1a9c..6d6ad8ef 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -127,12 +127,17 @@ def get(name: str) -> RequirementLevel: class Profile: # store the map of profiles: profile URI -> Profile instance - __profiles_map: MultiIndexMap = MultiIndexMap("uri", indexes=[MapIndex("name"), MapIndex("token", unique=True)]) + __profiles_map: MultiIndexMap = MultiIndexMap("uri", + indexes=[MapIndex("name"), MapIndex("token", unique=False), MapIndex("identifier", unique=True)]) + def __init__(self, profiles_base_path: Path, + profile_path: Path, requirements: Optional[list[Requirement]] = None, + identifier: str = None, publicID: Optional[str] = None, severity: Severity = Severity.REQUIRED): + self._identifier: Optional[str] = identifier self._profiles_base_path = profiles_base_path self._profile_path = profile_path self._description: Optional[str] = None @@ -160,8 +165,9 @@ class Profile: self._profile_specification_graph = profile # initialize the token and version self._token, self._version = self.__init_token_version__() + # add the profile to the profiles map self.__profiles_map.add( - self._profile_node.toPython(), self, token=self.token, name=self.name) # add the profile to the profiles map + self._profile_node.toPython(), self, token=self.token, name=self.name, identifier=self.identifier) # add the profile to the profiles map else: raise ProfileSpecificationError( message=f"Profile specification file {spec_file} must contain exactly one profile") @@ -178,6 +184,13 @@ def __get_specification_property__(self, property: str, namespace: Namespace, def path(self): return self._profile_path + @property + def identifier(self) -> str: + if not self._identifier: + version = self.version + self._identifier = f"{self.token}-{version}" if version else self.token + return self._identifier + @property def name(self): return self._name @@ -288,27 +301,28 @@ def remove_requirement(self, requirement: Requirement): def __eq__(self, other: object) -> bool: return isinstance(other, Profile) \ - and self.name == other.name \ + and self.identifier == other.identifier \ and self.path == other.path \ and self.requirements == other.requirements def __lt__(self, other: object) -> bool: if not isinstance(other, Profile): raise TypeError(f"Cannot compare {type(self)} with {type(other)}") - return self.name < other.name + return self.identifier < other.identifier def __hash__(self) -> int: - return hash((self.name, self.path, self.requirements)) + return hash((self.identifier, self.path, self.requirements)) def __repr__(self) -> str: return ( - f'Profile(name={self.name}, ' + f'Profile(identifier={self.identifier}, ' + f'name={self.name}, ' f'path={self.path}, ' if self.path else '' f'requirements={self.requirements})' ) def __str__(self) -> str: - return self.name + return f"{self.name} ({self.identifier})" @staticmethod def __extract_version_from_token__(token: str) -> Optional[str]: From d26cfa4c123bc7c80f9eaa835d6eba2b18001084 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:05:11 +0200 Subject: [PATCH 571/902] feat(core): :sparkles: allow to get Profile instances by identitier --- rocrate_validator/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 6d6ad8ef..959b12d0 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -411,6 +411,10 @@ def load_profiles(profiles_path: Union[str, Path], # order profiles according to the dependencies between them: first the profiles that do not depend on ??? return profiles + @classmethod + def get_by_identifier(cls, identifier: str) -> Profile: + return cls.__profiles_map.get_by_index("identifier", identifier) + @classmethod def get_by_uri(cls, uri: str) -> Profile: return cls.__profiles_map.get_by_key(uri) From b768946a5acc8f028eaa59ea4eb743bc4678b833 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:06:34 +0200 Subject: [PATCH 572/902] refactor(core): :recycle: map Profile name to specs label --- rocrate_validator/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 959b12d0..63903f36 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -140,6 +140,7 @@ def __init__(self, self._identifier: Optional[str] = identifier self._profiles_base_path = profiles_base_path self._profile_path = profile_path + self._name: Optional[str] = None self._description: Optional[str] = None self._requirements: list[Requirement] = requirements if requirements is not None else [] self._publicID = publicID @@ -193,7 +194,7 @@ def identifier(self) -> str: @property def name(self): - return self._name + return self.label or f"Profile {self.uri}" @property def profile_node(self): From d9c25ec035cfdba520cf50b578970d7ce0879743 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:07:39 +0200 Subject: [PATCH 573/902] fix(utils): :bug: update specs property getter to `None` as return --- rocrate_validator/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 63903f36..952010a6 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -179,7 +179,9 @@ def __get_specification_property__(self, property: str, namespace: Namespace, values = list(self._profile_specification_graph.objects(self._profile_node, namespace[property])) if values and as_Python_object: values = [v.toPython() for v in values] - return values[0] if values and len(values) >= 1 and pop_first else values + if pop_first: + return values[0] if values and len(values) >= 1 else None + return values @property def path(self): From f6694b87163335aa001740314f3119aa853c8905 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:09:54 +0200 Subject: [PATCH 574/902] feat(core): :sparkles: enable more flexible folder structure for profiles --- rocrate_validator/models.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 952010a6..c98baa37 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -3,12 +3,13 @@ import bisect import enum import inspect +import re from abc import ABC, abstractmethod from collections.abc import Collection from dataclasses import asdict, dataclass from functools import total_ordering from pathlib import Path -from typing import Optional, Union +from typing import Optional, Tuple, Union from rdflib import RDF, RDFS, Graph, Namespace, URIRef @@ -379,16 +380,20 @@ def __init_token_version__(self) -> Tuple[str, str, str]: # return the candidate token and version return candidate_token, version + @classmethod + def load(cls, profiles_base_path: str, + profile_path: Union[str, Path], publicID: Optional[str] = None, severity: Severity = Severity.REQUIRED) -> Profile: # if the path is a string, convert it to a Path - if isinstance(path, str): - path = Path(path) + if isinstance(profile_path, str): + profile_path = Path(profile_path) # check if the path is a directory - if not path.is_dir(): - raise InvalidProfilePath(path) + if not profile_path.is_dir(): + raise InvalidProfilePath(profile_path) # create a new profile - profile = Profile(name=path.name, path=path, publicID=publicID, severity=severity) + profile = Profile(profiles_base_path=profiles_base_path, + profile_path=profile_path, publicID=publicID, severity=severity) logger.debug("Loaded profile: %s", profile) return profile @@ -404,12 +409,16 @@ def load_profiles(profiles_path: Union[str, Path], raise InvalidProfilePath(profiles_path) # initialize the profiles list profiles = [] + # calculate the list of profiles path as the subdirectories of the profiles path + # where the profile specification file is present + profile_paths = [p.parent for p in profiles_path.rglob('*.*') if p.name == PROFILE_SPECIFICATION_FILE] + # iterate through the directories and load the profiles - for profile_path in profiles_path.iterdir(): + for profile_path in profile_paths: logger.debug("Checking profile path: %s %s %r", profile_path, profile_path.is_dir(), IGNORED_PROFILE_DIRECTORIES) if profile_path.is_dir() and profile_path not in IGNORED_PROFILE_DIRECTORIES: - profile = Profile.load(profile_path, publicID=publicID, severity=severity) + profile = Profile.load(profiles_path, profile_path, publicID=publicID, severity=severity) profiles.append(profile) # order profiles according to the dependencies between them: first the profiles that do not depend on ??? return profiles From 9cd0c4b779ba3ce2c0bb4cd6e79438751094d748 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:12:04 +0200 Subject: [PATCH 575/902] fix(core): :bug: update logic to compute inherited profiles --- rocrate_validator/models.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index c98baa37..ba604cd1 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -282,11 +282,12 @@ def __get_nested_profiles__(cls, source: str) -> list[str]: visited.append(p) profile = cls.__profiles_map.get_by_key(p) inherited_profiles = profile.is_profile_of - for p in sorted(inherited_profiles, reverse=True): - if not p in visited: - queue.append(p) - if not p in result: - result.insert(0, p) + if inherited_profiles: + for p in sorted(inherited_profiles, reverse=True): + if not p in visited: + queue.append(p) + if not p in result: + result.insert(0, p) return result @property From f42fe151cee1fd2584292966570e30942b7a194b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:12:35 +0200 Subject: [PATCH 576/902] fix(logging): :bug: update log messages --- rocrate_validator/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index ba604cd1..16ca7494 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -676,7 +676,7 @@ def ok_file(p: Path) -> bool: requirement._order_number = i + 1 # log and return the requirements logger.debug("Profile %s loaded %s requirements: %s", - profile.name, len(requirements), requirements) + profile.identifier, len(requirements), requirements) return requirements @@ -1076,13 +1076,13 @@ def __do_validate__(self, assert len(profiles) > 0, "No profiles to validate" for profile in profiles: - logger.debug("Validating profile %s (id: %s)", profile.name, profile.token) + logger.debug("Validating profile %s (id: %s)", profile.name, profile.identifier) # perform the requirements validation requirements = profile.get_requirements( context.requirement_severity, exact_match=context.requirement_severity_only) - logger.debug("Validating profile %s with %s requirements", profile.name, len(requirements)) + logger.debug("Validating profile %s with %s requirements", profile.identifier, len(requirements)) logger.debug("For profile %s, validating these %s requirements: %s", - profile.name, len(requirements), requirements) + profile.identifier, len(requirements), requirements) for requirement in requirements: passed = requirement.__do_validate__(context) logger.debug("Number of issues: %s", len(context.result.issues)) From 84f71db9d3f5d0c3e75fc5489057fb54f6515529 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:14:23 +0200 Subject: [PATCH 577/902] fix(core): :bug: update instantiation of some exceptions --- rocrate_validator/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 16ca7494..191459c8 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1241,7 +1241,7 @@ def __load_profiles__(self) -> list[Profile]: for check in profile_checks: # ย find duplicated checks and raise an error if check.name in profile_check_names and not self.allow_shapes_override: - raise DuplicateRequirementCheck(check.name, profile.name) + raise DuplicateRequirementCheck(check.name, profile.identifier) # ย add check to the list profile_check_names.append(check.name) # ย mark overridden checks @@ -1252,7 +1252,7 @@ def __load_profiles__(self) -> list[Profile]: check.overridden_by = check_chain[-1] check_chain.append(check) else: - raise DuplicateRequirementCheck(check.name, profile.name) + raise DuplicateRequirementCheck(check.name, profile.identifier) return profiles From ed2bac8e45cef0fb955f2ff91823ccb0cf23d92d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:15:10 +0200 Subject: [PATCH 578/902] feat(core): :sparkles: add profile getters on the validation context --- rocrate_validator/models.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 191459c8..beda0ff2 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1262,8 +1262,11 @@ def profiles(self) -> list[Profile]: self._profiles = self.__load_profiles__() return self._profiles.copy() - def get_profile_by_token(self, token: str) -> Profile: + def get_profile_by_token(self, token: str) -> list[Profile]: + return [p for p in self.profiles if p.token == token] + + def get_profile_by_identifier(self, identifier: str) -> list[Profile]: for p in self.profiles: - if p.token == token: + if p.identifier == identifier: return p - raise ProfileNotFound(f"Profile with token '{token}' not found") + raise ProfileNotFound(identifier) From ac70f718c0549a8f4f076f10d8fcfd5b8435b6fe Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:18:17 +0200 Subject: [PATCH 579/902] feat(core): :sparkles: enable simplified profile selection using the latest version When the profile identifier matches only the prefix (or token), the validation will be performed using the most recent version of the matching set of profiles --- rocrate_validator/models.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index beda0ff2..3dc0a919 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1217,9 +1217,22 @@ def __load_profiles__(self) -> list[Profile]: severity=self.requirement_severity) # Check if the target profile is in the list of profiles - profile = Profile.get_by_token(self.profile_name) or Profile.get_by_name(self.profile_name)[0] - if profile is None: - raise ProfileNotFound(f"Profile '{self.profile_name}' not found in '{self.profiles_path}'") + profile = Profile.get_by_identifier(self.profile_name) + if not profile: + try: + candidate_profiles = Profile.get_by_token(self.profile_name) + logger.error("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) + self.settings["profile_name"] = profile.identifier + logger.error("Profile with the highest version number: %s", profile) + # if the profile is found by token, set the profile name to the identifier + self.settings["profile_name"] = profile.identifier + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + raise ProfileNotFound(f"Profile '{self.profile_name}' not found in '{self.profiles_path}'") # Set the profiles to validate against as the target profile and its inherited profiles profiles = profile.inherited_profiles + [profile] From e77e6c2df34e738224945a44fb98cdf5365a4494 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:20:13 +0200 Subject: [PATCH 580/902] docs(services): :art: add the missing type hint --- rocrate_validator/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 8ca09bf8..457c2751 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -31,7 +31,7 @@ def validate(settings: Union[dict, ValidationSettings]) -> ValidationResult: return result -def get_profiles(profiles_path: Path = DEFAULT_PROFILES_PATH, publicID: str = None, severity=Severity.OPTIONAL) -> list: +def get_profiles(profiles_path: Path = DEFAULT_PROFILES_PATH, publicID: str = None, severity=Severity.OPTIONAL) -> list[Profile]: """ Load the profiles from the given path """ From 7ef82e49f459e88b5970b5d35bb8d7dc79b8e201 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:21:52 +0200 Subject: [PATCH 581/902] refactor(shacl): :recycle: identity the profile within the context using the `identifier` property --- .../requirements/shacl/validator.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index df95a787..4e3c5c54 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -48,13 +48,13 @@ def __enter__(self) -> SHACLValidationContext: logger.debug("Entering SHACLValidationContextManager") if not self._shacl_context.__set_current_validation_profile__(self._profile): raise SHACLValidationAlreadyProcessed( - self._profile.name, self._shacl_context.get_validation_result(self._profile)) - logger.debug("Processing profile: %s (id: %s)", self._profile.name, self._profile.token) + self._profile.identifier, self._shacl_context.get_validation_result(self._profile)) + logger.debug("Processing profile: %s (id: %s)", self._profile.name, self._profile.identifier) if self._context.settings.get("target_only_validation", False) and \ - self._profile.token != self._context.settings.get("profile_name", None): - logger.debug("Skipping validation of profile %s", self._profile.name) - raise SHACLValidationSkip(f"Skipping validation of profile {self._profile.name}") - logger.debug("ValidationContext of profile %s initialized", self._profile.name) + self._profile.identifier != self._context.settings.get("profile_name", None): + logger.debug("Skipping validation of profile %s", self._profile.identifier) + raise SHACLValidationSkip(f"Skipping validation of profile {self._profile.identifier}") + logger.debug("ValidationContext of profile %s initialized", self._profile.identifier) return self._shacl_context def __exit__(self, exc_type, exc_val, exc_tb) -> None: @@ -98,7 +98,7 @@ def __init__(self, context: ValidationContext): self._ontology_graph: Graph = Graph() def __set_current_validation_profile__(self, profile: Profile) -> bool: - if not profile.token in self._processed_profiles: + if not profile.identifier in self._processed_profiles: # augment the ontology graph with the profile ontology ontology_graph = self.__load_ontology_graph__(profile.path) if ontology_graph: @@ -152,11 +152,11 @@ def current_validation_result(self, result: ValidationResult): # store the validation result self._validation_result = result # mark the profile as processed and store the result - self._processed_profiles[self._current_validation_profile.token] = result + self._processed_profiles[self._current_validation_profile.identifier] = result def get_validation_result(self, profile: Profile) -> Optional[bool]: assert profile is not None, "Invalid profile" - return self._processed_profiles.get(profile.token, None) + return self._processed_profiles.get(profile.identifier, None) @property def result(self) -> ValidationResult: From 529a3388df9054067684010071702715e8f199fb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:22:47 +0200 Subject: [PATCH 582/902] refactor(profiles): :recycle: update default profile value --- rocrate_validator/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index a2ac7c86..91c3a600 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -22,7 +22,7 @@ ROCRATE_METADATA_FILE = "ro-crate-metadata.json" # Define the default profiles name -DEFAULT_PROFILE_NAME = "ro-crate" +DEFAULT_PROFILE_NAME = "ro-crate-1.1" # Define the default profiles path DEFAULT_PROFILES_PATH = "profiles" From 0cdf04cd8081786ca805d82da4d29f25dd096c56 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:24:22 +0200 Subject: [PATCH 583/902] refactor(cli): :bug: display the profile identifier on the profiles table --- rocrate_validator/cli/commands/profiles.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 404ea4fe..28351b7d 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -80,9 +80,9 @@ def list_profiles(ctx): # , profiles_path: Path = DEFAULT_PROFILES_PATH): for severity, count in requirements.items() if count > 0]) # Add the row to the table - table.add_row(profile.token, profile.uri, - profile.label, Markdown(profile.description.strip()), - ", ".join([p.token for p in profile.inherited_profiles]), + table.add_row(profile.identifier, profile.uri, + profile.name, Markdown(profile.description.strip()), + ", ".join([p.identifier for p in profile.inherited_profiles]), requirements) table.add_row() From a1a5415678b3425182dbadb70414d8ed7425113c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 16:40:05 +0200 Subject: [PATCH 584/902] refactor(core): :recycle: rename `profile_name` param as `profile_identifier` --- rocrate_validator/cli/commands/profiles.py | 15 +++++----- rocrate_validator/cli/commands/validate.py | 14 ++++----- rocrate_validator/constants.py | 2 +- rocrate_validator/models.py | 24 +++++++-------- .../requirements/shacl/validator.py | 8 ++--- rocrate_validator/services.py | 8 ++--- .../workflow-ro-crate/test_main_workflow.py | 20 ++++++------- .../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 | 2 +- tests/shared.py | 6 ++-- tests/unit/requirements/test_profiles.py | 30 +++++++++---------- 14 files changed, 71 insertions(+), 72 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 28351b7d..17d66d79 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -7,7 +7,7 @@ from rocrate_validator import services from rocrate_validator.cli.main import cli, click from rocrate_validator.colors import get_severity_color -from rocrate_validator.constants import DEFAULT_PROFILE_NAME +from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER from rocrate_validator.models import (LevelCollection, RequirementLevel) from rocrate_validator.utils import get_profiles_path @@ -58,7 +58,7 @@ def list_profiles(ctx): # , profiles_path: Path = DEFAULT_PROFILES_PATH): caption="[cyan](*)[/cyan] Number of requirements by severity") # Define columns - table.add_column("ID", style="magenta bold", justify="center") + table.add_column("Identifier", style="magenta bold", justify="center") table.add_column("URI", style="yellow bold", justify="center") table.add_column("Name", style="white bold", justify="center") table.add_column("Description", style="white italic") @@ -80,7 +80,7 @@ def list_profiles(ctx): # , profiles_path: Path = DEFAULT_PROFILES_PATH): for severity, count in requirements.items() if count > 0]) # Add the row to the table - table.add_row(profile.identifier, profile.uri, + table.add_row(__format_version_identifier__(profile), profile.uri, profile.name, Markdown(profile.description.strip()), ", ".join([p.identifier for p in profile.inherited_profiles]), requirements) @@ -99,10 +99,10 @@ def list_profiles(ctx): # , profiles_path: Path = DEFAULT_PROFILES_PATH): default=False, show_default=True ) -@click.argument("profile-name", type=click.STRING, default=DEFAULT_PROFILE_NAME, required=True) +@click.argument("profile-identifier", type=click.STRING, default=DEFAULT_PROFILE_IDENTIFIER, required=True) @click.pass_context def describe_profile(ctx, - profile_name: str = DEFAULT_PROFILE_NAME, + profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, profiles_path: Path = DEFAULT_PROFILES_PATH, verbose: bool = False): """ @@ -110,10 +110,9 @@ def describe_profile(ctx, """ console = ctx.obj['console'] # Get the profile - profile = services.get_profile(profiles_path=profiles_path, profile_name=profile_name) - + profile = services.get_profile(profiles_path=profiles_path, profile_identifier=profile_identifier) console.print("\n", style="white bold") - console.print(f"[bold]Profile: {profile_name}[/bold]", style="magenta bold") + console.print(f"[bold]Profile: {profile_identifier}[/bold]", style="magenta bold") console.print("\n", style="white bold") console.print(Markdown(profile.description.strip())) console.print("\n", style="white bold") diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 2c0efebb..ec48a258 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -8,7 +8,7 @@ from rich.markdown import Markdown import rocrate_validator.log as logging -from rocrate_validator.constants import DEFAULT_PROFILE_NAME +from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER from ... import services from ...colors import get_severity_color @@ -45,11 +45,11 @@ ) @click.option( "-p", - "--profile-name", + "--profile-identifier", type=click.STRING, - default=DEFAULT_PROFILE_NAME, + default=DEFAULT_PROFILE_IDENTIFIER, show_default=True, - help="Name of the profile to use for validation", + help="Identifier of the profile to use for validation", ) @click.option( '-nh', @@ -85,7 +85,7 @@ @click.pass_context def validate(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH, - profile_name: str = DEFAULT_PROFILE_NAME, + profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, disable_profile_inheritance: bool = False, requirement_severity: str = Severity.REQUIRED.name, requirement_severity_only: bool = False, @@ -98,7 +98,7 @@ def validate(ctx, console: Console = ctx.obj['console'] # Log the input parameters for debugging logger.debug("profiles_path: %s", os.path.abspath(profiles_path)) - logger.debug("profile_name: %s", profile_name) + logger.debug("profile_identifier: %s", profile_identifier) logger.debug("requirement_severity: %s", requirement_severity) logger.debug("requirement_severity_only: %s", requirement_severity_only) @@ -116,7 +116,7 @@ def validate(ctx, result: ValidationResult = services.validate( { "profiles_path": profiles_path, - "profile_name": profile_name, + "profile_identifier": profile_identifier, "requirement_severity": requirement_severity, "requirement_severity_only": requirement_severity_only, "inherit_profiles": not disable_profile_inheritance, diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index 91c3a600..abeec8c9 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -22,7 +22,7 @@ ROCRATE_METADATA_FILE = "ro-crate-metadata.json" # Define the default profiles name -DEFAULT_PROFILE_NAME = "ro-crate-1.1" +DEFAULT_PROFILE_IDENTIFIER = "ro-crate-1.1" # Define the default profiles path DEFAULT_PROFILES_PATH = "profiles" diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 3dc0a919..5b284f86 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -15,7 +15,7 @@ import rocrate_validator.log as logging from rocrate_validator.constants import (DEFAULT_ONTOLOGY_FILE, - DEFAULT_PROFILE_NAME, + DEFAULT_PROFILE_IDENTIFIER, DEFAULT_PROFILE_README_FILE, IGNORED_PROFILE_DIRECTORIES, PROF_NS, PROFILE_FILE_EXTENSIONS, @@ -254,7 +254,7 @@ def description(self) -> str: with open(self.readme_file_path, "r") as f: self._description = f.read() else: - self._description = "RO-Crate profile" + self._description = self.comment return self._description @property @@ -984,7 +984,7 @@ class ValidationSettings: data_path: Path # Profile settings profiles_path: Path = DEFAULT_PROFILES_PATH - profile_name: str = DEFAULT_PROFILE_NAME + profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER inherit_profiles: bool = True allow_shapes_override: bool = True disable_check_for_duplicates: bool = False @@ -1090,7 +1090,7 @@ def __do_validate__(self, logger.debug("Validation Requirement passed") else: logger.debug(f"Validation Requirement {requirement} failed ") - if context.settings.get("abort_on_first") is True and context.profile_name == profile.name: + if context.settings.get("abort_on_first") is True and context.profile_identifier == profile.name: logger.debug("Aborting on first requirement failure") return context.result @@ -1188,8 +1188,8 @@ def inheritance_enabled(self) -> bool: return self.settings.get("inherit_profiles", False) @property - def profile_name(self) -> str: - return self.settings.get("profile_name") + def profile_identifier(self) -> str: + return self.settings.get("profile_identifier") @property def allow_shapes_override(self) -> bool: @@ -1205,7 +1205,7 @@ def __load_profiles__(self) -> list[Profile]: if not self.inheritance_enabled: profile = Profile.load( self.profiles_path, - self.profiles_path / self.profile_name, + self.profiles_path / self.profile_identifier, publicID=self.publicID, severity=self.requirement_severity) return [profile] @@ -1217,22 +1217,22 @@ def __load_profiles__(self) -> list[Profile]: severity=self.requirement_severity) # Check if the target profile is in the list of profiles - profile = Profile.get_by_identifier(self.profile_name) + profile = Profile.get_by_identifier(self.profile_identifier) if not profile: try: - candidate_profiles = Profile.get_by_token(self.profile_name) + candidate_profiles = Profile.get_by_token(self.profile_identifier) logger.error("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) - self.settings["profile_name"] = profile.identifier + self.settings["profile_identifier"] = profile.identifier logger.error("Profile with the highest version number: %s", profile) # if the profile is found by token, set the profile name to the identifier - self.settings["profile_name"] = profile.identifier + self.settings["profile_identifier"] = profile.identifier except Exception as e: if logger.isEnabledFor(logging.DEBUG): logger.exception(e) - raise ProfileNotFound(f"Profile '{self.profile_name}' not found in '{self.profiles_path}'") + raise ProfileNotFound(f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'") # Set the profiles to validate against as the target profile and its inherited profiles profiles = profile.inherited_profiles + [profile] diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 4e3c5c54..a089d94c 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -31,8 +31,8 @@ class SHACLValidationSkip(Exception): class SHACLValidationAlreadyProcessed(Exception): - def __init__(self, profile_name: str, result: SHACLValidationResult) -> None: - super().__init__(f"Profile {profile_name} has already been processed") + def __init__(self, profile_identifier: str, result: SHACLValidationResult) -> None: + super().__init__(f"Profile {profile_identifier} has already been processed") self.result = result @@ -51,8 +51,8 @@ def __enter__(self) -> SHACLValidationContext: self._profile.identifier, self._shacl_context.get_validation_result(self._profile)) logger.debug("Processing profile: %s (id: %s)", self._profile.name, self._profile.identifier) if self._context.settings.get("target_only_validation", False) and \ - self._profile.identifier != self._context.settings.get("profile_name", None): - logger.debug("Skipping validation of profile %s", self._profile.identifier) + self._profile.identifier != self._context.settings.get("profile_identifier", None): + logger.error("Skipping validation of profile %s", self._profile.identifier) raise SHACLValidationSkip(f"Skipping validation of profile {self._profile.identifier}") logger.debug("ValidationContext of profile %s initialized", self._profile.identifier) return self._shacl_context diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 457c2751..2cf15396 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -2,7 +2,7 @@ from typing import Union import rocrate_validator.log as logging -from rocrate_validator.constants import DEFAULT_PROFILE_NAME +from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER from .models import (Profile, Severity, ValidationResult, ValidationSettings, Validator) @@ -41,14 +41,14 @@ def get_profiles(profiles_path: Path = DEFAULT_PROFILES_PATH, publicID: str = No def get_profile(profiles_path: Path = DEFAULT_PROFILES_PATH, - profile_name: str = DEFAULT_PROFILE_NAME, publicID: str = None) -> Profile: + profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, publicID: str = None) -> Profile: """ Load the profiles from the given path """ - profile_path = profiles_path / profile_name + profile_path = profiles_path / profile_identifier if not Path(profiles_path).exists(): raise FileNotFoundError(f"Profile not found: {profile_path}") - profile = Profile.load(profiles_path, f"{profiles_path}/{profile_name}", + profile = Profile.load(profiles_path, f"{profiles_path}/{profile_identifier}", publicID=publicID, severity=Severity.OPTIONAL) logger.debug("Profile loaded: %s", profile) return profile 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 3b96ce11..0815dc74 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py +++ b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py @@ -18,7 +18,7 @@ def test_main_workflow_bad_type(): False, ["Main Workflow definition"], ["The Main Workflow must have types File, SoftwareSourceCode, ComputationalWorfklow"], - profile_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) @@ -33,7 +33,7 @@ def test_main_workflow_no_lang(): False, ["Main Workflow definition"], ["The Main Workflow must refer to its language via programmingLanguage"], - profile_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) @@ -48,7 +48,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_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) @@ -63,7 +63,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_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) @@ -78,7 +78,7 @@ def test_main_workflow_cwl_desc_bad_type(): False, ["Main Workflow optional properties"], ["The CWL Description type must be File, SoftwareSourceCode, HowTo"], - profile_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) @@ -93,7 +93,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_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) @@ -107,7 +107,7 @@ def test_main_workflow_file_existence(): False, ["Main Workflow file existence"], ["Main Workflow", "not found in crate"], - profile_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) @@ -122,7 +122,7 @@ def test_workflow_diagram_file_existence(): False, ["Workflow-related files existence"], ["Workflow diagram", "not found in crate"], - profile_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) @@ -137,7 +137,7 @@ def test_workflow_description_file_existence(): False, ["Workflow-related files existence"], ["Workflow CWL description", "not found in crate"], - profile_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) @@ -152,5 +152,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_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) 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 b5652ee6..2b092b94 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py +++ b/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py @@ -13,11 +13,11 @@ def test_valid_workflow_roc_required(): ValidROC().workflow_roc, Severity.REQUIRED, True, - profile_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) do_entity_test( ValidROC().workflow_roc_string_license, Severity.REQUIRED, True, - profile_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) 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 b3b9d523..b7ec71cf 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py @@ -17,7 +17,7 @@ def test_wroc_no_tests(): False, ["test directory"], ["The test/ dir should be a Dataset"], - profile_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) @@ -31,5 +31,5 @@ def test_wroc_no_examples(): False, ["examples directory"], ["The examples/ dir should be a Dataset"], - profile_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) 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 a92c044b..b1a387ad 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py @@ -19,5 +19,5 @@ def test_wroc_descriptor_bad_conforms_to(): False, ["WROC Metadata File Descriptor properties"], ["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_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) 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 46e5a212..0e7b8ea2 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py @@ -17,7 +17,7 @@ def test_wroc_readme_not_about_crate(): False, ["README.md properties"], ["The README.md SHOULD be about the crate"], - profile_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) @@ -31,5 +31,5 @@ def test_wroc_readme_wrong_encoding_format(): False, ["README.md properties"], ["The README.md SHOULD have text/markdown as its encodingFormat"], - profile_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) 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 32d62c17..2688f664 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 @@ -17,5 +17,5 @@ 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_name="workflow-ro-crate" + profile_identifier="workflow-ro-crate" ) diff --git a/tests/shared.py b/tests/shared.py index f1c46621..34e11a84 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -8,7 +8,7 @@ from typing import Optional, TypeVar, Union from rocrate_validator import models, services -from rocrate_validator.constants import DEFAULT_PROFILE_NAME +from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER logger = logging.getLogger(__name__) @@ -27,7 +27,7 @@ def do_entity_test( expected_triggered_requirements: Optional[list[str]] = None, expected_triggered_issues: Optional[list[str]] = None, abort_on_first: bool = True, - profile_name: str = DEFAULT_PROFILE_NAME + profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER ): """ Shared function to test a RO-Crate entity @@ -57,7 +57,7 @@ def do_entity_test( "data_path": rocrate_path, "requirement_severity": requirement_severity, "abort_on_first": abort_on_first, - "profile_name": profile_name + "profile_identifier": profile_identifier })) logger.debug("Expected validation result: %s", expected_validation_result) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 9910a7e8..3227a865 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -3,7 +3,7 @@ import pytest -from rocrate_validator.constants import DEFAULT_PROFILE_NAME +from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER from rocrate_validator.errors import DuplicateRequirementCheck, InvalidProfilePath from rocrate_validator.models import (Profile, ValidationContext, ValidationSettings, Validator) @@ -25,7 +25,7 @@ def test_order_of_loaded_profiles(profiles_path: str): assert len(profiles) > 0 # Extract the profile names - profile_names = sorted([profile.name for profile in profiles]) + profile_names = sorted([profile.token for profile in profiles]) logger.debug("The profile names: %r", profile_names) # The order of the profiles should be the same as the order of the directories @@ -39,7 +39,7 @@ def test_load_invalid_profile_from_validation_context(fake_profiles_path: str): """Test the loaded profiles from the validator context.""" settings = { "profiles_path": "/tmp/random_path_xxx", - "profile_name": DEFAULT_PROFILE_NAME, + "profile_identifier": DEFAULT_PROFILE_IDENTIFIER, "data_path": "/tmp/random_path", "inherit_profiles": False } @@ -61,7 +61,7 @@ def test_load_valid_profile_without_inheritance_from_validation_context(fake_pro """Test the loaded profiles from the validator context.""" settings = { "profiles_path": fake_profiles_path, - "profile_name": "c", + "profile_identifier": "c", "data_path": "/tmp/random_path", "inherit_profiles": False } @@ -85,7 +85,7 @@ def test_profile_spec_properties(fake_profiles_path: str): """Test the loaded profiles from the validator context.""" settings = { "profiles_path": fake_profiles_path, - "profile_name": "c", + "profile_identifier": "c", "data_path": "/tmp/random_path", "inherit_profiles": True, "disable_check_for_duplicates": True, @@ -106,8 +106,8 @@ def test_profile_spec_properties(fake_profiles_path: str): assert len(profiles) == 2, "The number of profiles should be 2" # Get the profile - profile = context.get_profile_by_token("c") - assert profile.name == "c", "The profile name should be c" + profile = context.get_profile_by_token("c")[0] + assert profile.token == "c", "The profile name should be c" assert profile.comment == "Comment for the Profile C.", "The profile comment should be 'Comment for the Profile C.'" assert profile.version == "1.0.0", "The profile version should be 1.0.0" assert profile.is_profile_of == ["https://w3id.org/a"], "The profileOf property should be ['a']" @@ -118,10 +118,10 @@ def test_profile_spec_properties(fake_profiles_path: str): def test_loaded_valid_profile_with_inheritance_from_validator_context(fake_profiles_path: str): """Test the loaded profiles from the validator context.""" - def __perform_test__(profile_name: str, expected_inherited_profiles: list[str]): + def __perform_test__(profile_identifier: str, expected_inherited_profiles: list[str]): settings = { "profiles_path": fake_profiles_path, - "profile_name": profile_name, + "profile_identifier": profile_identifier, "data_path": "/tmp/random_path", "disable_check_for_duplicates": True, } @@ -137,11 +137,11 @@ def __perform_test__(profile_name: str, expected_inherited_profiles: list[str]): logger.debug("The profiles: %r", profiles) # get and check the profile - profile = context.get_profile_by_token(profile_name) - assert profile.name == profile_name, f"The profile name should be {profile_name}" + profile = context.get_profile_by_token(profile_identifier)[0] + assert profile.token == profile_identifier, f"The profile name should be {profile_identifier}" # The number of profiles should be 1 - profiles_names = [_.name for _ in profile.inherited_profiles] + profiles_names = [_.token for _ in profile.inherited_profiles] assert profiles_names == expected_inherited_profiles, f"The number of profiles should be {expected_inherited_profiles}" # Test the inheritance mode with 1 profile @@ -160,7 +160,7 @@ def test_load_invalid_profile_no_override_enabled(fake_profiles_path: str): """Test the loaded profiles from the validator context.""" settings = { "profiles_path": fake_profiles_path, - "profile_name": "invalid-duplicated-shapes", + "profile_identifier": "invalid-duplicated-shapes", "data_path": "/tmp/random_path", "inherit_profiles": True, "allow_shapes_override": False, @@ -184,7 +184,7 @@ def test_load_invalid_profile_with_override_on_same_profile(fake_profiles_path: """Test the loaded profiles from the validator context.""" settings = { "profiles_path": fake_profiles_path, - "profile_name": "invalid-duplicated-shapes", + "profile_identifier": "invalid-duplicated-shapes", "data_path": "/tmp/random_path", "inherit_profiles": True, "allow_shapes_override": False @@ -207,7 +207,7 @@ def test_load_valid_profile_with_override_on_inherited_profile(fake_profiles_pat """Test the loaded profiles from the validator context.""" settings = { "profiles_path": fake_profiles_path, - "profile_name": "c-overridden", + "profile_identifier": "c-overridden", "data_path": "/tmp/random_path", "inherit_profiles": True, "allow_shapes_override": True From c6497333a20e0ace61b9804497323591a7ceff8b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 16:45:38 +0200 Subject: [PATCH 585/902] docs(profiles/workflow-ro-crate): :memo: update workflow ro-crate comment --- rocrate_validator/profiles/workflow-ro-crate/profile.ttl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/profiles/workflow-ro-crate/profile.ttl b/rocrate_validator/profiles/workflow-ro-crate/profile.ttl index 99fec7cf..7f293477 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/profile.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/profile.ttl @@ -12,7 +12,7 @@ rdfs:label "Workflow RO-Crate Metadata Specification 1.0" ; # regular metadata, a basic description of the Profile - rdfs:comment """RO-Crate Metadata Specification."""@en ; + rdfs:comment """Workflow RO-Crate Metadata Specification."""@en ; # URI of the publisher of the Workflow RO-Crate Metadata Specification dct:publisher ; From 2e8d31e01e0195b83f00fee80f9fb0b4fb629227 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 16:46:30 +0200 Subject: [PATCH 586/902] style(cli): :lipstick: reformat profile identifier --- rocrate_validator/cli/commands/profiles.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 17d66d79..7456e275 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -90,6 +90,23 @@ def list_profiles(ctx): # , profiles_path: Path = DEFAULT_PROFILES_PATH): console.print(table) +def __format_version_identifier__(profile): + """ + Format the version and identifier + """ + + table = Table(show_header=True, + title=profile.identifier, + header_style="bold cyan", + border_style="bright_black", + show_footer=False) + table.add_column("prefix", style="magenta bold", justify="center") + table.add_column("version", style="yellow bold", justify="center") + + table.add_row(profile.token, profile.version) + return table + + @profiles.command("describe") @click.option( '-v', From 49e8d4e9373f57fc66b251ae35e6cbfbc08792ed Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:33:59 +0200 Subject: [PATCH 587/902] fix(logging): :bug: fix log level --- tests/unit/requirements/test_profiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 3227a865..e051b99d 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -18,7 +18,7 @@ def test_order_of_loaded_profiles(profiles_path: str): """Test the order of the loaded profiles.""" - logger.error("The profiles path: %r", profiles_path) + logger.debug("The profiles path: %r", profiles_path) assert os.path.exists(profiles_path) profiles = Profile.load_profiles(profiles_path=profiles_path) # The number of profiles should be greater than 0 From 7997a9b864377d5c3773bb7692ed572463742975 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 17:58:26 +0200 Subject: [PATCH 588/902] feat(shacl): :sparkles: fail fast SHACL validation when JSON data are invalid --- rocrate_validator/requirements/shacl/checks.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 38f4a3b7..3c5dab8c 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -1,3 +1,4 @@ +import json from timeit import default_timer as timer from typing import Optional @@ -67,10 +68,15 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): end_time = timer() logger.debug(f"Execution time for getting ontology graph: {end_time - start_time} seconds") - start_time = timer() - data_graph = shacl_context.data_graph - end_time = timer() - logger.debug(f"Execution time for getting data graph: {end_time - start_time} seconds") + data_graph = None + try: + start_time = timer() + data_graph = shacl_context.data_graph + end_time = timer() + logger.debug(f"Execution time for getting data graph: {end_time - start_time} seconds") + except json.decoder.JSONDecodeError as e: + logger.warning("Unable to perform metadata validation due to an error in the JSON-LD data file: %s", e) + return False # Begin the timer start_time = timer() From 8b23ad3e52d7d2c31df6b25027d4fa0780782e79 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 18:16:15 +0200 Subject: [PATCH 589/902] fix(test-conf): :truck: move pytest configuration file --- tests/pytest.ini => pytest.ini | 1 + 1 file changed, 1 insertion(+) rename tests/pytest.ini => pytest.ini (81%) diff --git a/tests/pytest.ini b/pytest.ini similarity index 81% rename from tests/pytest.ini rename to pytest.ini index a1080c1e..f47b3af6 100644 --- a/tests/pytest.ini +++ b/pytest.ini @@ -3,3 +3,4 @@ # log_cli=true # log_level=INFO ; filterwarnings = +addopts = -n auto From 81c4b253612b4af333e172cf071fd8b57ed334ff Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 18:16:56 +0200 Subject: [PATCH 590/902] fix(core): :sparkles: always sort profiles by deps order profiles according to the number of profiles they depend on: i.e, first the profiles that do not depend on any other profile then the profiles that depend on the previous ones, and so on --- rocrate_validator/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 5b284f86..00205ebd 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -421,8 +421,10 @@ def load_profiles(profiles_path: Union[str, Path], if profile_path.is_dir() and profile_path not in IGNORED_PROFILE_DIRECTORIES: profile = Profile.load(profiles_path, profile_path, publicID=publicID, severity=severity) profiles.append(profile) - # order profiles according to the dependencies between them: first the profiles that do not depend on ??? - return profiles + # order profiles according to the number of profiles they depend on: + # i.e, first the profiles that do not depend on any other profile + # then the profiles that depend on the previous ones, and so on + return sorted(profiles, key=lambda x: len(x.inherited_profiles)) @classmethod def get_by_identifier(cls, identifier: str) -> Profile: From 101fae1f37fb49c5ce1d90a9eccea51316e63f42 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 18:50:11 +0200 Subject: [PATCH 591/902] feat(cli): :lipstick: improve output of `list profiles` command --- rocrate_validator/cli/commands/profiles.py | 26 +++++----------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 7456e275..96b08225 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -60,9 +60,10 @@ def list_profiles(ctx): # , profiles_path: Path = DEFAULT_PROFILES_PATH): # Define columns table.add_column("Identifier", style="magenta bold", justify="center") table.add_column("URI", style="yellow bold", justify="center") + table.add_column("Version", style="green bold", justify="center") table.add_column("Name", style="white bold", justify="center") table.add_column("Description", style="white italic") - table.add_column("based on", style="white", justify="center") + table.add_column("Based on", style="white", justify="center") table.add_column("Requirements (*)", style="white", justify="center") # Add data to the table @@ -74,15 +75,15 @@ def list_profiles(ctx): # , profiles_path: Path = DEFAULT_PROFILES_PATH): if not requirements.get(req.severity.name, None): requirements[req.severity.name] = 0 requirements[req.severity.name] += 1 - requirements = ", ".join( + requirements = "\n".join( [f"[bold][{get_severity_color(severity)}]{severity}: " f"{count}[/{get_severity_color(severity)}][/bold]" for severity, count in requirements.items() if count > 0]) # Add the row to the table - table.add_row(__format_version_identifier__(profile), profile.uri, + table.add_row(profile.identifier, profile.uri, profile.version, profile.name, Markdown(profile.description.strip()), - ", ".join([p.identifier for p in profile.inherited_profiles]), + "\n".join([p.identifier for p in profile.inherited_profiles]), requirements) table.add_row() @@ -90,23 +91,6 @@ def list_profiles(ctx): # , profiles_path: Path = DEFAULT_PROFILES_PATH): console.print(table) -def __format_version_identifier__(profile): - """ - Format the version and identifier - """ - - table = Table(show_header=True, - title=profile.identifier, - header_style="bold cyan", - border_style="bright_black", - show_footer=False) - table.add_column("prefix", style="magenta bold", justify="center") - table.add_column("version", style="yellow bold", justify="center") - - table.add_row(profile.token, profile.version) - return table - - @profiles.command("describe") @click.option( '-v', From 2263dffd1ccd18f38af9f8f71f906fd6343992b3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 19:11:17 +0200 Subject: [PATCH 592/902] feat(cli): :lipstick: improve output of `profiles describe` command --- rocrate_validator/cli/commands/profiles.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 96b08225..fe9b75ba 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -1,6 +1,7 @@ from pathlib import Path from rich.markdown import Markdown +from rich.panel import Panel from rich.table import Table import rocrate_validator.log as logging @@ -8,8 +9,7 @@ from rocrate_validator.cli.main import cli, click from rocrate_validator.colors import get_severity_color from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER -from rocrate_validator.models import (LevelCollection, - RequirementLevel) +from rocrate_validator.models import LevelCollection, RequirementLevel from rocrate_validator.utils import get_profiles_path # set the default profiles path @@ -109,19 +109,25 @@ def describe_profile(ctx, """ Show a profile """ + # Get the console console = ctx.obj['console'] # Get the profile profile = services.get_profile(profiles_path=profiles_path, profile_identifier=profile_identifier) + # Print the profile header console.print("\n", style="white bold") - console.print(f"[bold]Profile: {profile_identifier}[/bold]", style="magenta bold") - console.print("\n", style="white bold") - console.print(Markdown(profile.description.strip())) - console.print("\n", style="white bold") - + title_text = f"[bold cyan]Version:[/bold cyan] [italic green]{profile.version}[/italic green]\n" + title_text += f"[bold cyan]URI:[/bold cyan] [italic yellow]{profile.uri}[/italic yellow]\n" + title_text += f"[bold cyan]Description:[/bold cyan] [italic]{profile.description.strip()}[/italic]" + box = Panel( + title_text, title=f"[bold][cyan]Profile:[/cyan] [magenta italic]{profile.identifier}[/magenta italic][/bold]", padding=(1, 1)) + console.print(box) + # Print the profile requirements if not verbose: __compacted_describe_profile__(console, profile) else: __verbose_describe_profile__(console, profile) + # End with a new line + console.print("\n") def __requirement_level_style__(requirement: RequirementLevel): From 12b0279c9bc5b1474346e6d59c6337a70c292d4d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 19:21:34 +0200 Subject: [PATCH 593/902] feat(cli): :lipstick: report the total number of reqs and checks --- rocrate_validator/cli/commands/profiles.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index fe9b75ba..203a22c6 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -144,10 +144,8 @@ def __compacted_describe_profile__(console, profile): """ table_rows = [] levels_list = set() - for requirement in profile.requirements: - # skip hidden requirements - if requirement.hidden: - continue + requirements = [_ for _ in profile.requirements if not _.hidden] + for requirement in requirements: # add the requirement to the list color = get_severity_color(requirement.severity) level_info = f"[{color}]{requirement.severity.name}[/{color}]" @@ -159,7 +157,7 @@ def __compacted_describe_profile__(console, profile): f"{len(requirement.get_checks_by_level(LevelCollection.OPTIONAL))}")) table = Table(show_header=True, - title="Profile Requirements", + title=f"[cyan]{len(requirements)}[/cyan] Profile Requirements", title_style="italic bold", header_style="bold cyan", border_style="bright_black", @@ -188,6 +186,7 @@ def __verbose_describe_profile__(console, profile): """ table_rows = [] levels_list = set() + count_checks = 0 for requirement in profile.requirements: # skip hidden requirements if requirement.hidden: @@ -201,9 +200,10 @@ def __verbose_describe_profile__(console, profile): # checks.append(check) table_rows.append((str(check.identifier).rjust(14), check.name, Markdown(check.description.strip()), level_info)) + count_checks += 1 table = Table(show_header=True, - title="Profile Requirements Checks", + title=f"[cyan]{count_checks}[/cyan] Profile Requirements Checks", title_style="italic bold", header_style="bold cyan", border_style="bright_black", From 47e459f1b10b80a6948da594ab5f77367fb164e6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 27 Jun 2024 19:25:59 +0200 Subject: [PATCH 594/902] refactor(cli): :lipstick: minor change to cli output --- rocrate_validator/cli/commands/profiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 203a22c6..78b06fe7 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -116,7 +116,7 @@ def describe_profile(ctx, # Print the profile header console.print("\n", style="white bold") title_text = f"[bold cyan]Version:[/bold cyan] [italic green]{profile.version}[/italic green]\n" - title_text += f"[bold cyan]URI:[/bold cyan] [italic yellow]{profile.uri}[/italic yellow]\n" + title_text += f"[bold cyan]URI:[/bold cyan] [italic yellow]{profile.uri}[/italic yellow]\n\n" title_text += f"[bold cyan]Description:[/bold cyan] [italic]{profile.description.strip()}[/italic]" box = Panel( title_text, title=f"[bold][cyan]Profile:[/cyan] [magenta italic]{profile.identifier}[/magenta italic][/bold]", padding=(1, 1)) From 2b1b3f4d739e3f29fed1e581b87717e238114458 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 28 Jun 2024 12:08:44 +0200 Subject: [PATCH 595/902] fix(core): :bug: preserve alphabetical order of profiles --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 00205ebd..2c57a127 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -424,7 +424,7 @@ def load_profiles(profiles_path: Union[str, Path], # order profiles according to the number of profiles they depend on: # i.e, first the profiles that do not depend on any other profile # then the profiles that depend on the previous ones, and so on - return sorted(profiles, key=lambda x: len(x.inherited_profiles)) + return sorted(profiles, key=lambda x: f"{len(x.inherited_profiles)}_{x.identifier}") @classmethod def get_by_identifier(cls, identifier: str) -> Profile: From 9199819da4c6415a3dc10009d4325ee49d844608 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 28 Jun 2024 12:14:19 +0200 Subject: [PATCH 596/902] test(core): :white_check_mark: test profiles loaded from a free folder structure --- tests/conftest.py | 5 +++ .../free_folder_structure/a/profile.ttl | 28 +++++++++++++++++ .../free_folder_structure/a/shape_a.ttl | 22 +++++++++++++ .../nested_b/nested_b/b/profile.ttl | 27 ++++++++++++++++ .../nested_b/nested_b/b/shape_b.ttl | 22 +++++++++++++ .../nested_c/c/profile.ttl | 31 +++++++++++++++++++ .../nested_c/c/shape_c.ttl | 22 +++++++++++++ tests/unit/requirements/test_profiles.py | 13 ++++++++ 8 files changed, 170 insertions(+) create mode 100644 tests/data/profiles/free_folder_structure/a/profile.ttl create mode 100644 tests/data/profiles/free_folder_structure/a/shape_a.ttl create mode 100644 tests/data/profiles/free_folder_structure/nested_b/nested_b/b/profile.ttl create mode 100644 tests/data/profiles/free_folder_structure/nested_b/nested_b/b/shape_b.ttl create mode 100644 tests/data/profiles/free_folder_structure/nested_c/c/profile.ttl create mode 100644 tests/data/profiles/free_folder_structure/nested_c/c/shape_c.ttl diff --git a/tests/conftest.py b/tests/conftest.py index 6e4e72a0..d47e185a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,6 +38,11 @@ def fake_profiles_path(): return f"{TEST_DATA_PATH}/profiles/fake" +@fixture +def profiles_with_free_folder_structure_path(): + return f"{TEST_DATA_PATH}/profiles/free_folder_structure" + + @fixture def graphs_path(): return f"{TEST_DATA_PATH}/graphs" diff --git a/tests/data/profiles/free_folder_structure/a/profile.ttl b/tests/data/profiles/free_folder_structure/a/profile.ttl new file mode 100644 index 00000000..8f7739db --- /dev/null +++ b/tests/data/profiles/free_folder_structure/a/profile.ttl @@ -0,0 +1,28 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Profile A" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Comment for the Profile A."""@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.1 profile + # prof:isProfileOf ; + + # # Explicitly state that this profile is a transitive profile of the RO-Crate Metadata Specification 1.1 profile + # prof:isTransitiveProfileOf , ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "a" ; +. diff --git a/tests/data/profiles/free_folder_structure/a/shape_a.ttl b/tests/data/profiles/free_folder_structure/a/shape_a.ttl new file mode 100644 index 00000000..97f898ad --- /dev/null +++ b/tests/data/profiles/free_folder_structure/a/shape_a.ttl @@ -0,0 +1,22 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +ro:ShapeA + a sh:NodeShape ; + sh:name "The Shape A" ; + sh:description "This is the Shape A" ; + sh:targetNode ro:ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check Metadata File Descriptor entity existence" ; + sh:description "Check if the RO-Crate Metadata File Descriptor entity exists" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + ] . diff --git a/tests/data/profiles/free_folder_structure/nested_b/nested_b/b/profile.ttl b/tests/data/profiles/free_folder_structure/nested_b/nested_b/b/profile.ttl new file mode 100644 index 00000000..707e55e7 --- /dev/null +++ b/tests/data/profiles/free_folder_structure/nested_b/nested_b/b/profile.ttl @@ -0,0 +1,27 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Profile B" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Comment for the Profile B."""@en ; + + # URI of the publisher of the profile B + dct:publisher ; + + # This profile is an extension of the profile A + prof:isProfileOf ; + + # Explicitly state that this profile is a transitive profile of the profile A + prof:isTransitiveProfileOf ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "b" ; +. diff --git a/tests/data/profiles/free_folder_structure/nested_b/nested_b/b/shape_b.ttl b/tests/data/profiles/free_folder_structure/nested_b/nested_b/b/shape_b.ttl new file mode 100644 index 00000000..328fb881 --- /dev/null +++ b/tests/data/profiles/free_folder_structure/nested_b/nested_b/b/shape_b.ttl @@ -0,0 +1,22 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +ro:ShapeB + a sh:NodeShape ; + sh:name "The Shape B" ; + sh:description "This is the Shape B" ; + sh:targetNode ro:ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check Metadata File Descriptor entity existence" ; + sh:description "Check if the RO-Crate Metadata File Descriptor entity exists" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + ] . diff --git a/tests/data/profiles/free_folder_structure/nested_c/c/profile.ttl b/tests/data/profiles/free_folder_structure/nested_c/c/profile.ttl new file mode 100644 index 00000000..c622c862 --- /dev/null +++ b/tests/data/profiles/free_folder_structure/nested_c/c/profile.ttl @@ -0,0 +1,31 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . +@prefix schema: . + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Profile C" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Comment for the Profile C."""@en ; + + # the version of the profile + schema:version "1.0.0" ; + + # URI of the publisher of the profile C + dct:publisher ; + + # This profile is an extension of the profile A + prof:isProfileOf ; + + # Explicitly state that this profile is a transitive profile of the profile A + prof:isTransitiveProfileOf ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "c" ; +. diff --git a/tests/data/profiles/free_folder_structure/nested_c/c/shape_c.ttl b/tests/data/profiles/free_folder_structure/nested_c/c/shape_c.ttl new file mode 100644 index 00000000..cb9a0b59 --- /dev/null +++ b/tests/data/profiles/free_folder_structure/nested_c/c/shape_c.ttl @@ -0,0 +1,22 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +ro:ShapeC + a sh:NodeShape ; + sh:name "The Shape C" ; + sh:description "This is the Shape C" ; + sh:targetNode ro:ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check Metadata File Descriptor entity existence" ; + sh:description "Check if the RO-Crate Metadata File Descriptor entity exists" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + ] . diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index e051b99d..5a11c554 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -115,6 +115,19 @@ def test_profile_spec_properties(fake_profiles_path: str): "https://w3id.org/a"], "The transitiveProfileOf property should be ['a']" +def test_profiles_loading_free_folder_structure(profiles_with_free_folder_structure_path): + """Test the loaded profiles from the validator context.""" + profiles = Profile.load_profiles(profiles_path=profiles_with_free_folder_structure_path) + logger.debug("The profiles: %r", profiles) + for p in profiles: + logger.warning("The profile '%s' has %d requirements", p, len(p.requirements)) + + assert len(profiles) == 3, "The number of profiles should be 3" + assert profiles[0].token == "a", "The profile name should be 'a'" + assert profiles[1].token == "b", "The profile name should be 'b'" + assert profiles[2].token == "c", "The profile name should be 'c'" + + def test_loaded_valid_profile_with_inheritance_from_validator_context(fake_profiles_path: str): """Test the loaded profiles from the validator context.""" From 4a01a2c1047950251b1a64cd1534e2f07ae8e38f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 28 Jun 2024 12:48:40 +0200 Subject: [PATCH 597/902] test(core): :white_check_mark: refactor test --- tests/unit/requirements/test_profiles.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 5a11c554..2465d1ca 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -122,7 +122,14 @@ def test_profiles_loading_free_folder_structure(profiles_with_free_folder_struct for p in profiles: logger.warning("The profile '%s' has %d requirements", p, len(p.requirements)) + # The number of profiles should be 3 assert len(profiles) == 3, "The number of profiles should be 3" + + # The profile names should be a, b, and c + assert profiles[0].token == "a", "The profile name should be 'a'" + assert profiles[1].token == "b", "The profile name should be 'b'" + assert profiles[2].token == "c", "The profile name should be 'c'" + assert profiles[0].token == "a", "The profile name should be 'a'" assert profiles[1].token == "b", "The profile name should be 'b'" assert profiles[2].token == "c", "The profile name should be 'c'" From 9f62229640252714ca1142a72dc8756a81475591 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 28 Jun 2024 12:49:55 +0200 Subject: [PATCH 598/902] test(core): :white_check_mark: test detection of profile version --- tests/conftest.py | 4 +++ .../a_explicit_version_property/profile.ttl | 26 +++++++++++++++++ .../a_explicit_version_property/shape_a.ttl | 22 +++++++++++++++ .../inferred_by_folder/2.0/profile.ttl | 27 ++++++++++++++++++ .../inferred_by_folder/2.0/shape_b.ttl | 22 +++++++++++++++ .../inferred_from_uri/profile.ttl | 28 +++++++++++++++++++ .../inferred_from_uri/shape_c.ttl | 22 +++++++++++++++ tests/unit/requirements/test_profiles.py | 19 +++++++++++++ 8 files changed, 170 insertions(+) create mode 100644 tests/data/profiles/fake_versioned_profiles/a_explicit_version_property/profile.ttl create mode 100644 tests/data/profiles/fake_versioned_profiles/a_explicit_version_property/shape_a.ttl create mode 100644 tests/data/profiles/fake_versioned_profiles/inferred_by_folder/2.0/profile.ttl create mode 100644 tests/data/profiles/fake_versioned_profiles/inferred_by_folder/2.0/shape_b.ttl create mode 100644 tests/data/profiles/fake_versioned_profiles/inferred_from_uri/profile.ttl create mode 100644 tests/data/profiles/fake_versioned_profiles/inferred_from_uri/shape_c.ttl diff --git a/tests/conftest.py b/tests/conftest.py index d47e185a..a0b4b612 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -43,6 +43,10 @@ def profiles_with_free_folder_structure_path(): return f"{TEST_DATA_PATH}/profiles/free_folder_structure" +@fixture +def fake_versioned_profiles_path(): + return f"{TEST_DATA_PATH}/profiles/fake_versioned_profiles" + @fixture def graphs_path(): return f"{TEST_DATA_PATH}/graphs" diff --git a/tests/data/profiles/fake_versioned_profiles/a_explicit_version_property/profile.ttl b/tests/data/profiles/fake_versioned_profiles/a_explicit_version_property/profile.ttl new file mode 100644 index 00000000..78555e3d --- /dev/null +++ b/tests/data/profiles/fake_versioned_profiles/a_explicit_version_property/profile.ttl @@ -0,0 +1,26 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . +@prefix schema: . + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Profile A" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Comment for the Profile A."""@en ; + + # URI of the publisher of the Workflow RO-Crate Metadata Specification + dct:publisher ; + + # the version of the profile + schema:version "1.0.0" ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "a" ; + +. diff --git a/tests/data/profiles/fake_versioned_profiles/a_explicit_version_property/shape_a.ttl b/tests/data/profiles/fake_versioned_profiles/a_explicit_version_property/shape_a.ttl new file mode 100644 index 00000000..97f898ad --- /dev/null +++ b/tests/data/profiles/fake_versioned_profiles/a_explicit_version_property/shape_a.ttl @@ -0,0 +1,22 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +ro:ShapeA + a sh:NodeShape ; + sh:name "The Shape A" ; + sh:description "This is the Shape A" ; + sh:targetNode ro:ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check Metadata File Descriptor entity existence" ; + sh:description "Check if the RO-Crate Metadata File Descriptor entity exists" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + ] . diff --git a/tests/data/profiles/fake_versioned_profiles/inferred_by_folder/2.0/profile.ttl b/tests/data/profiles/fake_versioned_profiles/inferred_by_folder/2.0/profile.ttl new file mode 100644 index 00000000..707e55e7 --- /dev/null +++ b/tests/data/profiles/fake_versioned_profiles/inferred_by_folder/2.0/profile.ttl @@ -0,0 +1,27 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Profile B" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Comment for the Profile B."""@en ; + + # URI of the publisher of the profile B + dct:publisher ; + + # This profile is an extension of the profile A + prof:isProfileOf ; + + # Explicitly state that this profile is a transitive profile of the profile A + prof:isTransitiveProfileOf ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "b" ; +. diff --git a/tests/data/profiles/fake_versioned_profiles/inferred_by_folder/2.0/shape_b.ttl b/tests/data/profiles/fake_versioned_profiles/inferred_by_folder/2.0/shape_b.ttl new file mode 100644 index 00000000..328fb881 --- /dev/null +++ b/tests/data/profiles/fake_versioned_profiles/inferred_by_folder/2.0/shape_b.ttl @@ -0,0 +1,22 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +ro:ShapeB + a sh:NodeShape ; + sh:name "The Shape B" ; + sh:description "This is the Shape B" ; + sh:targetNode ro:ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check Metadata File Descriptor entity existence" ; + sh:description "Check if the RO-Crate Metadata File Descriptor entity exists" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + ] . diff --git a/tests/data/profiles/fake_versioned_profiles/inferred_from_uri/profile.ttl b/tests/data/profiles/fake_versioned_profiles/inferred_from_uri/profile.ttl new file mode 100644 index 00000000..10e790af --- /dev/null +++ b/tests/data/profiles/fake_versioned_profiles/inferred_from_uri/profile.ttl @@ -0,0 +1,28 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . +@prefix schema: . + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Profile C" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Comment for the Profile C."""@en ; + + # URI of the publisher of the profile C + dct:publisher ; + + # This profile is an extension of the profile A + prof:isProfileOf ; + + # Explicitly state that this profile is a transitive profile of the profile A + prof:isTransitiveProfileOf ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "c" ; +. diff --git a/tests/data/profiles/fake_versioned_profiles/inferred_from_uri/shape_c.ttl b/tests/data/profiles/fake_versioned_profiles/inferred_from_uri/shape_c.ttl new file mode 100644 index 00000000..cb9a0b59 --- /dev/null +++ b/tests/data/profiles/fake_versioned_profiles/inferred_from_uri/shape_c.ttl @@ -0,0 +1,22 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +ro:ShapeC + a sh:NodeShape ; + sh:name "The Shape C" ; + sh:description "This is the Shape C" ; + sh:targetNode ro:ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check Metadata File Descriptor entity existence" ; + sh:description "Check if the RO-Crate Metadata File Descriptor entity exists" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + ] . diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 2465d1ca..48b229ed 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -130,9 +130,28 @@ def test_profiles_loading_free_folder_structure(profiles_with_free_folder_struct assert profiles[1].token == "b", "The profile name should be 'b'" assert profiles[2].token == "c", "The profile name should be 'c'" + +def test_versioned_profiles_loading(fake_versioned_profiles_path): + """Test the loaded profiles from the validator context.""" + profiles = Profile.load_profiles(profiles_path=fake_versioned_profiles_path) + logger.debug("The profiles: %r", profiles) + for p in profiles: + logger.warning("The profile '%s' has %d requirements", p, len(p.requirements)) + # The number of profiles should be 3 + assert len(profiles) == 3, "The number of profiles should be 3" + + # The profile a should have an explicit version 1.0.0 assert profiles[0].token == "a", "The profile name should be 'a'" + assert profiles[0].version == "1.0.0", "The profile version should be 1.0.0" + + # The profile b should have a version inferred by the 2.0 assert profiles[1].token == "b", "The profile name should be 'b'" + assert profiles[1].version == "2.0", "The profile version should be 2.0" + + # The profile c should have a version inferred by the 3.2.1 assert profiles[2].token == "c", "The profile name should be 'c'" + assert profiles[2].version == "3.2.1", "The profile version should be 3.2.1" + def test_loaded_valid_profile_with_inheritance_from_validator_context(fake_profiles_path: str): From c2d890219451b1d4226079793c9ebaa045e48737 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 28 Jun 2024 12:50:47 +0200 Subject: [PATCH 599/902] test(core): :white_check_mark: test conflicting versions --- tests/conftest.py | 6 ++++ .../conflicting_versions/3.2.3/profile.ttl | 31 +++++++++++++++++++ .../conflicting_versions/3.2.3/shape_c.ttl | 22 +++++++++++++ tests/unit/requirements/test_profiles.py | 10 +++++- 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/data/profiles/conflicting_versions/3.2.3/profile.ttl create mode 100644 tests/data/profiles/conflicting_versions/3.2.3/shape_c.ttl diff --git a/tests/conftest.py b/tests/conftest.py index a0b4b612..c9f28965 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,6 +47,12 @@ def profiles_with_free_folder_structure_path(): def fake_versioned_profiles_path(): return f"{TEST_DATA_PATH}/profiles/fake_versioned_profiles" + +@fixture +def fake_conflicting_versioned_profiles_path(): + return f"{TEST_DATA_PATH}/profiles/conflicting_versions" + + @fixture def graphs_path(): return f"{TEST_DATA_PATH}/graphs" diff --git a/tests/data/profiles/conflicting_versions/3.2.3/profile.ttl b/tests/data/profiles/conflicting_versions/3.2.3/profile.ttl new file mode 100644 index 00000000..25493919 --- /dev/null +++ b/tests/data/profiles/conflicting_versions/3.2.3/profile.ttl @@ -0,0 +1,31 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . +@prefix schema: . + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Profile D" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Comment for the Profile D."""@en ; + + # URI of the publisher of the profile D + dct:publisher ; + + # This profile is an extension of the profile A + prof:isProfileOf ; + + # Explicit version in conflict with the inferred version + schema:version "3.2.2" ; + + # Explicitly state that this profile is a transitive profile of the profile A + prof:isTransitiveProfileOf ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "c" ; +. diff --git a/tests/data/profiles/conflicting_versions/3.2.3/shape_c.ttl b/tests/data/profiles/conflicting_versions/3.2.3/shape_c.ttl new file mode 100644 index 00000000..cb9a0b59 --- /dev/null +++ b/tests/data/profiles/conflicting_versions/3.2.3/shape_c.ttl @@ -0,0 +1,22 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +ro:ShapeC + a sh:NodeShape ; + sh:name "The Shape C" ; + sh:description "This is the Shape C" ; + sh:targetNode ro:ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "Check Metadata File Descriptor entity existence" ; + sh:description "Check if the RO-Crate Metadata File Descriptor entity exists" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; + ] . diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 48b229ed..419943a6 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -4,7 +4,7 @@ import pytest from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER -from rocrate_validator.errors import DuplicateRequirementCheck, InvalidProfilePath +from rocrate_validator.errors import DuplicateRequirementCheck, InvalidProfilePath, ProfileSpecificationError from rocrate_validator.models import (Profile, ValidationContext, ValidationSettings, Validator) from tests.ro_crates import InvalidFileDescriptorEntity @@ -153,6 +153,14 @@ def test_versioned_profiles_loading(fake_versioned_profiles_path): assert profiles[2].version == "3.2.1", "The profile version should be 3.2.1" +def test_conflicting_versioned_profiles_loading(fake_conflicting_versioned_profiles_path): + """Test the loaded profiles from the validator context.""" + with pytest.raises(ProfileSpecificationError) as excinfo: + # Load the profiles + Profile.load_profiles(profiles_path=fake_conflicting_versioned_profiles_path) + # Check that the conflicting versions are found + assert "Inconsistent versions found: {'3.2.2', '3.2.1', '2.3'}" + def test_loaded_valid_profile_with_inheritance_from_validator_context(fake_profiles_path: str): """Test the loaded profiles from the validator context.""" From f8b264b1c62775d3f8a4629e595fe8394ee6540d Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 1 Jul 2024 13:41:06 +0200 Subject: [PATCH 600/902] pytest.ini: remove duplicate entry --- pytest.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 52b7c92d..f023c908 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,4 +4,3 @@ # log_level=INFO addopts = -n auto ; filterwarnings = -addopts = -n auto From fbb4629f614142084e332e670451f4e780815d29 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 1 Jul 2024 14:48:37 +0200 Subject: [PATCH 601/902] fix: :loud_sound: fix log level of some messages --- rocrate_validator/models.py | 4 ++-- rocrate_validator/requirements/shacl/validator.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 2c57a127..e0926ecf 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1223,12 +1223,12 @@ def __load_profiles__(self) -> list[Profile]: if not profile: try: candidate_profiles = Profile.get_by_token(self.profile_identifier) - logger.error("Candidate profiles found by token: %s", profile) + 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) self.settings["profile_identifier"] = profile.identifier - logger.error("Profile with the highest version number: %s", profile) + logger.debug("Profile with the highest version number: %s", profile) # if the profile is found by token, set the profile name to the identifier self.settings["profile_identifier"] = profile.identifier except Exception as e: diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index a089d94c..19b013ca 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -52,7 +52,7 @@ def __enter__(self) -> SHACLValidationContext: logger.debug("Processing profile: %s (id: %s)", self._profile.name, self._profile.identifier) if self._context.settings.get("target_only_validation", False) and \ self._profile.identifier != self._context.settings.get("profile_identifier", None): - logger.error("Skipping validation of profile %s", self._profile.identifier) + logger.debug("Skipping validation of profile %s", self._profile.identifier) 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 9899e1686b9ac909c88702f91193f1af063f596f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 2 Jul 2024 10:34:38 +0200 Subject: [PATCH 602/902] feat(utils): :sparkles: add class to represent URI objects --- rocrate_validator/utils.py | 89 +++++++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index afec0d87..336751ea 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -1,11 +1,15 @@ +from __future__ import annotations + import inspect import os import re import sys from importlib import import_module from pathlib import Path -from typing import Optional +from typing import Optional, Union +from urllib.parse import ParseResult, parse_qsl, urlparse +import requests import toml from rdflib import Graph @@ -227,6 +231,89 @@ def to_camel_case(snake_str: str) -> str: return components[0].capitalize() + ''.join(x.title() for x in components[1:]) +class URI: + + REMOTE_SUPPORTED_SCHEMA = ('http', 'https', 'ftp') + + def __init__(self, uri: Union[str, Path]): + self._uri = uri = str(uri) + try: + # map local path to URI with file scheme + if not re.match(r'^\w+://', uri): + uri = f"file://{uri}" + # parse the value to extract the scheme + self._parse_result = urlparse(uri) + assert self.scheme in self.REMOTE_SUPPORTED_SCHEMA + ('file',), "Invalid URI scheme" + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.debug(e) + raise ValueError("Invalid URI: %s", uri) + + @property + def uri(self) -> str: + return self._uri + + @property + def parse_result(self) -> ParseResult: + return self._parse_result + + @property + def scheme(self) -> str: + return self._parse_result.scheme + + @property + def fragment(self) -> Optional[str]: + fragment = self._parse_result.fragment + return fragment if fragment else None + + def get_query_param(self, param: str) -> Optional[str]: + query_params = dict(parse_qsl(self._parse_result.query)) + return query_params.get(param) + + def as_path(self) -> Path: + if not self.is_local_resource(): + raise ValueError("URI is not a local resource") + return Path(self._uri) + + def is_remote_resource(self) -> bool: + return self.scheme in self.REMOTE_SUPPORTED_SCHEMA + + def is_local_resource(self) -> bool: + return not self.is_remote_resource() + + def is_local_directory(self) -> bool: + return self.is_local_resource() and self.as_path().is_dir() + + def is_local_file(self) -> bool: + return self.is_local_resource() and self.as_path().is_file() + + def is_available(self) -> bool: + """Check if the resource is available""" + if self.is_remote_resource(): + try: + response = requests.head(self._uri, allow_redirects=True) + return response.status_code in (200, 302) + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.debug(e) + return False + return Path(self._uri).exists() + + def __str__(self): + return self._uri + + def __repr__(self): + return f"URI(uri={self._uri})" + + def __eq__(self, other): + if isinstance(other, URI): + return self._uri == other.uri + return False + + def __hash__(self): + return hash(self._uri) + + class MapIndex: def __init__(self, name: str, unique: bool = False): From 5676794f79836125bb7511e766abd6346ffc548f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 2 Jul 2024 10:43:42 +0200 Subject: [PATCH 603/902] feat(services): :sparkles: extend validation service to support zip (local or remote) --- rocrate_validator/services.py | 81 +++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 2cf15396..e92b7b61 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -1,12 +1,17 @@ +import shutil +import tempfile +import zipfile from pathlib import Path from typing import Union +import requests + import rocrate_validator.log as logging from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER from .models import (Profile, Severity, ValidationResult, ValidationSettings, Validator) -from .utils import get_profiles_path +from .utils import URI, get_profiles_path # set the default profiles path DEFAULT_PROFILES_PATH = get_profiles_path() @@ -22,13 +27,73 @@ def validate(settings: Union[dict, ValidationSettings]) -> ValidationResult: # if settings is a dict, convert to ValidationSettings settings = ValidationSettings.parse(settings) - # create a validator - validator = Validator(settings) - logger.debug("Validator created. Starting validation...") - # validate the RO-Crate - result = validator.validate() - logger.debug("Validation completed: %s", result) - return result + # parse the rocrate path + rocrate_path: URI = URI(settings.data_path) + logger.debug("Validating RO-Crate: %s", rocrate_path) + + # check if the RO-Crate exists + if not rocrate_path.is_available(): + raise FileNotFoundError(f"RO-Crate not found: {rocrate_path}") + + def __do_validate__(settings: ValidationSettings): + # create a validator + validator = Validator(settings) + logger.debug("Validator created. Starting validation...") + # validate the RO-Crate + result = validator.validate() + logger.debug("Validation completed: %s", result) + return result + + def __extract_and_validate_rocrate__(rocrate_path: Path): + # store the original data path + original_data_path = settings.data_path + + with tempfile.TemporaryDirectory() as tmp_dir: + try: + # extract the RO-Crate to the temporary directory + 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) + # update the data path to point to the temporary directory + settings.data_path = Path(tmp_dir) + # continue with the validation process + return __do_validate__(settings) + finally: + # restore the original data path + settings.data_path = original_data_path + logger.debug("Original data path restored: %s", original_data_path) + + # check if the RO-Crate is a remote RO-Crate, + # i.e., if the RO-Crate is a URL. If so, download the RO-Crate + # and extract it to a temporary directory. We support either http or https + # or ftp protocols to download the remote RO-Crate. + if rocrate_path.scheme in ('http', 'https', 'ftp'): + logger.debug("RO-Crate is a remote RO-Crate") + # create a temp folder to store the downloaded RO-Crate + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + # download the remote RO-Crate + with requests.get(rocrate_path.uri, stream=True, allow_redirects=True) as r: + with open(tmp_file.name, 'wb') as f: + shutil.copyfileobj(r.raw, f) + logger.debug("RO-Crate downloaded to temporary file: %s", tmp_file.name) + # continue with the validation process by extracting the RO-Crate and validating it + return __extract_and_validate_rocrate__(Path(tmp_file.name)) + + # check if the RO-Crate is a ZIP file + elif rocrate_path.as_path().suffix == ".zip": + logger.debug("RO-Crate is a local ZIP file") + # continue with the validation process by extracting the RO-Crate and validating it + return __extract_and_validate_rocrate__(rocrate_path.as_path()) + + # if the RO-Crate is not a ZIP file, directly validate the RO-Crate + elif rocrate_path.is_local_directory(): + logger.debug("RO-Crate is a local directory") + settings.data_path = rocrate_path.as_path() + return __do_validate__(settings) + else: + raise ValueError( + f"Invalid RO-Crate URI: {rocrate_path}. " + "It MUST be a local directory or a ZIP file (local or remote).") def get_profiles(profiles_path: Path = DEFAULT_PROFILES_PATH, publicID: str = None, severity=Severity.OPTIONAL) -> list[Profile]: From b007981b1ab69d8a9554bd3931779b0c4ddb69ea Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 2 Jul 2024 10:47:55 +0200 Subject: [PATCH 604/902] feat(cli): :sparkles: update validate cmd to support zipped RO-Crates --- rocrate_validator/cli/commands/validate.py | 34 +++++++++++++++++----- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index ec48a258..6be60546 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -13,7 +13,7 @@ from ... import services from ...colors import get_severity_color from ...models import Severity, ValidationResult -from ...utils import get_profiles_path +from ...utils import URI, get_profiles_path from ..main import cli, click # from rich.markdown import Markdown @@ -26,8 +26,28 @@ logger = logging.getLogger(__name__) +def validate_uri(ctx, param, value): + """ + Validate if the value is a path or a URI + """ + 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) + + return value + + @cli.command("validate") -@click.argument("rocrate-path", type=click.Path(exists=True), default=".") +@click.argument("rocrate-uri", callback=validate_uri, default=".") @click.option( '-nff', '--no-fail-fast', @@ -89,7 +109,7 @@ def validate(ctx, disable_profile_inheritance: bool = False, requirement_severity: str = Severity.REQUIRED.name, requirement_severity_only: bool = False, - rocrate_path: Path = Path("."), + rocrate_uri: Path = ".", no_fail_fast: bool = False, ontologies_path: Optional[Path] = None): """ @@ -103,14 +123,14 @@ def validate(ctx, logger.debug("requirement_severity_only: %s", requirement_severity_only) logger.debug("disable_inheritance: %s", disable_profile_inheritance) - logger.debug("rocrate_path: %s", os.path.abspath(rocrate_path)) + logger.debug("rocrate_uri: %s", rocrate_uri) logger.debug("no_fail_fast: %s", no_fail_fast) logger.debug("fail fast: %s", not no_fail_fast) if ontologies_path: logger.debug("ontologies_path: %s", os.path.abspath(ontologies_path)) - if rocrate_path: - logger.debug("rocrate_path: %s", os.path.abspath(rocrate_path)) + if rocrate_uri: + logger.debug("rocrate_path: %s", os.path.abspath(rocrate_uri)) # Validate the RO-Crate result: ValidationResult = services.validate( @@ -120,7 +140,7 @@ def validate(ctx, "requirement_severity": requirement_severity, "requirement_severity_only": requirement_severity_only, "inherit_profiles": not disable_profile_inheritance, - "data_path": Path(rocrate_path).absolute(), + "data_path": rocrate_uri, "ontology_path": Path(ontologies_path).absolute() if ontologies_path else None, "abort_on_first": not no_fail_fast } From 34f01b45a90fdafc4653ef6c7974d7ed0b0a57f7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 2 Jul 2024 10:49:27 +0200 Subject: [PATCH 605/902] test(cli): :white_check_mark: test local and remote zipped RO-Crates --- tests/ro_crates.py | 9 +++++++++ tests/test_cli.py | 26 ++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 074923fd..70f1de5b 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -32,6 +32,15 @@ def workflow_roc(self) -> Path: def workflow_roc_string_license(self) -> Path: return VALID_CRATES_DATA_PATH / "workflow-roc-string-license" + @property + def sort_and_change_remote(self) -> Path: + # TODO: replace with a stable remote URL; this one might be deleted + return "https://dev.workflowhub.eu/workflows/161/ro_crate?version=1" + + @property + def sort_and_change_archive(self) -> Path: + return VALID_CRATES_DATA_PATH / "sortchangecase.crate.zip" + class InvalidFileDescriptor: diff --git a/tests/test_cli.py b/tests/test_cli.py index a01b4156..ee896b6e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,10 +4,14 @@ from click.testing import CliRunner from pytest import fixture +from rocrate_validator import log as logging from rocrate_validator.cli.main import cli from rocrate_validator.utils import get_version from tests.ro_crates import InvalidFileDescriptor, ValidROC +# set up logging +logger = logging.getLogger(__name__) + @fixture def cli_runner() -> CliRunner: @@ -20,12 +24,26 @@ def test_version(cli_runner: CliRunner): assert get_version() in result.output -def test_validate_subcmd_valid_rocrate(cli_runner: CliRunner): +def test_validate_subcmd_invalid_rocrate1(cli_runner: CliRunner): + result = cli_runner.invoke(cli, ['validate', str(InvalidFileDescriptor().invalid_json_format)]) + assert result.exit_code == 1 + + +def test_validate_subcmd_valid_local_folder_rocrate(cli_runner: CliRunner): result = cli_runner.invoke(cli, ['validate', str(ValidROC().wrroc_paper_long_date)]) assert result.exit_code == 0 assert re.search(r'RO-Crate.*is valid', result.output) -def test_validate_subcmd_invalid_rocrate1(cli_runner: CliRunner): - result = cli_runner.invoke(cli, ['validate', str(InvalidFileDescriptor().invalid_json_format)]) - assert result.exit_code == 1 +def test_validate_subcmd_valid_remote_rocrate(cli_runner: CliRunner): + result = cli_runner.invoke( + cli, ['validate', str(ValidROC().sort_and_change_remote)]) + assert result.exit_code == 0 + logger.error(result.output) + assert re.search(r'RO-Crate.*is valid', result.output) + + +def test_validate_subcmd_invalid_local_archive_rocrate(cli_runner: CliRunner): + result = cli_runner.invoke(cli, ['validate', str(ValidROC().sort_and_change_archive)]) + assert result.exit_code == 0 + assert re.search(r'RO-Crate.*is valid', result.output) From 004da7f040b5ac85da62a791107f9064517fe853 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 3 Jul 2024 14:19:25 +0200 Subject: [PATCH 606/902] test(profiles): :white_check_mark: use a valid `data_path` to test profiles --- tests/unit/requirements/test_profiles.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 419943a6..746f78b8 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -4,10 +4,12 @@ import pytest from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER -from rocrate_validator.errors import DuplicateRequirementCheck, InvalidProfilePath, ProfileSpecificationError +from rocrate_validator.errors import (DuplicateRequirementCheck, + InvalidProfilePath, + ProfileSpecificationError) from rocrate_validator.models import (Profile, ValidationContext, ValidationSettings, Validator) -from tests.ro_crates import InvalidFileDescriptorEntity +from tests.ro_crates import InvalidFileDescriptorEntity, ValidROC # set up logging logger = logging.getLogger(__name__) @@ -40,7 +42,7 @@ def test_load_invalid_profile_from_validation_context(fake_profiles_path: str): settings = { "profiles_path": "/tmp/random_path_xxx", "profile_identifier": DEFAULT_PROFILE_IDENTIFIER, - "data_path": "/tmp/random_path", + "data_path": ValidROC().wrroc_paper, "inherit_profiles": False } @@ -62,7 +64,7 @@ def test_load_valid_profile_without_inheritance_from_validation_context(fake_pro settings = { "profiles_path": fake_profiles_path, "profile_identifier": "c", - "data_path": "/tmp/random_path", + "data_path": ValidROC().wrroc_paper, "inherit_profiles": False } @@ -86,7 +88,7 @@ def test_profile_spec_properties(fake_profiles_path: str): settings = { "profiles_path": fake_profiles_path, "profile_identifier": "c", - "data_path": "/tmp/random_path", + "data_path": ValidROC().wrroc_paper, "inherit_profiles": True, "disable_check_for_duplicates": True, } @@ -169,7 +171,7 @@ def __perform_test__(profile_identifier: str, expected_inherited_profiles: list[ settings = { "profiles_path": fake_profiles_path, "profile_identifier": profile_identifier, - "data_path": "/tmp/random_path", + "data_path": ValidROC().wrroc_paper, "disable_check_for_duplicates": True, } @@ -208,7 +210,7 @@ def test_load_invalid_profile_no_override_enabled(fake_profiles_path: str): settings = { "profiles_path": fake_profiles_path, "profile_identifier": "invalid-duplicated-shapes", - "data_path": "/tmp/random_path", + "data_path": ValidROC().wrroc_paper, "inherit_profiles": True, "allow_shapes_override": False, } @@ -232,7 +234,7 @@ def test_load_invalid_profile_with_override_on_same_profile(fake_profiles_path: settings = { "profiles_path": fake_profiles_path, "profile_identifier": "invalid-duplicated-shapes", - "data_path": "/tmp/random_path", + "data_path": ValidROC().wrroc_paper, "inherit_profiles": True, "allow_shapes_override": False } @@ -255,7 +257,7 @@ def test_load_valid_profile_with_override_on_inherited_profile(fake_profiles_pat settings = { "profiles_path": fake_profiles_path, "profile_identifier": "c-overridden", - "data_path": "/tmp/random_path", + "data_path": ValidROC().wrroc_paper, "inherit_profiles": True, "allow_shapes_override": True } From 5ed09c0e47a9d54642706986a7c49647a7358d4a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 4 Jul 2024 19:51:12 +0200 Subject: [PATCH 607/902] feat(utils): :sparkles: more methods to inspect URI --- rocrate_validator/utils.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 336751ea..4e8254e3 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -253,6 +253,10 @@ def __init__(self, uri: Union[str, Path]): def uri(self) -> str: return self._uri + @property + def base_uri(self) -> str: + return f"{self.scheme}://{self._parse_result.netloc}{self._parse_result.path}" + @property def parse_result(self) -> ParseResult: return self._parse_result @@ -266,6 +270,18 @@ def fragment(self) -> Optional[str]: fragment = self._parse_result.fragment return fragment if fragment else None + def get_scheme(self) -> str: + return self._parse_result.scheme + + def get_netloc(self) -> str: + return self._parse_result.netloc + + def get_path(self) -> str: + return self._parse_result.path + + def get_query_string(self) -> str: + return self._parse_result.query + def get_query_param(self, param: str) -> Optional[str]: query_params = dict(parse_qsl(self._parse_result.query)) return query_params.get(param) From 339fce93674afdfacca9d3e03ba18d2d6987a945 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 4 Jul 2024 20:00:10 +0200 Subject: [PATCH 608/902] feat(core): :sparkles: add module to handle ROCrate archives and metadata --- rocrate_validator/rocrate.py | 500 +++++++++++++++++++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 rocrate_validator/rocrate.py diff --git a/rocrate_validator/rocrate.py b/rocrate_validator/rocrate.py new file mode 100644 index 00000000..db5888f5 --- /dev/null +++ b/rocrate_validator/rocrate.py @@ -0,0 +1,500 @@ +from __future__ import annotations + +import io +import json +import struct +import zipfile +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Union + +import requests +from rdflib import Graph + +from rocrate_validator import log as logging +from rocrate_validator.errors import ROCrateInvalidURIError +from rocrate_validator.utils import URI + +# set up logging +logger = logging.getLogger(__name__) + + +class ROCrateEntity: + + def __init__(self, metadata: ROCrateMetadata, raw_data: object) -> None: + self._raw_data = raw_data + self._metadata = metadata + + @property + def id(self) -> str: + return self._raw_data.get('@id') + + @property + def type(self) -> Union[str, list[str]]: + return self._raw_data.get('@type') + + @property + def name(self) -> str: + return self._raw_data.get('name') + + @property + def metadata(self) -> ROCrateMetadata: + return self._metadata + + @property + def ro_crate(self) -> ROCrate: + return self.metadata.ro_crate + + def has_type(self, entity_type: str) -> bool: + assert isinstance(entity_type, str), "Entity type must be a string" + e_types = self.type if isinstance(self.type, list) else [self.type] + return entity_type in e_types + + def has_types(self, entity_types: list[str]) -> bool: + assert isinstance(entity_types, list), "Entity types must be a list" + e_types = self.type if isinstance(self.type, list) else [self.type] + return any([t in e_types for t in entity_types]) + + def get_property(self, name: str, default=None) -> Union[str, ROCrateEntity]: + data = self._raw_data.get(name, default) + if isinstance(data, dict) and '@id' in data: + return self.metadata.get_entity(data['@id']) + return data + + @property + def raw_data(self) -> object: + return self._raw_data + + def is_available(self) -> bool: + try: + # check if the entity points to an external file + if self.id.startswith("http"): + return ROCrate.get_external_file_size(self.id) > 0 + + # check if the entity is part of the local RO-Crate + if self.ro_crate.uri.is_local_resource(): + # check if the file exists in the local file system + if isinstance(self.ro_crate, ROCrateLocalFolder): + logger.debug("Checking local folder: %s", self.ro_crate.uri.as_path().absolute() / self.id) + return self.ro_crate.has_file( + self.ro_crate.uri.as_path().absolute() / self.id) \ + or self.ro_crate.has_directory( + self.ro_crate.uri.as_path().absolute() / self.id) + # check if the file exists in the local zip file + if isinstance(self.ro_crate, ROCrateLocalZip): + if self.id in [str(_) for _ in self.ro_crate.list_files()]: + return self.ro_crate.get_file_size(Path(self.id)) > 0 + + # check if the entity is part of the remote RO-Crate + if self.ro_crate.uri.is_remote_resource(): + return self.ro_crate.get_file_size(Path(self.id)) > 0 + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return False + + raise ROCrateInvalidURIError(uri=self.id, message="Could not determine the availability of the entity") + + def get_size(self) -> int: + try: + return self.metadata.ro_crate.get_file_size(Path(self.id)) + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return 0 + + def __str__(self) -> str: + return f"Entity({self.id})" + + def __repr__(self) -> str: + return str(self) + + def __eq__(self, other: ROCrateEntity) -> bool: + if not isinstance(other, ROCrateEntity): + return False + return self.id == other.id + + +class ROCrateMetadata: + + METADATA_FILE_DESCRIPTOR = 'ro-crate-metadata.json' + + def __init__(self, ro_crate: ROCrate) -> None: + self._ro_crate = ro_crate + self._dict = None + self._json: str = None + + @property + def ro_crate(self) -> ROCrate: + return self._ro_crate + + @property + def size(self) -> int: + try: + return len(self.as_json()) + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return 0 + + def get_file_descriptor_entity(self) -> ROCrateEntity: + metadata_file_descriptor = self.get_entity(self.METADATA_FILE_DESCRIPTOR) + if not metadata_file_descriptor: + raise ValueError("no metadata file descriptor in crate") + return metadata_file_descriptor + + def get_root_data_entity(self) -> ROCrateEntity: + metadata_file_descriptor = self.get_file_descriptor_entity() + main_entity = metadata_file_descriptor.get_property('about') + if not main_entity: + raise ValueError("no main entity in metadata file descriptor") + return main_entity + + 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: + raise ValueError("no main workflow in metadata file descriptor") + return main_workflow + + def get_entity(self, entity_id: str) -> ROCrateEntity: + for entity in self.as_dict().get('@graph', []): + if entity.get('@id') == entity_id: + return ROCrateEntity(self, entity) + return None + + def get_entities(self) -> list[ROCrateEntity]: + entities = [] + for entity in self.as_dict().get('@graph', []): + entities.append(ROCrateEntity(self, entity)) + return entities + + def get_entities_by_type(self, entity_type: Union[str, list[str]]) -> list[ROCrateEntity]: + entities = [] + for e in self.get_entities(): + if e.has_type(entity_type): + entities.append(e) + return entities + + def get_dataset_entities(self) -> list[ROCrateEntity]: + return self.get_entities_by_type('Dataset') + + def get_file_entities(self) -> list[ROCrateEntity]: + return self.get_entities_by_type('File') + + def get_web_data_entities(self) -> list[ROCrateEntity]: + entities = [] + for entity in self.get_entities(): + if entity.has_type('File') or entity.has_type('Dataset'): + if entity.id.startswith("http"): + entities.append(entity) + return entities + + def as_json(self) -> str: + if not self._json: + self._json = self.ro_crate.get_file_content( + Path(self.METADATA_FILE_DESCRIPTOR), binary_mode=False) + return self._json + + def as_dict(self) -> dict: + if not self._dict: + # if the dictionary is not cached, load it + self._dict = json.loads(self.as_json()) + return self._dict + + def as_graph(self, publicID: str = None) -> Graph: + if not self._graph: + # if the graph is not cached, load it + self._graph = Graph(base=publicID or self.ro_crate.uri) + self._graph.parse(data=self.as_json, format='json-ld') + return self._graph + + def __str__(self) -> str: + return f"Metadata({self.ro_crate})" + + def __repr__(self) -> str: + return str(self) + + def __eq__(self, other: ROCrateMetadata) -> bool: + if not isinstance(other, ROCrateMetadata): + return False + return self.ro_crate == other.ro_crate + + +class ROCrate(ABC): + + def __init__(self, path: Union[str, Path, URI]): + + # store the path to the crate + self._uri = URI(path) + + # cache the list of files + self._files = None + + # initialize variables to cache the data + self._dict: dict = None + self._graph = None + + self._metadata = None + + @property + def uri(self) -> URI: + return self._uri + + @property + def metadata(self) -> ROCrateMetadata: + if not self._metadata: + self._metadata = ROCrateMetadata(self) + return self._metadata + + @abstractmethod + def size(self) -> int: + pass + + @property + @abstractmethod + def list_files(self) -> list[Path]: + pass + + def __parse_path__(self, path: Path) -> Path: + assert path, "Path cannot be None" + # if the path is absolute, return it + if path.is_absolute(): + return path + try: + # if the path is relative, try to resolve it + return self.uri.as_path().absolute() / path.relative_to(self.uri.as_path()) + except ValueError: + # if the path cannot be resolved, return the absolute path + return self.uri.as_path().absolute() / path + + def has_descriptor(self) -> bool: + return (self.uri.as_path().absolute() / self.metadata.METADATA_FILE_DESCRIPTOR).is_file() + + def has_file(self, path: Path) -> bool: + try: + return self.__parse_path__(path).is_file() + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return False + + def has_directory(self, path: Path) -> bool: + try: + return self.__parse_path__(path).is_dir() + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return False + + @abstractmethod + def get_file_size(self, path: Path) -> int: + pass + + @abstractmethod + def get_file_content(self, path: Path, binary_mode: bool = True) -> Union[str, bytes]: + pass + + @staticmethod + def get_external_file_content(uri: str, binary_mode: bool = True) -> Union[str, bytes]: + response = requests.get(str(uri)) + response.raise_for_status() + return response.content if binary_mode else response.text + + @staticmethod + def get_external_file_size(uri: str) -> int: + response = requests.head(str(uri)) + response.raise_for_status() + return int(response.headers.get('Content-Length')) + + @staticmethod + def new_instance(uri: Union[str, Path, URI]) -> 'ROCrate': + # check if the URI is valid + if not uri: + raise ValueError("Invalid URI") + if not isinstance(uri, URI): + uri = URI(uri) + # check if the URI is a local directory + if uri.is_local_directory(): + return ROCrateLocalFolder(uri) + # check if the URI is a local zip file + if uri.is_local_file(): + return ROCrateLocalZip(uri) + # check if the URI is a remote zip file + if uri.is_remote_resource(): + return ROCrateRemoteZip(uri) + # if the URI is not supported, raise an error + raise ROCrateInvalidURIError(uri=uri, message="Unsupported URI") + + +class ROCrateLocalFolder(ROCrate): + + def __init__(self, path: Union[str, Path, URI]): + super().__init__(path) + + # cache the list of files + self._files = None + + # check if the path is a directory + if not self.has_directory(self.uri.as_path()): + raise ROCrateInvalidURIError(uri=path) + + @property + def size(self) -> int: + return sum(f.stat().st_size for f in self.list_files() if f.is_file()) + + def list_files(self) -> list[Path]: + if not self._files: + self._files = [] + base_path = self.uri.as_path() + for file in base_path.rglob('*'): + if file.is_file(): + self._files.append(base_path / file) + return self._files + + def get_file_size(self, path: Path) -> int: + path = self.__parse_path__(path) + if not self.has_file(path): + raise FileNotFoundError(f"File not found: {path}") + return path.stat().st_size + + def get_file_content(self, path: Path, binary_mode: bool = True) -> Union[str, bytes]: + path = self.__parse_path__(path) + if not self.has_file(path): + raise FileNotFoundError(f"File not found: {path}") + return path.read_bytes() if binary_mode else path.read_text() + + +class ROCrateLocalZip(ROCrate): + + def __init__(self, path: Union[str, Path, URI], init_zip: bool = True): + super().__init__(path) + + # initialize the zip reference + self._zipref = None + if init_zip: + self.__init_zip_reference__() + + # cache the list of files + self._files = None + + def __del__(self): + if self._zipref and self._zipref.fp is not None: + self._zipref.close() + del self._zipref + + @property + def size(self) -> int: + return self.uri.as_path().stat().st_size + + def __init_zip_reference__(self): + path = self.uri.as_path() + # check if the path is a file + if not self.uri.as_path().is_file(): + raise ROCrateInvalidURIError(uri=path) + # check if the file is a zip file + if not self.uri.as_path().suffix == '.zip': + raise ROCrateInvalidURIError(uri=path) + self._zipref = zipfile.ZipFile(path) + logger.debug("Initialized zip reference: %s", self._zipref) + + def __get_file_info__(self, path: Path) -> zipfile.ZipInfo: + return self._zipref.getinfo(str(path)) + + def has_descriptor(self) -> bool: + return ROCrateMetadata.METADATA_FILE_DESCRIPTOR in [str(_.name) for _ in self.list_files()] + + def has_file(self, path: Path) -> bool: + if path in self.list_files(): + info = self.__get_file_info__(path) + return not info.is_dir() + return False + + def has_directory(self, path: Path) -> bool: + if path in self.list_files(): + info = self.__get_file_info__(path) + return info.is_dir() + return False + + def list_files(self) -> list[Path]: + if not self._files: + self._files = [] + for file in self._zipref.namelist(): + self._files.append(Path(file)) + return self._files + + def get_file_size(self, path: Path) -> int: + return self._zipref.getinfo(str(path)).file_size + + def get_file_content(self, path: Path, binary_mode: bool = True) -> Union[str, bytes]: + if not self.has_file(path): + raise FileNotFoundError(f"File not found: {path}") + data = self._zipref.read(str(path)) + return data if binary_mode else data.decode('utf-8') + + +class ROCrateRemoteZip(ROCrateLocalZip): + + def __init__(self, path: Union[str, Path, URI]): + super().__init__(path, init_zip=False) + + logger.debug("Size: %s", self.size) + + # # initialize the zip reference + self.__init_zip_reference__() + + def __init_zip_reference__(self): + url = str(self.uri) + + # check if the URI is available + 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)) + + @property + def size(self) -> int: + response = requests.head(str(self.uri)) + 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) + else: + raise Exception("Could not determine the file size from the headers") + + @staticmethod + def __fetch_range__(uri: str, start, end): + headers = {'Range': f'bytes={start}-{end}'} + response = requests.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 Exception("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 From 9e293df277c17c6a8a86d2955ea6edc2a955f7ff Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 4 Jul 2024 20:02:02 +0200 Subject: [PATCH 609/902] feat(core): :sparkles: add more specific error classes --- rocrate_validator/errors.py | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 4ae735f5..02293c9b 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -219,3 +219,60 @@ def check(self): def __repr__(self): return f"CheckValidationError({self._check!r}, {self._message!r}, {self._path!r})" + + +class ROCrateInvalidURIError(ROCValidatorError): + """Raised when an invalid URI is provided.""" + + def __init__(self, uri: Optional[str] = None, message: Optional[str] = None): + self._uri = uri + self._message = message + + @property + def uri(self) -> Optional[str]: + """The invalid URI.""" + return self._uri + + @property + def message(self) -> Optional[str]: + """The error message.""" + 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}\"" + + def __repr__(self): + return f"ROCrateInvalidURIError({self._uri!r})" + + +class ROCrateMetadataNotFoundError(ROCValidatorError): + """Raised when the RO-Crate metadata is not found.""" + + def __init__(self, message: Optional[str] = None, path: Optional[str] = None): + self._message = message + self._path = path + + @property + def message(self) -> Optional[str]: + """The error message.""" + return self._message + + @property + def path(self) -> Optional[str]: + """The path where the error occurred.""" + return self._path + + def __str__(self) -> str: + if self._path: + if self._message: + return f"RO-Crate metadata not found on '{self._path!r}': {self._message!r}" + else: + return f"RO-Crate metadata not found on '{self._path!r}'" + else: + return "RO-Crate metadata not found" + + def __repr__(self): + return f"ROCrateMetadataNotFoundError({self._path!r},{self._message!r})" From 2e36f043c202fbdfd4011a27f544862d9ad8a004 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 4 Jul 2024 20:06:43 +0200 Subject: [PATCH 610/902] feat(core): :sparkles: init ROCrate object on validation context --- rocrate_validator/models.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 2c57a127..7a961d01 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1115,6 +1115,18 @@ def __init__(self, validator: Validator, settings: dict[str, object]): # additional properties for the context self._properties = {} + # parse the rocrate path + rocrate_path: URI = URI(settings.get("data_path")) + logger.debug("Validating RO-Crate: %s", rocrate_path) + + # initialize the ROCrate object + self._rocrate = ROCrate.new_instance(rocrate_path) + assert isinstance(self._rocrate, ROCrate), "Invalid RO-Crate instance" + + @property + def ro_crate(self) -> ROCrate: + return self._rocrate + @property def validator(self) -> Validator: return self._validator From d315bcb789433fb6e3a1b43e3f267066ffdcea53 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 4 Jul 2024 20:07:50 +0200 Subject: [PATCH 611/902] refactor(core): :recycle: update `publicID` URI --- rocrate_validator/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 7a961d01..fb6b447c 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1143,9 +1143,9 @@ def settings(self) -> dict[str, object]: @property def publicID(self) -> str: - path = str(self.rocrate_path) + path = str(self.ro_crate.uri.base_uri) if not path.endswith("/"): - return f"{path}/" + path = f"{path}/" return path @property From adca56758d0609d72fa68c1aa04ad1392691f233 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 4 Jul 2024 20:11:38 +0200 Subject: [PATCH 612/902] refactor(core): :wastebasket: remove obsolete descriptor_path property --- rocrate_validator/models.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index fb6b447c..a64922f1 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1167,10 +1167,6 @@ def requirement_severity_only(self) -> bool: def rocrate_path(self) -> Path: return self.settings.get("data_path") - @property - def file_descriptor_path(self) -> Path: - return self.rocrate_path / ROCRATE_METADATA_FILE - @property def fail_fast(self) -> bool: return self.settings.get("abort_on_first", True) @@ -1181,8 +1177,8 @@ def rel_fd_path(self) -> Path: def __load_data_graph__(self): data_graph = Graph() - logger.debug("Loading RO-Crate metadata: %s", self.file_descriptor_path) - _ = data_graph.parse(self.file_descriptor_path, + logger.debug("Loading RO-Crate metadata of: %s", self.ro_crate.uri) + _ = data_graph.parse(data=self.ro_crate.metadata.as_dict(), format="json-ld", publicID=self.publicID) logger.debug("RO-Crate metadata loaded: %s", data_graph) return data_graph From faf08666fb9173d1043544b5962c9fb56f166284 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 4 Jul 2024 20:23:04 +0200 Subject: [PATCH 613/902] fix(core): :recycle: catch errors when loading the data graph --- rocrate_validator/models.py | 16 +++++++++++----- rocrate_validator/requirements/shacl/checks.py | 6 +++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index a64922f1..99282958 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -26,8 +26,10 @@ from rocrate_validator.errors import (DuplicateRequirementCheck, InvalidProfilePath, ProfileNotFound, ProfileSpecificationError, - ProfileSpecificationNotFound) -from rocrate_validator.utils import (MapIndex, MultiIndexMap, + ProfileSpecificationNotFound, + ROCrateMetadataNotFoundError) +from rocrate_validator.rocrate import ROCrate +from rocrate_validator.utils import (URI, MapIndex, MultiIndexMap, get_profiles_path, get_requirement_name_from_file) @@ -1185,9 +1187,13 @@ def __load_data_graph__(self): def get_data_graph(self, refresh: bool = False): # load the data graph - if not self._data_graph or refresh: - self._data_graph = self.__load_data_graph__() - return self._data_graph + try: + if not self._data_graph or refresh: + self._data_graph = self.__load_data_graph__() + return self._data_graph + except FileNotFoundError as e: + logger.debug("Error loading data graph: %s", e) + raise ROCrateMetadataNotFoundError(self.rocrate_path) @property def data_graph(self) -> Graph: diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 3c5dab8c..ef1341e2 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -3,6 +3,7 @@ from typing import Optional import rocrate_validator.log as logging +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import (Requirement, RequirementCheck, ValidationContext) from rocrate_validator.requirements.shacl.models import Shape @@ -57,6 +58,9 @@ def execute_check(self, context: ValidationContext): # the validation is postponed to the subsequent profiles # ย so we return True to continue the validation return True + except ROCrateMetadataNotFoundError as e: + logger.debug("Unable to perform metadata validation due to missing metadata file: %s", e) + return False def __do_execute_check__(self, shacl_context: SHACLValidationContext): # get the shapes registry @@ -75,7 +79,7 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): end_time = timer() logger.debug(f"Execution time for getting data graph: {end_time - start_time} seconds") except json.decoder.JSONDecodeError as e: - logger.warning("Unable to perform metadata validation due to an error in the JSON-LD data file: %s", e) + logger.debug("Unable to perform metadata validation due to an error in the JSON-LD data file: %s", e) return False # Begin the timer From ae6e46eca1d29b10a08ca3d2ff3e301a5df0b2d5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 4 Jul 2024 20:25:07 +0200 Subject: [PATCH 614/902] fix(shacl): :recycle: update function to make URIs relative --- rocrate_validator/requirements/shacl/checks.py | 2 +- rocrate_validator/requirements/shacl/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index ef1341e2..360bd51d 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -128,7 +128,7 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): severity=violation.get_result_severity(), resultPath=violation.resultPath.toPython() if violation.resultPath else None, focusNode=make_uris_relative( - violation.focusNode.toPython(), shacl_context.rocrate_path), + violation.focusNode.toPython(), shacl_context.publicID), value=violation.value) logger.debug("Added validation issue to the context: %s", c) if shacl_context.base_context.fail_fast: diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index 3bd401f0..fee6e69f 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -53,7 +53,7 @@ def map_severity(shacl_severity: str) -> Severity: def make_uris_relative(text: str, ro_crate_path: Union[Path, str]) -> str: # globally replace the string "file://" with "./ - return text.replace(f'file://{ro_crate_path}', '.') + return text.replace(str(ro_crate_path), './') def inject_attributes(obj: object, node_graph: Graph, node: Node) -> object: From a736f7d4371dc3e875ab07253afbab3062963d4f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 4 Jul 2024 20:51:45 +0200 Subject: [PATCH 615/902] feat(core): :sparkles: set remote validation to default --- rocrate_validator/models.py | 1 + rocrate_validator/services.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 99282958..a3036ef9 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1003,6 +1003,7 @@ class ValidationSettings: meta_shacl: bool = False iterate_rules: bool = True target_only_validation: bool = True + remote_validation: bool = True # Requirement severity settings requirement_severity: Union[str, Severity] = Severity.REQUIRED requirement_severity_only: bool = False diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index e92b7b61..680d88fc 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -35,6 +35,19 @@ def validate(settings: Union[dict, ValidationSettings]) -> ValidationResult: if not rocrate_path.is_available(): raise FileNotFoundError(f"RO-Crate not found: {rocrate_path}") + + # check if remote validation is enabled + remote_validation = settings.remote_validation + logger.debug("Remote validation: %s", remote_validation) + if remote_validation: + # create a validator + validator = Validator(settings) + logger.debug("Validator created. Starting validation...") + # validate the RO-Crate + result = validator.validate() + logger.debug("Validation completed: %s", result) + return result + def __do_validate__(settings: ValidationSettings): # create a validator validator = Validator(settings) From 0b2fd2c7b0f22d1d6855ec2c7c9a0834c109c073 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 4 Jul 2024 20:55:15 +0200 Subject: [PATCH 616/902] feat(utils): :zap: enable http cache by default --- poetry.lock | 94 +++++++++++++++++++++++++++++++++-- pyproject.toml | 1 + rocrate_validator/models.py | 1 + rocrate_validator/services.py | 11 ++++ 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index c4b54665..3bca5185 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "appnope" @@ -43,6 +43,25 @@ six = ">=1.12.0" astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + [[package]] name = "backcall" version = "0.2.0" @@ -54,6 +73,31 @@ files = [ {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, ] +[[package]] +name = "cattrs" +version = "23.2.3" +description = "Composable complex class support for attrs and dataclasses." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cattrs-23.2.3-py3-none-any.whl", hash = "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108"}, + {file = "cattrs-23.2.3.tar.gz", hash = "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"}, +] + +[package.dependencies] +attrs = ">=23.1.0" +exceptiongroup = {version = ">=1.1.1", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.1.0,<4.6.3 || >4.6.3", markers = "python_version < \"3.11\""} + +[package.extras] +bson = ["pymongo (>=4.4.0)"] +cbor2 = ["cbor2 (>=5.4.6)"] +msgpack = ["msgpack (>=1.0.5)"] +orjson = ["orjson (>=3.9.2)"] +pyyaml = ["pyyaml (>=6.0)"] +tomlkit = ["tomlkit (>=0.11.8)"] +ujson = ["ujson (>=5.7.0)"] + [[package]] name = "certifi" version = "2024.6.2" @@ -1008,8 +1052,8 @@ astroid = ">=3.2.2,<=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" @@ -1319,6 +1363,36 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-cache" +version = "1.2.1" +description = "A persistent cache for python requests" +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests_cache-1.2.1-py3-none-any.whl", hash = "sha256:1285151cddf5331067baa82598afe2d47c7495a1334bfe7a7d329b43e9fd3603"}, + {file = "requests_cache-1.2.1.tar.gz", hash = "sha256:68abc986fdc5b8d0911318fbb5f7c80eebcd4d01bfacc6685ecf8876052511d1"}, +] + +[package.dependencies] +attrs = ">=21.2" +cattrs = ">=22.2" +platformdirs = ">=2.5" +requests = ">=2.22" +url-normalize = ">=1.4" +urllib3 = ">=1.25.5" + +[package.extras] +all = ["boto3 (>=1.15)", "botocore (>=1.18)", "itsdangerous (>=2.0)", "pymongo (>=3)", "pyyaml (>=6.0.1)", "redis (>=3)", "ujson (>=5.4)"] +bson = ["bson (>=0.5)"] +docs = ["furo (>=2023.3,<2024.0)", "linkify-it-py (>=2.0,<3.0)", "myst-parser (>=1.0,<2.0)", "sphinx (>=5.0.2,<6.0.0)", "sphinx-autodoc-typehints (>=1.19)", "sphinx-automodapi (>=0.14)", "sphinx-copybutton (>=0.5)", "sphinx-design (>=0.2)", "sphinx-notfound-page (>=0.8)", "sphinxcontrib-apidoc (>=0.3)", "sphinxext-opengraph (>=0.9)"] +dynamodb = ["boto3 (>=1.15)", "botocore (>=1.18)"] +json = ["ujson (>=5.4)"] +mongodb = ["pymongo (>=3)"] +redis = ["redis (>=3)"] +security = ["itsdangerous (>=2.0)"] +yaml = ["pyyaml (>=6.0.1)"] + [[package]] name = "rich" version = "13.7.1" @@ -1467,6 +1541,20 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "url-normalize" +version = "1.4.3" +description = "URL normalization for Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "url-normalize-1.4.3.tar.gz", hash = "sha256:d23d3a070ac52a67b83a1c59a0e68f8608d1cd538783b401bc9de2c0fac999b2"}, + {file = "url_normalize-1.4.3-py2.py3-none-any.whl", hash = "sha256:ec3c301f04e5bb676d333a7fa162fa977ad2ca04b7e652bfc9fac4e405728eed"}, +] + +[package.dependencies] +six = "*" + [[package]] name = "urllib3" version = "2.2.2" @@ -1524,4 +1612,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "1b88cdfd9d1197574df9aa07fece4cab40064dedce4d1830be6c0d0cb96d5506" +content-hash = "9ae41d39fca6e10850ca8ee582f1defb89d3281648e6e47740e0a0f841306235" diff --git a/pyproject.toml b/pyproject.toml index 644d6099..472b3347 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ toml = "^0.10.2" rich-click = "^1.7.3" colorlog = "^6.8" requests = "^2.32.3" +requests-cache = "^1.2.1" [tool.poetry.group.dev.dependencies] pyproject-flake8 = "^6.1.0" diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index a3036ef9..9d5a02f7 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1004,6 +1004,7 @@ class ValidationSettings: iterate_rules: bool = True target_only_validation: bool = True remote_validation: bool = True + http_cache_timeout: int = 60 # Requirement severity settings requirement_severity: Union[str, Severity] = Severity.REQUIRED requirement_severity_only: bool = False diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 680d88fc..65027ae0 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -5,6 +5,7 @@ from typing import Union import requests +import requests_cache import rocrate_validator.log as logging from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER @@ -35,6 +36,16 @@ def validate(settings: Union[dict, ValidationSettings]) -> ValidationResult: if not rocrate_path.is_available(): raise FileNotFoundError(f"RO-Crate not found: {rocrate_path}") + # check if the requests cache is enabled + if settings.http_cache_timeout > 0: + # Set up requests cache + requests_cache.install_cache( + '/tmp/rocrate_validator_cache', + expire_after=settings.http_cache_timeout, # Cache expiration time in seconds + backend='sqlite', # Use SQLite backend + allowable_methods=('GET',), # Cache GET + allowable_codes=(200, 302, 404) # Cache responses with these status codes + ) # check if remote validation is enabled remote_validation = settings.remote_validation From 20670eec97c37480652426aeb37db3dc70c9de30 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 4 Jul 2024 20:55:59 +0200 Subject: [PATCH 617/902] test(core): :white_check_mark: update tests --- tests/test_models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index 663846bb..e32c945b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -3,7 +3,7 @@ from rocrate_validator import models, services from rocrate_validator.models import (LevelCollection, RequirementLevel, Severity, ValidationSettings) -from tests.ro_crates import InvalidFileDescriptor, InvalidRootDataEntity +from tests.ro_crates import InvalidRootDataEntity, WROCInvalidReadme def test_severity_ordering(): @@ -64,7 +64,7 @@ def validation_settings(): ) -@pytest.mark.skip(reason="Temporarily disabled: we need an RO-Crate with multiple failed requirements to test this.") +# @pytest.mark.skip(reason="Temporarily disabled: we need an RO-Crate with multiple failed requirements to test this.") def test_sortability_requirements(validation_settings: ValidationSettings): validation_settings.data_path = InvalidRootDataEntity().invalid_root_type result: models.ValidationResult = services.validate(validation_settings) @@ -75,7 +75,7 @@ def test_sortability_requirements(validation_settings: ValidationSettings): def test_sortability_checks(validation_settings: ValidationSettings): - validation_settings.data_path = InvalidFileDescriptor().invalid_json_format + validation_settings.data_path = WROCInvalidReadme().wroc_readme_wrong_encoding_format result: models.ValidationResult = services.validate(validation_settings) failed_checks = sorted(result.failed_checks, reverse=True) assert len(failed_checks) > 1 @@ -86,7 +86,7 @@ def test_sortability_checks(validation_settings: ValidationSettings): def test_sortability_issues(validation_settings: ValidationSettings): - validation_settings.data_path = InvalidFileDescriptor().invalid_json_format + validation_settings.data_path = WROCInvalidReadme().wroc_readme_wrong_encoding_format result: models.ValidationResult = services.validate(validation_settings) issues = sorted(result.get_issues(min_severity=Severity.OPTIONAL), reverse=True) assert len(issues) > 1 From af78fa5938cbd4ae025411c94bed135662e3131a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 4 Jul 2024 20:57:06 +0200 Subject: [PATCH 618/902] fix(cli): :lipstick: fix output of the validation command --- rocrate_validator/cli/commands/validate.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 6be60546..a6798d7d 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -12,7 +12,7 @@ from ... import services from ...colors import get_severity_color -from ...models import Severity, ValidationResult +from ...models import LevelCollection, Severity, ValidationResult from ...utils import URI, get_profiles_path from ..main import cli, click @@ -151,7 +151,7 @@ def validate(ctx, # using ctx.exit seems to raise an Exception that gets caught below, # so we use sys.exit instead. - sys.exit(0 if result.passed(Severity.RECOMMENDED) else 1) + sys.exit(0 if result.passed(LevelCollection.get(requirement_severity).severity) else 1) def __print_validation_result__( @@ -204,9 +204,10 @@ def __print_validation_result__( if issue.resultPath: path += "=" path += f"\"[green]{issue.value}[/green]\"" - path = path + " of " if len(path) > 0 else "on " + if issue.focusNode: + path = path + " of " if len(path) > 0 else " on " + f"[cyan]<{issue.focusNode}>[/cyan]" console.print( f"{' ' * 6}- [[red]Violation[/red] of " - f"[{issue_color} bold]{issue.check.identifier}[/{issue_color} bold] {path}[cyan]<{issue.focusNode}>[/cyan]]: " + f"[{issue_color} bold]{issue.check.identifier}[/{issue_color} bold]{path}]: " f"{Markdown(issue.message).markup}",) console.print("\n", style="white") From 62b1eb44d54e64de0211183ca02319f5a74c4e78 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 5 Jul 2024 08:52:04 +0200 Subject: [PATCH 619/902] test(profiles): :recycle: remove duplicate code by using the ROCrate object within the context --- .../ro-crate/must/0_file_descriptor_format.py | 100 ++++++------- .../should/2_root_data_entity_relative_uri.py | 56 +------ .../should/5_web_data_entity_metadata.py | 137 +----------------- .../workflow-ro-crate/may/1_main_workflow.py | 73 ++-------- .../workflow-ro-crate/must/0_main_workflow.py | 64 ++------ 5 files changed, 88 insertions(+), 342 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py index 2fc4fd28..b3e25942 100644 --- a/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py @@ -1,6 +1,3 @@ -import json -from typing import Optional - import rocrate_validator.log as logging from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import (PyFunctionCheck, check, @@ -19,7 +16,7 @@ def test_existence(self, context: ValidationContext) -> bool: """ Check if the file descriptor is present in the RO-Crate """ - if not context.file_descriptor_path.exists(): + if not context.ro_crate.has_descriptor(): message = f'file descriptor "{context.rel_fd_path}" is not present' context.result.add_error(message, self) return False @@ -30,11 +27,11 @@ def test_size(self, context: ValidationContext) -> bool: """ Check if the file descriptor is not empty """ - if not context.file_descriptor_path.exists(): + if not context.ro_crate.has_descriptor(): message = f'file descriptor {context.rel_fd_path} is empty' context.result.add_error(message, self) return False - if context.file_descriptor_path.stat().st_size == 0: + if context.ro_crate.metadata.size == 0: context.result.add_error(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) return False return True @@ -49,9 +46,8 @@ class FileDescriptorJsonFormat(PyFunctionCheck): 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.file_descriptor_path) - with open(context.file_descriptor_path, "r") as file: - json.load(file) + logger.debug("Checking validity of JSON file at %s", context.ro_crate.metadata) + context.ro_crate.metadata.as_dict() return True except Exception as e: context.result.add_error( @@ -67,58 +63,54 @@ class FileDescriptorJsonLdFormat(PyFunctionCheck): The file descriptor MUST be a valid JSON-LD file """ - _json_dict_cache: Optional[dict] = None - - def get_json_dict(self, context: ValidationContext) -> dict: - if self._json_dict_cache is None or \ - self._json_dict_cache['file_descriptor_path'] != context.file_descriptor_path: - # invalid cache - try: - with open(context.file_descriptor_path, "r") as file: - self._json_dict_cache = dict( - json=json.load(file), - file_descriptor_path=context.file_descriptor_path) - except Exception as e: - context.result.add_error( - f'file descriptor "{context.rel_fd_path}" is not in the correct format', self) - if logger.isEnabledFor(logging.DEBUG): - logger.exception(e) - return {} - return self._json_dict_cache['json'] - @check(name="File Descriptor @context property validation") - def check_context(self, validation_context: ValidationContext) -> bool: + def check_context(self, context: ValidationContext) -> bool: """ Check if the file descriptor contains the @context property """ - json_dict = self.get_json_dict(validation_context) - if "@context" not in json_dict: - validation_context.result.add_error( - f'RO-Crate file descriptor "{validation_context.rel_fd_path}" ' - "does not contain a context", self) - return False - return True + try: + json_dict = context.ro_crate.metadata.as_dict() + if "@context" not in json_dict: + context.result.add_error( + f'RO-Crate file descriptor "{context.rel_fd_path}" ' + "does not contain a context", 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 """ - json_dict = self.get_json_dict(context) - for entity in json_dict["@graph"]: - if "@id" not in entity: - context.result.add_error( - 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 + try: + json_dict = context.ro_crate.metadata.as_dict() + for entity in json_dict["@graph"]: + if "@id" not in entity: + context.result.add_error( + 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 as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + 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 """ - json_dict = self.get_json_dict(context) - for entity in json_dict["@graph"]: - if "@type" not in entity: - context.result.add_error( - 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 + try: + json_dict = context.ro_crate.metadata.as_dict() + for entity in json_dict["@graph"]: + if "@type" not in entity: + context.result.add_error( + 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 as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return False diff --git a/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py b/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py index a083d0ba..bfc4a80d 100644 --- a/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py +++ b/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py @@ -1,6 +1,3 @@ -import json -from typing import Optional - import rocrate_validator.log as logging from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import (PyFunctionCheck, check, @@ -16,55 +13,16 @@ class RootDataEntityRelativeURI(PyFunctionCheck): The Root Data Entity SHOULD be denoted by the string / """ - _json_dict_cache: Optional[dict] = None - - def get_json_dict(self, context: ValidationContext) -> dict: - if self._json_dict_cache is None or \ - self._json_dict_cache['file_descriptor_path'] != context.file_descriptor_path: - # invalid cache - try: - with open(context.file_descriptor_path, "r") as file: - self._json_dict_cache = dict( - json=json.load(file), - file_descriptor_path=context.file_descriptor_path) - except Exception as e: - context.result.add_error( - f'file descriptor "{context.rel_fd_path}" is not in the correct format', self) - if logger.isEnabledFor(logging.DEBUG): - logger.exception(e) - return {} - return self._json_dict_cache['json'] - - def find_entity(self, context: ValidationContext, entity_id: str) -> dict: - json_dict = self.get_json_dict(context) - for entity in json_dict["@graph"]: - if entity["@id"] == entity_id: - return entity - return {} - - def find_property(self, context: ValidationContext, entity_id: str, property_name: str) -> dict: - entity = self.find_entity(context, entity_id) - if entity: - return entity.get(property_name, {}) - return {} - @check(name="Root Data Entity: RECOMMENDED value") def check_relative_uris(self, context: ValidationContext) -> bool: """Check if the Root Data Entity is denoted by the string `./` in the file descriptor JSON-LD""" - about_property = self.find_property(context, "ro-crate-metadata.json", "about") - if not about_property: - context.result.add_error( - 'Unable to find the about property on `ro-crate-metadata.json`', self) - return False - root_entity_id = about_property.get("@id", None) - if not root_entity_id: - context.result.add_error( - 'Unable to identity the Root Data Entity from the `about` property of the `ro-crate-metadata.json`', self) + try: + if not context.ro_crate.metadata.get_root_data_entity().id == './': + context.result.add_error( + 'Root Data Entity URI is not denoted by the string `./`', self) + return True return False - # check relative URIs - if not root_entity_id == './': + except Exception as e: context.result.add_error( - 'Root Data Entity URI is not denoted by the string `./`', self) + f'Error checking Root Data Entity URI: {str(e)}', self) return False - - return False diff --git a/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py index 4ade478e..7189ddac 100644 --- a/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py @@ -1,10 +1,3 @@ -import http -import json -import urllib.request -from typing import Optional - -import requests - import rocrate_validator.log as logging from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import (PyFunctionCheck, check, @@ -14,119 +7,12 @@ logger = logging.getLogger(__name__) -class WebDataEntity: - - def __init__(self, entity: dict): - self._data = entity - self._remote_site = None - self._download_exception = None - - @property - def data(self) -> dict: - return self._data.copy() - - @property - def id(self) -> str: - return self._data.get("@id", "") - - def get_property(self, property_name: str) -> dict: - return self._data.get(property_name, None) - - def is_web_data_entity(self) -> bool: - return self._data.get("@id", "").startswith(("http", "https")) - - def is_ftp_data_entity(self) -> bool: - return self._data.get("@id", "").startswith(("ftp", "ftps")) - - @property - def remote_resource(self) -> http.client.HTTPResponse: - if not self._remote_site: - self._remote_site = urllib.request.urlopen(self.id) - return self._remote_site - - @property - def content_size(self) -> Optional[int]: - r = self.remote_resource - if not r: - return -1 - try: - return int(r.info().get('Content-Length')) - except Exception as e: - if logger.isEnabledFor(logging.DEBUG): - logger.exception(e) - return -1 - - def is_downloadable(self, silent: bool = True) -> bool: - if not self.is_web_data_entity(): - return False - # if self.is_ftp_data_entity() and not self._data.get("@id", "").startswith(("ftp://", "ftps://")): - # return False - try: - remote = self.remote_resource - return remote and remote.status == 200 - except Exception as e: - logger.exception(e) - # if not silent raise the exception - if not silent: - raise e - return False - - @requirement(name="Web-based Data Entity: RECOMMENDED resource availability") class WebDataEntityRecommendedChecker(PyFunctionCheck): """ Web-based Data Entity instances SHOULD be available at the URIs specified in the `@id` property of the Web-based Data Entity. """ - _json_dict_cache: Optional[dict] = None - _resources_cache: dict[str, requests.Response] = {} - - def get_json_dict(self, context: ValidationContext) -> dict: - if self._json_dict_cache is None or \ - self._json_dict_cache['file_descriptor_path'] != context.file_descriptor_path: - # invalid cache - try: - with open(context.file_descriptor_path, "r") as file: - self._json_dict_cache = dict( - json=json.load(file), - file_descriptor_path=context.file_descriptor_path) - except Exception as e: - context.result.add_error( - f'file descriptor "{context.rel_fd_path}" is not in the correct format', self) - if logger.isEnabledFor(logging.DEBUG): - logger.exception(e) - return {} - return self._json_dict_cache['json'] - - def find_entity(self, context: ValidationContext, entity_id: str) -> dict: - json_dict = self.get_json_dict(context) - for entity in json_dict["@graph"]: - if entity["@id"] == entity_id: - return entity - return {} - - def find_property(self, context: ValidationContext, entity_id: str, property_name: str) -> dict: - entity = self.find_entity(context, entity_id) - if entity: - return entity.get(property_name, {}) - return {} - - def get_web_data_entities(self, context: ValidationContext) -> list[WebDataEntity]: - json_dict = self.get_json_dict(context) - web_data_entities = [] - for entity in json_dict["@graph"]: - entity_id = entity.get("@id", None) - entity_type = entity.get("@type", None) - # Skip entity if it is not a File - if isinstance(entity_type, list): - if not "File" in entity_type: - continue - if not "File" in entity_type: - continue - if entity_id is not None and entity_id.startswith(("http", "https")): - web_data_entities.append(WebDataEntity(entity)) - return web_data_entities - @check(name="Web-based Data Entity: resource availability") def check_availability(self, context: ValidationContext) -> bool: """ @@ -134,19 +20,13 @@ def check_availability(self, context: ValidationContext) -> bool: by a simple retrieval (e.g. HTTP GET) permitting redirection and HTTP/HTTPS URIs """ result = True - for entity in self.get_web_data_entities(context): + for entity in context.ro_crate.metadata.get_web_data_entities(): assert entity.id is not None, "Entity has no @id" try: - if not entity.is_downloadable(silent=False): - response = entity.remote_resource - if response is None: - context.result.add_error( - f'Web-based Data Entity {entity.id} is not available', self) - result = False - elif response.status != 200: - context.result.add_error( - f'Web-based Data Entity {entity.id} is not available (HTTP {response.status_code})', self) - result = False + if not entity.is_available(): + context.result.add_error( + f'Web-based Data Entity {entity.id} is not available', self) + result = False except Exception as e: context.result.add_error( f'Web-based Data Entity {entity.id} is not available: {e}', self) @@ -162,11 +42,11 @@ 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 self.get_web_data_entities(context): + for entity in context.ro_crate.metadata.get_web_data_entities(): assert entity.id is not None, "Entity has no @id" - if entity.is_downloadable(): + if entity.is_available(): content_size = entity.get_property("contentSize") - if content_size and int(content_size) != entity.content_size: + if content_size and int(content_size) != context.ro_crate.get_external_file_size(entity.id): context.result.add_check_issue( f'The property contentSize={content_size} of the Web-based Data Entity {entity.id} does not match the actual size of ' f'the downloadable content, i.e., {entity.content_size} (bytes)', self, @@ -174,5 +54,4 @@ def check_content_size(self, context: ValidationContext) -> bool: result = False if not result and context.fail_fast: return result - return result 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 8f687746..9218a2d0 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 @@ -1,6 +1,3 @@ -import json -from typing import Optional - import rocrate_validator.log as logging from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import (PyFunctionCheck, check, @@ -10,76 +7,34 @@ logger = logging.getLogger(__name__) -def find_metadata_file_descriptor(entity_dict: dict): - for k, v in entity_dict.items(): - if k.endswith("ro-crate-metadata.json"): - return v - raise ValueError("no metadata file descriptor in crate") - - -def find_root_data_entity(entity_dict: dict): - metadata_file_descriptor = find_metadata_file_descriptor(entity_dict) - return entity_dict[metadata_file_descriptor["about"]["@id"]] - - -def find_main_workflow(entity_dict: dict): - root_data_entity = find_root_data_entity(entity_dict) - return entity_dict[root_data_entity["mainEntity"]["@id"]] - - @requirement(name="Workflow-related files existence") class WorkflowFilesExistence(PyFunctionCheck): """Checks for workflow-related crate files existence.""" - _json_dict_cache: Optional[dict] = None - - def get_json_dict(self, context: ValidationContext) -> dict: - if self._json_dict_cache is None or \ - self._json_dict_cache['file_descriptor_path'] != context.file_descriptor_path: - # invalid cache - try: - with open(context.file_descriptor_path, "r") as file: - self._json_dict_cache = dict( - json=json.load(file), - file_descriptor_path=context.file_descriptor_path) - except Exception as e: - context.result.add_error( - f'file descriptor "{context.rel_fd_path}" is not in the correct format', self) - if logger.isEnabledFor(logging.DEBUG): - logger.exception(e) - return {} - return self._json_dict_cache['json'] - @check(name="Workflow diagram existence") - def check_workflow_diagram(self, validation_context: ValidationContext) -> bool: + def check_workflow_diagram(self, context: ValidationContext) -> bool: """Check if the crate contains the workflow diagram.""" - json_dict = self.get_json_dict(validation_context) - entity_dict = {_["@id"]: _ for _ in json_dict["@graph"]} - main_workflow = find_main_workflow(entity_dict) - image = main_workflow.get("image") - diagram_relpath = image["@id"] if image else None + 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: - validation_context.result.add_error(f"main workflow does not have an 'image' property", self) + context.result.add_error(f"main workflow does not have an 'image' property", self) return False - diagram_path = validation_context.rocrate_path / diagram_relpath - if not diagram_path.is_file(): - validation_context.result.add_error(f"Workflow diagram {diagram_path} not found in crate", self) + if not image.is_available(): + context.result.add_error(f"Workflow diagram '{image.id}' not found in crate", self) return False return True @check(name="Workflow description existence") - def check_workflow_description(self, validation_context: ValidationContext) -> bool: + def check_workflow_description(self, context: ValidationContext) -> bool: """Check if the crate contains the workflow CWL description.""" - json_dict = self.get_json_dict(validation_context) - entity_dict = {_["@id"]: _ for _ in json_dict["@graph"]} - main_workflow = find_main_workflow(entity_dict) - main_workflow = main_workflow.get("subjectOf") - description_relpath = main_workflow["@id"] if main_workflow else None + 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: - validation_context.result.add_error("main workflow does not have a 'subjectOf' property", self) + context.result.add_error("main workflow does not have a 'subjectOf' property", self) return False - description_path = validation_context.rocrate_path / description_relpath - if not description_path.is_file(): - validation_context.result.add_error(f"Workflow CWL description {description_path} not found in crate", self) + if not main_workflow_subject.is_available(): + context.result.add_error(f"Workflow CWL description {main_workflow_subject.id} not found in crate", self) return False return True 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 37934d34..6bb63bb1 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 @@ -1,6 +1,3 @@ -import json -from typing import Optional - import rocrate_validator.log as logging from rocrate_validator.models import ValidationContext from rocrate_validator.requirements.python import (PyFunctionCheck, check, @@ -10,58 +7,23 @@ logger = logging.getLogger(__name__) -def find_metadata_file_descriptor(entity_dict: dict): - for k, v in entity_dict.items(): - if k.endswith("ro-crate-metadata.json"): - return v - raise ValueError("no metadata file descriptor in crate") - - -def find_root_data_entity(entity_dict: dict): - metadata_file_descriptor = find_metadata_file_descriptor(entity_dict) - return entity_dict[metadata_file_descriptor["about"]["@id"]] - - -def find_main_workflow(entity_dict: dict): - root_data_entity = find_root_data_entity(entity_dict) - return entity_dict[root_data_entity["mainEntity"]["@id"]] - - @requirement(name="Main Workflow file existence") class MainWorkflowFileExistence(PyFunctionCheck): """Checks for main workflow file existence.""" - _json_dict_cache: Optional[dict] = None - - def get_json_dict(self, context: ValidationContext) -> dict: - if self._json_dict_cache is None or \ - self._json_dict_cache['file_descriptor_path'] != context.file_descriptor_path: - # invalid cache - try: - with open(context.file_descriptor_path, "r") as file: - self._json_dict_cache = dict( - json=json.load(file), - file_descriptor_path=context.file_descriptor_path) - except Exception as e: - context.result.add_error( - f'file descriptor "{context.rel_fd_path}" is not in the correct format', self) - if logger.isEnabledFor(logging.DEBUG): - logger.exception(e) - return {} - return self._json_dict_cache['json'] - @check(name="Main Workflow file must exist") - def check_workflow(self, validation_context: ValidationContext) -> bool: + def check_workflow(self, context: ValidationContext) -> bool: """Check if the crate contains the main workflow file.""" - json_dict = self.get_json_dict(validation_context) - entity_dict = {_["@id"]: _ for _ in json_dict["@graph"]} - main_workflow = find_main_workflow(entity_dict) - if not main_workflow: - validation_context.result.add_error(f"main workflow does not exist in metadata file", self) - return False - workflow_relpath = main_workflow["@id"] - workflow_path = validation_context.rocrate_path / workflow_relpath - if not workflow_path.is_file(): - validation_context.result.add_error(f"Main Workflow {workflow_path} not found in crate", self) - return False + try: + main_workflow = context.ro_crate.metadata.get_main_workflow() + if not main_workflow: + context.result.add_error(f"main workflow does not exist in metadata file", self) + return False + if not main_workflow.is_available(): + context.result.add_error(f"Main Workflow {main_workflow.id} not found in crate", self) + return False + except ValueError as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + raise ValueError("no metadata file descriptor in crate") return True From 82655aed44e2f34b077202b319da3170f26f33fc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 5 Jul 2024 08:54:56 +0200 Subject: [PATCH 620/902] test(utils): :white_check_mark: test URI class --- tests/unit/test_uri.py | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/unit/test_uri.py diff --git a/tests/unit/test_uri.py b/tests/unit/test_uri.py new file mode 100644 index 00000000..522da00d --- /dev/null +++ b/tests/unit/test_uri.py @@ -0,0 +1,72 @@ +import unittest + +import pytest + +from rocrate_validator.utils import URI + + +def test_valid_url(): + uri = URI("http://example.com") + assert uri.is_remote_resource() + + +def test_invalid_url(): + with pytest.raises(ValueError): + URI("httpx:///example.com") + + +def test_url_with_query_params(): + uri = URI("http://example.com?param1=value1¶m2=value2") + assert uri.get_query_param("param1") == "value1" + assert uri.get_query_param("param2") == "value2" + + +def test_url_without_query_params(): + uri = URI("http://example.com") + assert uri.get_query_param("param1") is None + + +def test_url_with_fragment(): + uri = URI("http://example.com#fragment") + assert uri.fragment == "fragment" + + +def test_url_without_fragment(): + uri = URI("http://example.com") + assert uri.fragment is None + + +def test_valid_path(): + uri = URI("README.md") + assert uri.is_local_resource() + assert uri.is_available() + + +def test_invalid_path(): + uri = URI("path/to/file.txt") + assert not uri.is_available() + + +def test_path_with_query_params(): + uri = URI("/path/to/file.txt?param1=value1¶m2=value2") + assert uri.get_query_param("param1") == "value1" + assert uri.get_query_param("param2") == "value2" + + +def test_path_without_query_params(): + uri = URI("/path/to/file.txt") + assert uri.get_query_param("param1") is None + + +def test_path_with_fragment(): + uri = URI("/path/to/file.txt#fragment") + assert uri.fragment == "fragment" + + +def test_path_without_fragment(): + uri = URI("/path/to/file.txt") + assert uri.fragment is None + + +if __name__ == '__main__': + unittest.main() From ce14c723aea4cae155b8f7b203be68d1ba155335 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 5 Jul 2024 08:55:53 +0200 Subject: [PATCH 621/902] test(core): :white_check_mark: test the ROCrate module --- tests/unit/test_rocrate.py | 218 +++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 tests/unit/test_rocrate.py diff --git a/tests/unit/test_rocrate.py b/tests/unit/test_rocrate.py new file mode 100644 index 00000000..1b418acb --- /dev/null +++ b/tests/unit/test_rocrate.py @@ -0,0 +1,218 @@ + +from pathlib import Path +import pytest + +from rocrate_validator import log as logging +from rocrate_validator.errors import ROCrateInvalidURIError +from rocrate_validator.rocrate import ROCrate, ROCrateEntity, ROCrateLocalFolder, ROCrateLocalZip, ROCrateMetadata, ROCrateRemoteZip +from tests.ro_crates import ValidROC + +# set up logging +logger = logging.getLogger(__name__) + + +metadata_file_descriptor = Path(ROCrateMetadata.METADATA_FILE_DESCRIPTOR) + + +################################ +###### ROCrateLocalFolder ###### +################################ + + +def test_invalid_local_ro_crate(): + with pytest.raises(ROCrateInvalidURIError): + ROCrateLocalFolder("/tmp/does_not_exist") + + +def test_valid_local_rocrate(): + roc = ROCrateLocalFolder(ValidROC().wrroc_paper) + assert isinstance(roc, ROCrateLocalFolder) + + # raise Exception("Test not implemented: %s", str(roc.uri)) + + # test list files + files = roc.list_files() + logger.debug(f"Files: {files}") + assert len(files) == 14, "Should have 14 files" + + # test is_file + assert roc.has_file(metadata_file_descriptor), "Should be a file" + + # test file size + size = roc.get_file_size(metadata_file_descriptor) + assert size == 26788, "Size should be 26788" + + # test crate size + assert roc.size == 309520, "Size should be 309520" + + # test get_file_content binary mode + content = roc.get_file_content(metadata_file_descriptor) + assert isinstance(content, bytes), "Content should be bytes" + + # test get_file_content text mode + content = roc.get_file_content(metadata_file_descriptor, binary_mode=False) + assert isinstance(content, str), "Content should be str" + + # test metadata + metadata = roc.metadata + assert isinstance(metadata, ROCrateMetadata), "Metadata should be ROCrateMetadata" + + # test metadata id + file_descriptor_entity = metadata.get_entity("ro-crate-metadata.json") + logger.debug(f"File descriptor entity: {file_descriptor_entity}") + assert isinstance(file_descriptor_entity, ROCrateEntity), "Entity should be ROCrateEntity" + assert file_descriptor_entity.id == "ro-crate-metadata.json", "Id should be ro-crate-metadata.json" + assert file_descriptor_entity.type == "CreativeWork", "Type should be File" + + # test root data entity + root_data_entity = metadata.get_entity("./") + logger.debug(f"Root data entity: {root_data_entity}") + assert isinstance(root_data_entity, ROCrateEntity), "Entity should be ROCrateEntity" + assert root_data_entity.id == "./", "Id should be ./" + assert root_data_entity.type == "Dataset", "Type should be Dataset" + assert root_data_entity.name == "Recording provenance of workflow runs with RO-Crate (RO-Crate and mapping)", "Name should be wrroc-paper" + + # check metadata consistency + assert root_data_entity.metadata == metadata, "Metadata should be the same" + assert root_data_entity.metadata == roc.metadata, "Metadata should be the same" + + # check availability + assert root_data_entity.is_available(), "Main entity should be available" + + +################################ +###### ROCrateLocalZip ######### +################################ +def test_valid_zip_rocrate(): + roc = ROCrateLocalZip(ValidROC().sort_and_change_archive) + assert isinstance(roc, ROCrateLocalZip) + + # test list files + files = roc.list_files() + logger.debug(f"Files: {files}") + assert len(files) == 9, "Should have 5 files" + + # test is_file + assert roc.has_file(metadata_file_descriptor), "Should be a file" + + # test file size + size = roc.get_file_size(metadata_file_descriptor) + assert size == 3882, "Size should be 26788" + + # test crate size + assert roc.size == 136267, "Size should be 136267" + + # test get_file_content binary mode + content = roc.get_file_content(metadata_file_descriptor) + assert isinstance(content, bytes), "Content should be bytes" + + # test get_file_content text mode + content = roc.get_file_content(metadata_file_descriptor, binary_mode=False) + assert isinstance(content, str), "Content should be str" + + # test metadata + metadata = roc.metadata + assert isinstance(metadata, ROCrateMetadata), "Metadata should be ROCrateMetadata" + + # test metadata id + file_descriptor_entity = metadata.get_entity("ro-crate-metadata.json") + logger.debug(f"File descriptor entity: {file_descriptor_entity}") + assert isinstance(file_descriptor_entity, ROCrateEntity), "Entity should be ROCrateEntity" + assert file_descriptor_entity.id == "ro-crate-metadata.json", "Id should be ro-crate-metadata.json" + assert file_descriptor_entity.type == "CreativeWork", "Type should be File" + + # test root data entity + root_data_entity = metadata.get_entity("./") + logger.debug(f"Root data entity: {root_data_entity}") + assert isinstance(root_data_entity, ROCrateEntity), "Entity should be ROCrateEntity" + assert root_data_entity.id == "./", "Id should be ./" + assert root_data_entity.type == "Dataset", "Type should be Dataset" + assert root_data_entity.name == "sort-and-change-case", "Name should be sort_and_change" + + # test subEntity mainEntity + main_entity = root_data_entity.get_property("mainEntity") + logger.debug(f"Main entity: {main_entity}") + assert isinstance(main_entity, ROCrateEntity), "Entity should be ROCrateEntity" + assert main_entity.id == "sort-and-change-case.ga", "Id should be main-entity" + assert "ComputationalWorkflow" in main_entity.type, "Type should be ComputationalWorkflow" + + # check metadata consistency + assert main_entity.metadata == metadata, "Metadata should be the same" + assert main_entity.metadata == roc.metadata, "Metadata should be the same" + + # check availability + assert main_entity.is_available(), "Main entity should be available" + + +################################ +###### ROCrateRemote ########### +################################ + + +def test_valid_remote_zip_rocrate(): + roc = ROCrateRemoteZip(ValidROC().sort_and_change_remote) + # assert isinstance(roc, ROCrateRemoteZip) + # return + # # test list files + files = roc.list_files() + logger.debug(f"Files: {files}") + assert len(files) == 9, "Should have 5 files" + + # test crate size + assert roc.size == 136267, "Size should be 136267" + + # test is_file + assert roc.has_file(metadata_file_descriptor), "Should be a file" + + # test file size + size = roc.get_file_size(metadata_file_descriptor) + assert size == 3882, "Size should be 1097" + + # test get_file_content binary mode + content = roc.get_file_content(metadata_file_descriptor) + assert isinstance(content, bytes), "Content should be bytes" + + # test get_file_content text mode + content = roc.get_file_content(metadata_file_descriptor, binary_mode=False) + assert isinstance(content, str), "Content should be str" + + # test metadata + metadata = roc.metadata + assert isinstance(metadata, ROCrateMetadata), "Metadata should be ROCrateMetadata" + + # test metadata id + file_descriptor_entity = metadata.get_entity("ro-crate-metadata.json") + logger.debug(f"File descriptor entity: {file_descriptor_entity}") + assert isinstance(file_descriptor_entity, ROCrateEntity), "Entity should be ROCrateEntity" + assert file_descriptor_entity.id == "ro-crate-metadata.json", "Id should be ro-crate-metadata.json" + assert file_descriptor_entity.type == "CreativeWork", "Type should be File" + + # test root data entity + root_data_entity = metadata.get_entity("./") + logger.debug(f"Root data entity: {root_data_entity}") + assert isinstance(root_data_entity, ROCrateEntity), "Entity should be ROCrateEntity" + assert root_data_entity.id == "./", "Id should be ./" + assert root_data_entity.type == "Dataset", "Type should be Dataset" + assert root_data_entity.name == "sort-and-change-case", "Name should be sort_and_change" + + # test subEntity mainEntity + main_entity = root_data_entity.get_property("mainEntity") + logger.debug(f"Main entity: {main_entity}") + assert isinstance(main_entity, ROCrateEntity), "Entity should be ROCrateEntity" + assert main_entity.id == "sort-and-change-case.ga", "Id should be main-entity" + assert "ComputationalWorkflow" in main_entity.type, "Type should be ComputationalWorkflow" + + # check metadata consistency + assert main_entity.metadata == metadata, "Metadata should be the same" + assert main_entity.metadata == roc.metadata, "Metadata should be the same" + + # check availability + assert main_entity.is_available(), "Main entity should be available" + + +def test_external_file(): + content = ROCrate.get_external_file_content(ValidROC().sort_and_change_remote) + assert isinstance(content, bytes), "Content should be bytes" + + size = ROCrate.get_external_file_size(ValidROC().sort_and_change_remote) + assert size == 136267, "Size should be 136267" From 721a8618e04275755ccfff024cd4b46f63271149 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 5 Jul 2024 08:59:42 +0200 Subject: [PATCH 622/902] test(test-data): :white_check_mark: add test data --- .../data/crates/valid/sortchangecase.crate.zip | Bin 0 -> 136267 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/data/crates/valid/sortchangecase.crate.zip diff --git a/tests/data/crates/valid/sortchangecase.crate.zip b/tests/data/crates/valid/sortchangecase.crate.zip new file mode 100644 index 0000000000000000000000000000000000000000..cae3882c15c2e3928da7ce0374d435f1a91f1121 GIT binary patch literal 136267 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
avOYM?Kqb>T=CXu9ZjE|*T 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 Date: Fri, 21 Jun 2024 12:44:19 +0200 Subject: [PATCH 623/902] process run crate: add checks on root data entity --- .../must/2_root_data_entity_metadata.ttl | 23 +++++ .../ro-crate-metadata.json | 99 +++++++++++++++++++ .../ro-crate-metadata.json | 97 ++++++++++++++++++ .../process-run-crate/ro-crate-metadata.json | 99 +++++++++++++++++++ .../test_prc_root_data_entity.py | 53 ++++++++++ .../process-run-crate/test_valid_prc.py | 17 ++++ .../workflow-ro-crate/test_valid_wroc.py | 2 + tests/ro_crates.py | 17 ++++ 8 files changed, 407 insertions(+) create mode 100644 rocrate_validator/profiles/s_process-run-crate/must/2_root_data_entity_metadata.ttl create mode 100644 tests/data/crates/invalid/3_process_run_crate/conformsto_bad_profile/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/conformsto_bad_type/ro-crate-metadata.json create mode 100644 tests/data/crates/valid/process-run-crate/ro-crate-metadata.json create mode 100644 tests/integration/profiles/process-run-crate/test_prc_root_data_entity.py create mode 100644 tests/integration/profiles/process-run-crate/test_valid_prc.py diff --git a/rocrate_validator/profiles/s_process-run-crate/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/s_process-run-crate/must/2_root_data_entity_metadata.ttl new file mode 100644 index 00000000..0d38b9be --- /dev/null +++ b/rocrate_validator/profiles/s_process-run-crate/must/2_root_data_entity_metadata.ttl @@ -0,0 +1,23 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix rocrate: . + +ro:ProcRCRootDataEntityMetadata a sh:NodeShape ; + sh:name "Root Data Entity Metadata" ; + sh:description "Properties of the Root Data Entity" ; + sh:targetClass rocrate:RootDataEntity ; + sh:property [ + a sh:PropertyShape ; + sh:name "Root Data Entity conformsTo" ; + sh:description "The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile" ; + sh:path dct:conformsTo ; + sh:class schema:CreativeWork; + sh:pattern "^https://w3id.org/ro/wfrun/process/.*" ; + sh:minCount 1 ; + sh:message "The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile" ; + ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/conformsto_bad_profile/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/conformsto_bad_profile/ro-crate-metadata.json new file mode 100644 index 00000000..6fd1c2aa --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/conformsto_bad_profile/ro-crate-metadata.json @@ -0,0 +1,99 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/foobar/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/foobar/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "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" + } + }, + { + "@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/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/invalid/3_process_run_crate/conformsto_bad_type/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/conformsto_bad_type/ro-crate-metadata.json new file mode 100644 index 00000000..4d416a39 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/conformsto_bad_type/ro-crate-metadata.json @@ -0,0 +1,97 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "Action" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "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" + } + }, + { + "@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/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/valid/process-run-crate/ro-crate-metadata.json b/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json new file mode 100644 index 00000000..0a02e2d6 --- /dev/null +++ b/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json @@ -0,0 +1,99 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "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" + } + }, + { + "@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/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/integration/profiles/process-run-crate/test_prc_root_data_entity.py b/tests/integration/profiles/process-run-crate/test_prc_root_data_entity.py new file mode 100644 index 00000000..a35b3eee --- /dev/null +++ b/tests/integration/profiles/process-run-crate/test_prc_root_data_entity.py @@ -0,0 +1,53 @@ +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import ValidROC, InvalidProcRC +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_prc_no_conformsto(): + """\ + Test a Process Run Crate where the root data entity does not have a + conformsTo. + """ + do_entity_test( + ValidROC().workflow_roc, + Severity.REQUIRED, + False, + ["Root Data Entity Metadata"], + ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile"], + profile_name="s_process-run-crate" + ) + + +def test_prc_conformsto_bad_type(): + """\ + Test a Process Run Crate where the root data entity does not conformsTo a + CreativeWork. + """ + do_entity_test( + InvalidProcRC().conformsto_bad_type, + Severity.REQUIRED, + False, + ["Root Data Entity Metadata"], + ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile"], + profile_name="s_process-run-crate" + ) + + +def test_prc_conformsto_bad_profile(): + """\ + Test a Process Run Crate where the root data entity does not conformsTo a + Process Run Crate profile. + """ + do_entity_test( + InvalidProcRC().conformsto_bad_profile, + Severity.REQUIRED, + False, + ["Root Data Entity Metadata"], + ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile"], + profile_name="s_process-run-crate" + ) diff --git a/tests/integration/profiles/process-run-crate/test_valid_prc.py b/tests/integration/profiles/process-run-crate/test_valid_prc.py new file mode 100644 index 00000000..37059315 --- /dev/null +++ b/tests/integration/profiles/process-run-crate/test_valid_prc.py @@ -0,0 +1,17 @@ +import logging + +from rocrate_validator.models import Severity +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 Process Run Crate.""" + do_entity_test( + ValidROC().process_run_crate, + Severity.REQUIRED, + True, + profile_name="s_process-run-crate" + ) 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 2b092b94..15a25847 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py +++ b/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py @@ -1,4 +1,5 @@ import logging +import pytest from rocrate_validator.models import Severity from tests.ro_crates import ValidROC @@ -7,6 +8,7 @@ logger = logging.getLogger(__name__) +@pytest.mark.xfail(reason="workflow ro-crate loaded after process run crate") def test_valid_workflow_roc_required(): """Test a valid Workflow RO-Crate.""" do_entity_test( diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 70f1de5b..c9c2dbaa 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -41,6 +41,10 @@ def sort_and_change_remote(self) -> Path: def sort_and_change_archive(self) -> Path: return VALID_CRATES_DATA_PATH / "sortchangecase.crate.zip" + @property + def process_run_crate(self) -> Path: + return VALID_CRATES_DATA_PATH / "process-run-crate" + class InvalidFileDescriptor: @@ -252,3 +256,16 @@ class WROCNoLicense: @property def wroc_no_license(self) -> Path: return self.base_path / "no_license" + + +class InvalidProcRC: + + base_path = INVALID_CRATES_DATA_PATH / "3_process_run_crate/" + + @property + def conformsto_bad_type(self) -> Path: + return self.base_path / "conformsto_bad_type" + + @property + def conformsto_bad_profile(self) -> Path: + return self.base_path / "conformsto_bad_profile" From 50076ce1abfb3b626d71d743f1cc94bdad752aab Mon Sep 17 00:00:00 2001 From: simleo Date: Fri, 21 Jun 2024 17:34:27 +0200 Subject: [PATCH 624/902] process run crate: new should shapes --- .../should/0_software-application.ttl | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 rocrate_validator/profiles/s_process-run-crate/should/0_software-application.ttl diff --git a/rocrate_validator/profiles/s_process-run-crate/should/0_software-application.ttl b/rocrate_validator/profiles/s_process-run-crate/should/0_software-application.ttl new file mode 100644 index 00000000..05bdea0c --- /dev/null +++ b/rocrate_validator/profiles/s_process-run-crate/should/0_software-application.ttl @@ -0,0 +1,80 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix rocrate: . +@prefix bioschemas: . + + +ro:ProcRCApplication a sh:NodeShape ; + sh:name "ProcRC Application" ; + sh:description "Properties of a Process Run Crate Application" ; + sh:targetClass schema:SoftwareApplication, + schema:SoftwareSourceCode, + bioschemas:ComputationalWorkflow; + sh:property [ + a sh:PropertyShape ; + sh:name "Application name" ; + sh:description "The Application SHOULD have a name" ; + sh:path schema:name ; + sh:minCount 1 ; + sh:message "The Application SHOULD have a name" ; + ] . + + +ro:ProcRCSoftwareSourceCodeComputationalWorkflow a sh:NodeShape ; + sh:name "ProcRC SoftwareSourceCode or ComputationalWorkflow" ; + sh:description "Properties of a Process Run Crate SoftwareSourceCode or ComputationalWorkflow" ; + sh:targetClass schema:SoftwareSourceCode, + bioschemas:ComputationalWorkflow; + sh:property [ + a sh:PropertyShape ; + sh:name "version" ; + sh:description "The SoftwareSourceCode or ComputationalWorkflow SHOULD have a version" ; + sh:path schema:version ; + sh:minCount 1 ; + sh:message "The SoftwareSourceCode or ComputationalWorkflow SHOULD have a version" ; + ] . + + + # ro:ProcRCSoftwareApplication a sh:NodeShape ; + # sh:name "ProcRC SoftwareApplication" ; + # sh:description "Properties of a Process Run Crate SoftwareApplication" ; + # sh:targetClass schema:SoftwareApplication ; + # sh:or ( + # [ sh:property [ + # a sh:PropertyShape ; + # sh:name "version" ; + # sh:description "The SoftwareApplication SHOULD have a version or softwareVersion" ; + # sh:path schema:version ; + # sh:minCount 1 ; + # sh:message "The SoftwareApplication SHOULD have a version or softwareVersion" ; + # ]] + # [ sh:property [ + # a sh:PropertyShape ; + # sh:name "softwareVersion" ; + # sh:description "The SoftwareApplication SHOULD have a version or softwareVersion" ; + # sh:path schema:softwareVersion ; + # sh:minCount 1 ; + # sh:message "The SoftwareApplication SHOULD have a version or softwareVersion" ; + # ]] + # ) . + + +ro:ProcRCSoftwareApplicationID a sh:NodeShape ; + sh:name "ProcRC SoftwareApplication ID" ; + sh:description "Process Run Crate SoftwareApplication ID" ; + sh:targetNode schema:SoftwareApplication , + schema:SoftwareSourceCode, + bioschemas:ComputationalWorkflow; + sh:property [ + a sh:PropertyShape ; + sh:name "SoftwareApplication id" ; + sh:description "The SoftwareApplication id SHOULD be an absolute URI" ; + sh:path [ sh:inversePath rdf:type ] ; + sh:pattern "^http.*" ; + sh:message "The SoftwareApplication id SHOULD be an absolute URI" ; + ] . From 32313059f2253fef53b99913fae626a6ec26cd7b Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 24 Jun 2024 14:40:42 +0200 Subject: [PATCH 625/902] process run crate: continue with should shapes --- .../should/0_software-application.ttl | 45 ++++----- .../ro-crate-metadata.json | 99 +++++++++++++++++++ .../ro-crate-metadata.json | 98 ++++++++++++++++++ .../application_no_url/ro-crate-metadata.json | 98 ++++++++++++++++++ .../ro-crate-metadata.json | 98 ++++++++++++++++++ .../ro-crate-metadata.json | 99 +++++++++++++++++++ .../process-run-crate/test_prc_application.py | 81 +++++++++++++++ .../process-run-crate/test_valid_prc.py | 2 +- tests/ro_crates.py | 20 ++++ 9 files changed, 617 insertions(+), 23 deletions(-) create mode 100644 tests/data/crates/invalid/3_process_run_crate/application_id_no_absoluteuri/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/application_no_name/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/application_no_url/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/application_no_version/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/softwaresourcecode_no_version/ro-crate-metadata.json create mode 100644 tests/integration/profiles/process-run-crate/test_prc_application.py diff --git a/rocrate_validator/profiles/s_process-run-crate/should/0_software-application.ttl b/rocrate_validator/profiles/s_process-run-crate/should/0_software-application.ttl index 05bdea0c..ab7322e7 100644 --- a/rocrate_validator/profiles/s_process-run-crate/should/0_software-application.ttl +++ b/rocrate_validator/profiles/s_process-run-crate/should/0_software-application.ttl @@ -22,6 +22,14 @@ ro:ProcRCApplication a sh:NodeShape ; sh:path schema:name ; sh:minCount 1 ; sh:message "The Application SHOULD have a name" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Application url" ; + sh:description "The Application SHOULD have a url" ; + sh:path schema:url ; + sh:minCount 1 ; + sh:message "The Application SHOULD have a url" ; ] . @@ -40,28 +48,21 @@ ro:ProcRCSoftwareSourceCodeComputationalWorkflow a sh:NodeShape ; ] . - # ro:ProcRCSoftwareApplication a sh:NodeShape ; - # sh:name "ProcRC SoftwareApplication" ; - # sh:description "Properties of a Process Run Crate SoftwareApplication" ; - # sh:targetClass schema:SoftwareApplication ; - # sh:or ( - # [ sh:property [ - # a sh:PropertyShape ; - # sh:name "version" ; - # sh:description "The SoftwareApplication SHOULD have a version or softwareVersion" ; - # sh:path schema:version ; - # sh:minCount 1 ; - # sh:message "The SoftwareApplication SHOULD have a version or softwareVersion" ; - # ]] - # [ sh:property [ - # a sh:PropertyShape ; - # sh:name "softwareVersion" ; - # sh:description "The SoftwareApplication SHOULD have a version or softwareVersion" ; - # sh:path schema:softwareVersion ; - # sh:minCount 1 ; - # sh:message "The SoftwareApplication SHOULD have a version or softwareVersion" ; - # ]] - # ) . +ro:ProcRCSoftwareApplication a sh:NodeShape ; + sh:name "ProcRC SoftwareApplication" ; + sh:description "Properties of a Process Run Crate SoftwareApplication" ; + sh:targetClass schema:SoftwareApplication ; + sh:property [ + a sh:PropertyShape ; + sh:name "version or softwareVersion" ; + sh:description "The SoftwareApplication SHOULD have a version or softwareVersion" ; + sh:message "The SoftwareApplication SHOULD have a version or softwareVersion" ; + sh:path [ + sh:alternativePath ( schema:version schema:softwareVersion ) ; + ] ; + sh:minLength 1 ; + sh:minCount 1 ; + ] . ro:ProcRCSoftwareApplicationID a sh:NodeShape ; diff --git a/tests/data/crates/invalid/3_process_run_crate/application_id_no_absoluteuri/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/application_id_no_absoluteuri/ro-crate-metadata.json new file mode 100644 index 00000000..21203178 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/application_id_no_absoluteuri/ro-crate-metadata.json @@ -0,0 +1,99 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "#imagemagick", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "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" + } + }, + { + "@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/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/invalid/3_process_run_crate/application_no_name/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/application_no_name/ro-crate-metadata.json new file mode 100644 index 00000000..46fc2a37 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/application_no_name/ro-crate-metadata.json @@ -0,0 +1,98 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "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" + } + }, + { + "@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/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/invalid/3_process_run_crate/application_no_url/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/application_no_url/ro-crate-metadata.json new file mode 100644 index 00000000..a689bff5 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/application_no_url/ro-crate-metadata.json @@ -0,0 +1,98 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "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" + } + }, + { + "@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/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/invalid/3_process_run_crate/application_no_version/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/application_no_version/ro-crate-metadata.json new file mode 100644 index 00000000..920f0ba3 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/application_no_version/ro-crate-metadata.json @@ -0,0 +1,98 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick" + }, + { + "@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", + "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" + } + }, + { + "@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/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/invalid/3_process_run_crate/softwaresourcecode_no_version/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/softwaresourcecode_no_version/ro-crate-metadata.json new file mode 100644 index 00000000..e87874db --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/softwaresourcecode_no_version/ro-crate-metadata.json @@ -0,0 +1,99 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareSourceCode", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "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" + } + }, + { + "@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/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/integration/profiles/process-run-crate/test_prc_application.py b/tests/integration/profiles/process-run-crate/test_prc_application.py new file mode 100644 index 00000000..158e7ffc --- /dev/null +++ b/tests/integration/profiles/process-run-crate/test_prc_application.py @@ -0,0 +1,81 @@ +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import InvalidProcRC +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_prc_application_no_name(): + """\ + Test a Process Run Crate where the application does not have a name. + """ + do_entity_test( + InvalidProcRC().application_no_name, + Severity.RECOMMENDED, + False, + ["ProcRC Application"], + ["The Application SHOULD have a name"], + profile_name="s_process-run-crate" + ) + + +def test_prc_application_no_url(): + """\ + Test a Process Run Crate where the application does not have a url. + """ + do_entity_test( + InvalidProcRC().application_no_url, + Severity.RECOMMENDED, + False, + ["ProcRC Application"], + ["The Application SHOULD have a url"], + profile_name="s_process-run-crate" + ) + + +def test_prc_application_no_version(): + """\ + Test a Process Run Crate where the application does not have a version or + SoftwareVersion (SoftwareApplication). + """ + do_entity_test( + InvalidProcRC().application_no_version, + Severity.RECOMMENDED, + False, + ["ProcRC SoftwareApplication"], + ["The SoftwareApplication SHOULD have a version or softwareVersion"], + profile_name="s_process-run-crate" + ) + + +def test_prc_softwaresourcecode_no_version(): + """\ + Test a Process Run Crate where the application does not have a version + (SoftwareSourceCode). + """ + do_entity_test( + InvalidProcRC().softwaresourcecode_no_version, + Severity.RECOMMENDED, + False, + ["ProcRC SoftwareSourceCode or ComputationalWorkflow"], + ["The SoftwareSourceCode or ComputationalWorkflow SHOULD have a version"], + profile_name="s_process-run-crate" + ) + + +def test_prc_application_id_no_absoluteuri(): + """\ + Test a Process Run Crate where the id of the application is not an + absolute URI. + """ + do_entity_test( + InvalidProcRC().application_id_no_absoluteuri, + Severity.RECOMMENDED, + False, + ["ProcRC SoftwareApplication ID"], + ["The SoftwareApplication id SHOULD be an absolute URI"], + profile_name="s_process-run-crate" + ) diff --git a/tests/integration/profiles/process-run-crate/test_valid_prc.py b/tests/integration/profiles/process-run-crate/test_valid_prc.py index 37059315..518ee193 100644 --- a/tests/integration/profiles/process-run-crate/test_valid_prc.py +++ b/tests/integration/profiles/process-run-crate/test_valid_prc.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) -def test_valid_workflow_roc_required(): +def test_valid_process_run_crate_required(): """Test a valid Process Run Crate.""" do_entity_test( ValidROC().process_run_crate, diff --git a/tests/ro_crates.py b/tests/ro_crates.py index c9c2dbaa..be11de4c 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -269,3 +269,23 @@ def conformsto_bad_type(self) -> Path: @property def conformsto_bad_profile(self) -> Path: return self.base_path / "conformsto_bad_profile" + + @property + def application_no_name(self) -> Path: + return self.base_path / "application_no_name" + + @property + def application_no_url(self) -> Path: + return self.base_path / "application_no_url" + + @property + def application_no_version(self) -> Path: + return self.base_path / "application_no_version" + + @property + def softwaresourcecode_no_version(self) -> Path: + return self.base_path / "softwaresourcecode_no_version" + + @property + def application_id_no_absoluteuri(self) -> Path: + return self.base_path / "application_id_no_absoluteuri" From 8772d3a2ced66d408b1f8732706584900d405832 Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 25 Jun 2024 15:14:14 +0200 Subject: [PATCH 626/902] add check on SoftwareApplication that has both version and softwareVersion --- .../should/0_software-application.ttl | 21 ++++ .../ro-crate-metadata.json | 100 ++++++++++++++++++ .../process-run-crate/test_prc_application.py | 15 +++ tests/ro_crates.py | 4 + 4 files changed, 140 insertions(+) create mode 100644 tests/data/crates/invalid/3_process_run_crate/application_version_softwareVersion/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/s_process-run-crate/should/0_software-application.ttl b/rocrate_validator/profiles/s_process-run-crate/should/0_software-application.ttl index ab7322e7..bb2c7c54 100644 --- a/rocrate_validator/profiles/s_process-run-crate/should/0_software-application.ttl +++ b/rocrate_validator/profiles/s_process-run-crate/should/0_software-application.ttl @@ -65,6 +65,27 @@ ro:ProcRCSoftwareApplication a sh:NodeShape ; ] . +ro:ProcRCSoftwareApplicationSingleVersion a sh:NodeShape ; + sh:name "ProcRC SoftwareApplication SingleVersion" ; + sh:description "Process Run Crate SoftwareApplication should not have both version and softwareVersion" ; + sh:message "Process Run Crate SoftwareApplication should not have both version and softwareVersion" ; + sh:targetClass schema:SoftwareApplication ; + sh:not [ + sh:and ( + [ sh:property [ + a sh:PropertyShape ; + sh:path schema:version ; + sh:minCount 1 ; + ]] + [ sh:property [ + a sh:PropertyShape ; + sh:path schema:softwareVersion ; + sh:minCount 1 ; + ]] + ) ; + ] . + + ro:ProcRCSoftwareApplicationID a sh:NodeShape ; sh:name "ProcRC SoftwareApplication ID" ; sh:description "Process Run Crate SoftwareApplication ID" ; diff --git a/tests/data/crates/invalid/3_process_run_crate/application_version_softwareVersion/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/application_version_softwareVersion/ro-crate-metadata.json new file mode 100644 index 00000000..0b6da817 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/application_version_softwareVersion/ro-crate-metadata.json @@ -0,0 +1,100 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "version": "6.9.7-4", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "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" + } + }, + { + "@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/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/integration/profiles/process-run-crate/test_prc_application.py b/tests/integration/profiles/process-run-crate/test_prc_application.py index 158e7ffc..c5a802b7 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_application.py +++ b/tests/integration/profiles/process-run-crate/test_prc_application.py @@ -51,6 +51,21 @@ def test_prc_application_no_version(): ) +def test_prc_application_version_softwareversion(): + """\ + Test a Process Run Crate where the application has both a version and a + SoftwareVersion (SoftwareApplication). + """ + do_entity_test( + InvalidProcRC().application_version_softwareVersion, + Severity.RECOMMENDED, + False, + ["ProcRC SoftwareApplication SingleVersion"], + ["Process Run Crate SoftwareApplication should not have both version and softwareVersion"], + profile_name="s_process-run-crate" + ) + + def test_prc_softwaresourcecode_no_version(): """\ Test a Process Run Crate where the application does not have a version diff --git a/tests/ro_crates.py b/tests/ro_crates.py index be11de4c..fb20b09b 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -289,3 +289,7 @@ def softwaresourcecode_no_version(self) -> Path: @property def application_id_no_absoluteuri(self) -> Path: return self.base_path / "application_id_no_absoluteuri" + + @property + def application_version_softwareVersion(self) -> Path: + return self.base_path / "application_version_softwareVersion" From dabe976b15db2f8d85475f136d7076fe48ce8ae9 Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 26 Jun 2024 11:02:30 +0200 Subject: [PATCH 627/902] add must checks on process run crate action --- .../must/1_create_action.ttl | 29 ++++++ .../ro-crate-metadata.json | 99 +++++++++++++++++++ .../ro-crate-metadata.json | 96 ++++++++++++++++++ .../process-run-crate/test_prc_action.py | 37 +++++++ tests/ro_crates.py | 8 ++ 5 files changed, 269 insertions(+) create mode 100644 rocrate_validator/profiles/s_process-run-crate/must/1_create_action.ttl create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_instrument_bad_type/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_no_instrument/ro-crate-metadata.json create mode 100644 tests/integration/profiles/process-run-crate/test_prc_action.py diff --git a/rocrate_validator/profiles/s_process-run-crate/must/1_create_action.ttl b/rocrate_validator/profiles/s_process-run-crate/must/1_create_action.ttl new file mode 100644 index 00000000..657d5539 --- /dev/null +++ b/rocrate_validator/profiles/s_process-run-crate/must/1_create_action.ttl @@ -0,0 +1,29 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix rocrate: . +@prefix bioschemas: . + +ro:ProcRCAction a sh:NodeShape ; + sh:name "Process Run Crate Action" ; + sh:description "Properties of the Process Run Crate Action" ; + sh:targetClass schema:CreateAction , + schema:ActivateAction , + schema:UpdateAction ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action instrument" ; + sh:description "The Action MUST have an instrument property that references the executed tool" ; + sh:path schema:instrument ; + sh:or ( + [ sh:class schema:SoftwareApplication ; ] + [ sh:class schema:SoftwareSourceCode ; ] + [ sh:class bioschemas:ComputationalWorkflow ; ] + ) ; + sh:minCount 1 ; + sh:message "The Action MUST have an instrument property that references the executed tool" ; + ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/action_instrument_bad_type/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_instrument_bad_type/ro-crate-metadata.json new file mode 100644 index 00000000..0a0a25ae --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_instrument_bad_type/ro-crate-metadata.json @@ -0,0 +1,99 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "Thing", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "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" + } + }, + { + "@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/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/invalid/3_process_run_crate/action_no_instrument/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_no_instrument/ro-crate-metadata.json new file mode 100644 index 00000000..097c9dcb --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_no_instrument/ro-crate-metadata.json @@ -0,0 +1,96 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "endTime": "2024-05-17T01:04:52+01:00", + "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" + } + }, + { + "@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/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/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py new file mode 100644 index 00000000..19e90790 --- /dev/null +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -0,0 +1,37 @@ +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import InvalidProcRC +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_prc_action_no_instrument(): + """\ + Test a Process Run Crate where the action does not have an instrument. + """ + do_entity_test( + InvalidProcRC().action_no_instrument, + Severity.REQUIRED, + False, + ["Process Run Crate Action"], + ["The Action MUST have an instrument property that references the executed tool"], + profile_name="s_process-run-crate" + ) + + +def test_prc_action_instrument_bad_type(): + """\ + Test a Process Run Crate where the instrument does not point to a + SoftwareApplication, SoftwareSourceCode or ComputationalWorkflow. + """ + do_entity_test( + InvalidProcRC().action_instrument_bad_type, + Severity.REQUIRED, + False, + ["Process Run Crate Action"], + ["The Action MUST have an instrument property that references the executed tool"], + profile_name="s_process-run-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index fb20b09b..b1e54a1a 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -293,3 +293,11 @@ def application_id_no_absoluteuri(self) -> Path: @property def application_version_softwareVersion(self) -> Path: return self.base_path / "application_version_softwareVersion" + + @property + def action_no_instrument(self) -> Path: + return self.base_path / "action_no_instrument" + + @property + def action_instrument_bad_type(self) -> Path: + return self.base_path / "action_instrument_bad_type" From b3aff5eac9e726489c64f56e6a4a828c657111d1 Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 26 Jun 2024 14:31:08 +0200 Subject: [PATCH 628/902] add check that action is mentioned by the root data entity --- .../must/1_create_action.ttl | 2 +- .../should/1_create_action.ttl | 25 +++++ .../ro-crate-metadata.json | 96 +++++++++++++++++++ .../process-run-crate/test_prc_action.py | 15 +++ tests/ro_crates.py | 4 + 5 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_not_mentioned/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/s_process-run-crate/must/1_create_action.ttl b/rocrate_validator/profiles/s_process-run-crate/must/1_create_action.ttl index 657d5539..a90e19a9 100644 --- a/rocrate_validator/profiles/s_process-run-crate/must/1_create_action.ttl +++ b/rocrate_validator/profiles/s_process-run-crate/must/1_create_action.ttl @@ -8,7 +8,7 @@ @prefix rocrate: . @prefix bioschemas: . -ro:ProcRCAction a sh:NodeShape ; +ro:ProcRCActionRequired a sh:NodeShape ; sh:name "Process Run Crate Action" ; sh:description "Properties of the Process Run Crate Action" ; sh:targetClass schema:CreateAction , diff --git a/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl new file mode 100644 index 00000000..3e6a2186 --- /dev/null +++ b/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl @@ -0,0 +1,25 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix rocrate: . +@prefix bioschemas: . + +ro:ProcRCActionRecommended a sh:NodeShape ; + sh:name "Process Run Crate Action SHOULD" ; + sh:description "Recommended properties of the Process Run Crate Action" ; + sh:targetClass schema:CreateAction , + schema:ActivateAction , + schema:UpdateAction ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action SHOULD be referenced via mentions from root" ; + sh:description "The Action SHOULD be referenced from the Root Data Entity via mentions" ; + sh:path [ sh:inversePath schema:mentions ] ; + sh:node rocrate:RootDataEntity ; + sh:minCount 1 ; + sh:message "The Action SHOULD be referenced from the Root Data Entity via mentions" ; + ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/action_not_mentioned/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_not_mentioned/ro-crate-metadata.json new file mode 100644 index 00000000..6ca0ed38 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_not_mentioned/ro-crate-metadata.json @@ -0,0 +1,96 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "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" + } + }, + { + "@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/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/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index 19e90790..85fc37db 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -35,3 +35,18 @@ def test_prc_action_instrument_bad_type(): ["The Action MUST have an instrument property that references the executed tool"], profile_name="s_process-run-crate" ) + + +def test_prc_action_not_mentioned(): + """\ + Test a Process Run Crate where the action is not listed in the Root Data + Entity's mentions. + """ + do_entity_test( + InvalidProcRC().action_not_mentioned, + Severity.RECOMMENDED, + False, + ["Process Run Crate Action SHOULD"], + ["The Action SHOULD be referenced from the Root Data Entity via mentions"], + profile_name="s_process-run-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index b1e54a1a..9f53cb72 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -301,3 +301,7 @@ def action_no_instrument(self) -> Path: @property def action_instrument_bad_type(self) -> Path: return self.base_path / "action_instrument_bad_type" + + @property + def action_not_mentioned(self) -> Path: + return self.base_path / "action_not_mentioned" From 1d381fa27e73e87f9e339ec481c03870514144f7 Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 27 Jun 2024 10:16:02 +0200 Subject: [PATCH 629/902] procrc: add checks for name, description, endTime --- .../should/1_create_action.ttl | 24 +++++ .../ro-crate-metadata.json | 98 +++++++++++++++++++ .../action_no_endtime/ro-crate-metadata.json | 98 +++++++++++++++++++ .../action_no_name/ro-crate-metadata.json | 98 +++++++++++++++++++ .../process-run-crate/test_prc_action.py | 42 ++++++++ tests/ro_crates.py | 12 +++ 6 files changed, 372 insertions(+) create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_no_description/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_no_endtime/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_no_name/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl index 3e6a2186..0d7ea57f 100644 --- a/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl +++ b/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl @@ -22,4 +22,28 @@ ro:ProcRCActionRecommended a sh:NodeShape ; sh:node rocrate:RootDataEntity ; sh:minCount 1 ; sh:message "The Action SHOULD be referenced from the Root Data Entity via mentions" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action name" ; + sh:description "The Action SHOULD have a name" ; + sh:path schema:name ; + sh:minCount 1 ; + sh:message "The Action SHOULD have a name" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action description" ; + sh:description "The Action SHOULD have a description" ; + sh:path schema:description ; + sh:minCount 1 ; + sh:message "The Action SHOULD have a description" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action endTime" ; + sh:description "The Action SHOULD have an endTime" ; + sh:path schema:endTime ; + sh:minCount 1 ; + sh:message "The Action SHOULD have an endTime" ; ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/action_no_description/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_no_description/ro-crate-metadata.json new file mode 100644 index 00000000..d707b86a --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_no_description/ro-crate-metadata.json @@ -0,0 +1,98 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@id": "#SepiaConversion_1", + "@type": "CreateAction", + "name": "Convert dog image to sepia", + "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" + } + }, + { + "@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/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/invalid/3_process_run_crate/action_no_endtime/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_no_endtime/ro-crate-metadata.json new file mode 100644 index 00000000..e6321d60 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_no_endtime/ro-crate-metadata.json @@ -0,0 +1,98 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "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" + } + }, + { + "@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/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/invalid/3_process_run_crate/action_no_name/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_no_name/ro-crate-metadata.json new file mode 100644 index 00000000..2023bbf3 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_no_name/ro-crate-metadata.json @@ -0,0 +1,98 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@id": "#SepiaConversion_1", + "@type": "CreateAction", + "description": "convert -sepia-tone 80% pics/2017-06-11\\ 12.56.14.jpg pics/sepia_fence.jpg", + "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" + } + }, + { + "@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/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/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index 85fc37db..ca18ca90 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -50,3 +50,45 @@ def test_prc_action_not_mentioned(): ["The Action SHOULD be referenced from the Root Data Entity via mentions"], profile_name="s_process-run-crate" ) + + +def test_prc_action_no_name(): + """\ + Test a Process Run Crate where the action does not have an name. + """ + do_entity_test( + InvalidProcRC().action_no_name, + Severity.RECOMMENDED, + False, + ["Process Run Crate Action SHOULD"], + ["The Action SHOULD have a name"], + profile_name="s_process-run-crate" + ) + + +def test_prc_action_no_description(): + """\ + Test a Process Run Crate where the action does not have a description. + """ + do_entity_test( + InvalidProcRC().action_no_description, + Severity.RECOMMENDED, + False, + ["Process Run Crate Action SHOULD"], + ["The Action SHOULD have a description"], + profile_name="s_process-run-crate" + ) + + +def test_prc_action_no_endtime(): + """\ + Test a Process Run Crate where the action does not have an endTime. + """ + do_entity_test( + InvalidProcRC().action_no_endtime, + Severity.RECOMMENDED, + False, + ["Process Run Crate Action SHOULD"], + ["The Action SHOULD have an endTime"], + profile_name="s_process-run-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 9f53cb72..5ee91af5 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -305,3 +305,15 @@ def action_instrument_bad_type(self) -> Path: @property def action_not_mentioned(self) -> Path: return self.base_path / "action_not_mentioned" + + @property + def action_no_name(self) -> Path: + return self.base_path / "action_no_name" + + @property + def action_no_description(self) -> Path: + return self.base_path / "action_no_description" + + @property + def action_no_endtime(self) -> Path: + return self.base_path / "action_no_endtime" From eff6f78faffb06ac6ad5d1048e4621c4785d2cdc Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 27 Jun 2024 11:12:52 +0200 Subject: [PATCH 630/902] procrc: add check for datetime format --- .../should/1_create_action.ttl | 5 +- .../action_bad_endtime/ro-crate-metadata.json | 99 +++++++++++++++++++ .../process-run-crate/test_prc_action.py | 16 ++- tests/ro_crates.py | 4 + 4 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_bad_endtime/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl index 0d7ea57f..814d617b 100644 --- a/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl +++ b/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl @@ -42,8 +42,9 @@ ro:ProcRCActionRecommended a sh:NodeShape ; sh:property [ a sh:PropertyShape ; sh:name "Action endTime" ; - sh:description "The Action SHOULD have an endTime" ; + sh:description "The Action SHOULD have an endTime in ISO 8601 format" ; sh:path schema:endTime ; + sh:pattern "^(\\d{4}-\\d{2}-\\d{2})(T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?\\+\\d{2}:\\d{2})?$" ; sh:minCount 1 ; - sh:message "The Action SHOULD have an endTime" ; + sh:message "The Action SHOULD have an endTime in ISO 8601 format" ; ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/action_bad_endtime/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_bad_endtime/ro-crate-metadata.json new file mode 100644 index 00000000..cb643d09 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_bad_endtime/ro-crate-metadata.json @@ -0,0 +1,99 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "endTime": "May 17 2024", + "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" + } + }, + { + "@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/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/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index ca18ca90..c5b9f255 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -89,6 +89,20 @@ def test_prc_action_no_endtime(): Severity.RECOMMENDED, False, ["Process Run Crate Action SHOULD"], - ["The Action SHOULD have an endTime"], + ["The Action SHOULD have an endTime in ISO 8601 format"], + profile_name="s_process-run-crate" + ) + + +def test_prc_action_bad_endtime(): + """\ + Test a Process Run Crate where the action does not have an endTime. + """ + do_entity_test( + InvalidProcRC().action_bad_endtime, + Severity.RECOMMENDED, + False, + ["Process Run Crate Action SHOULD"], + ["The Action SHOULD have an endTime in ISO 8601 format"], profile_name="s_process-run-crate" ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 5ee91af5..70f29781 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -317,3 +317,7 @@ def action_no_description(self) -> Path: @property def action_no_endtime(self) -> Path: return self.base_path / "action_no_endtime" + + @property + def action_bad_endtime(self) -> Path: + return self.base_path / "action_bad_endtime" From db11bb0c28230546e3b683e176b72b0bc245401a Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 27 Jun 2024 12:35:40 +0200 Subject: [PATCH 631/902] procrc: add check for agent --- .../should/1_create_action.ttl | 12 +++ .../action_bad_agent/ro-crate-metadata.json | 99 +++++++++++++++++++ .../action_no_agent/ro-crate-metadata.json | 91 +++++++++++++++++ .../process-run-crate/test_prc_action.py | 29 ++++++ tests/ro_crates.py | 8 ++ 5 files changed, 239 insertions(+) create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_bad_agent/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_no_agent/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl index 814d617b..140e565b 100644 --- a/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl +++ b/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl @@ -47,4 +47,16 @@ ro:ProcRCActionRecommended a sh:NodeShape ; sh:pattern "^(\\d{4}-\\d{2}-\\d{2})(T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?\\+\\d{2}:\\d{2})?$" ; sh:minCount 1 ; sh:message "The Action SHOULD have an endTime in ISO 8601 format" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action agent" ; + sh:description "The Action SHOULD have an agent that is a Person or Organization" ; + sh:path schema:agent ; + sh:or ( + [ sh:class schema:Person ; ] + [ sh:class schema:Organization ; ] + ) ; + sh:minCount 1 ; + sh:message "The Action SHOULD have an agent that is a Person or Organization" ; ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/action_bad_agent/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_bad_agent/ro-crate-metadata.json new file mode 100644 index 00000000..b0f92114 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_bad_agent/ro-crate-metadata.json @@ -0,0 +1,99 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "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://example.com/foo" + } + }, + { + "@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/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://example.com/foo", + "@type": "MedicalEntity", + "name": "Foo" + } + ] +} diff --git a/tests/data/crates/invalid/3_process_run_crate/action_no_agent/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_no_agent/ro-crate-metadata.json new file mode 100644 index 00000000..8ed91563 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_no_agent/ro-crate-metadata.json @@ -0,0 +1,91 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "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" + } + }, + { + "@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/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" + } + ] +} diff --git a/tests/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index c5b9f255..5e196ae7 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -106,3 +106,32 @@ def test_prc_action_bad_endtime(): ["The Action SHOULD have an endTime in ISO 8601 format"], profile_name="s_process-run-crate" ) + + +def test_prc_action_no_agent(): + """\ + Test a Process Run Crate where the action does not have an agent. + """ + do_entity_test( + InvalidProcRC().action_no_agent, + Severity.RECOMMENDED, + False, + ["Process Run Crate Action SHOULD"], + ["The Action SHOULD have an agent that is a Person or Organization"], + profile_name="s_process-run-crate" + ) + + +def test_prc_action_bad_agent(): + """\ + Test a Process Run Crate where the agent is neither a Person nor an + Organization. + """ + do_entity_test( + InvalidProcRC().action_bad_agent, + Severity.RECOMMENDED, + False, + ["Process Run Crate Action SHOULD"], + ["The Action SHOULD have an agent that is a Person or Organization"], + profile_name="s_process-run-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 70f29781..6b6aad55 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -321,3 +321,11 @@ def action_no_endtime(self) -> Path: @property def action_bad_endtime(self) -> Path: return self.base_path / "action_bad_endtime" + + @property + def action_no_agent(self) -> Path: + return self.base_path / "action_no_agent" + + @property + def action_bad_agent(self) -> Path: + return self.base_path / "action_bad_agent" From dfef1098d849013d75641671f8ce080459e67f7e Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 27 Jun 2024 13:17:06 +0200 Subject: [PATCH 632/902] procrc: add check for result --- .../should/1_create_action.ttl | 14 +++ .../action_no_result/ro-crate-metadata.json | 86 +++++++++++++++++++ .../process-run-crate/test_prc_action.py | 15 ++++ tests/ro_crates.py | 4 + 4 files changed, 119 insertions(+) create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_no_result/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl index 140e565b..b6886696 100644 --- a/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl +++ b/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl @@ -60,3 +60,17 @@ ro:ProcRCActionRecommended a sh:NodeShape ; sh:minCount 1 ; sh:message "The Action SHOULD have an agent that is a Person or Organization" ; ] . + +ro:ProcRCCreateUpdateActionRecommended a sh:NodeShape ; + sh:name "Process Run Crate CreateAction UpdateAction SHOULD" ; + sh:description "Recommended properties of the Process Run Crate CreateAction or UpdateAction" ; + sh:targetClass schema:CreateAction , + schema:UpdateAction ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action result" ; + sh:description "The Action SHOULD have a result" ; + sh:path schema:result ; + sh:minCount 1 ; + sh:message "The Action SHOULD have a result" ; + ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/action_no_result/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_no_result/ro-crate-metadata.json new file mode 100644 index 00000000..97e1662e --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_no_result/ro-crate-metadata.json @@ -0,0 +1,86 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "endTime": "2024-05-17T01:04:52+01:00", + "instrument": { + "@id": "https://www.imagemagick.org/" + }, + "object": { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + "agent": { + "@id": "https://orcid.org/0000-0001-9842-9718" + } + }, + { + "@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": "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/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index 5e196ae7..55964559 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -135,3 +135,18 @@ def test_prc_action_bad_agent(): ["The Action SHOULD have an agent that is a Person or Organization"], profile_name="s_process-run-crate" ) + + +def test_prc_action_no_result(): + """\ + Test a Process Run Crate where the CreateAction or UpdateAction does not + have a result. + """ + do_entity_test( + InvalidProcRC().action_no_result, + Severity.RECOMMENDED, + False, + ["Process Run Crate CreateAction UpdateAction SHOULD"], + ["The Action SHOULD have a result"], + profile_name="s_process-run-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 6b6aad55..26d58cc8 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -329,3 +329,7 @@ def action_no_agent(self) -> Path: @property def action_bad_agent(self) -> Path: return self.base_path / "action_bad_agent" + + @property + def action_no_result(self) -> Path: + return self.base_path / "action_no_result" From 6b814fe66b6fcda447c3403fa843b71eebd0f3ec Mon Sep 17 00:00:00 2001 From: simleo Date: Fri, 28 Jun 2024 11:39:30 +0200 Subject: [PATCH 633/902] process run crate: add profile.ttl file --- .../must/1_create_action.ttl | 0 .../must/2_root_data_entity_metadata.ttl | 0 .../profiles/process-run-crate/profile.ttl | 67 +++++++++++++++++++ .../should/0_software-application.ttl | 0 .../should/1_create_action.ttl | 0 .../process-run-crate/test_prc_action.py | 20 +++--- .../process-run-crate/test_prc_application.py | 12 ++-- .../test_prc_root_data_entity.py | 6 +- .../process-run-crate/test_valid_prc.py | 2 +- .../workflow-ro-crate/test_valid_wroc.py | 1 - 10 files changed, 87 insertions(+), 21 deletions(-) rename rocrate_validator/profiles/{s_process-run-crate => process-run-crate}/must/1_create_action.ttl (100%) rename rocrate_validator/profiles/{s_process-run-crate => process-run-crate}/must/2_root_data_entity_metadata.ttl (100%) create mode 100644 rocrate_validator/profiles/process-run-crate/profile.ttl rename rocrate_validator/profiles/{s_process-run-crate => process-run-crate}/should/0_software-application.ttl (100%) rename rocrate_validator/profiles/{s_process-run-crate => process-run-crate}/should/1_create_action.ttl (100%) diff --git a/rocrate_validator/profiles/s_process-run-crate/must/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl similarity index 100% rename from rocrate_validator/profiles/s_process-run-crate/must/1_create_action.ttl rename to rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl diff --git a/rocrate_validator/profiles/s_process-run-crate/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl similarity index 100% rename from rocrate_validator/profiles/s_process-run-crate/must/2_root_data_entity_metadata.ttl rename to rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl diff --git a/rocrate_validator/profiles/process-run-crate/profile.ttl b/rocrate_validator/profiles/process-run-crate/profile.ttl new file mode 100644 index 00000000..45d13ad2 --- /dev/null +++ b/rocrate_validator/profiles/process-run-crate/profile.ttl @@ -0,0 +1,67 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Process Run Crate 0.5" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """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.1 profile + prof:isProfileOf ; + + # Explicitly state that this profile is a transitive profile of the RO-Crate Metadata Specification 1.1 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 refered 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 refered 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.1 + prof:isInheritedFrom ; + ] ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "process-run-crate" ; +. diff --git a/rocrate_validator/profiles/s_process-run-crate/should/0_software-application.ttl b/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl similarity index 100% rename from rocrate_validator/profiles/s_process-run-crate/should/0_software-application.ttl rename to rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl diff --git a/rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl similarity index 100% rename from rocrate_validator/profiles/s_process-run-crate/should/1_create_action.ttl rename to rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl diff --git a/tests/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index 55964559..fa9a16fb 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -18,7 +18,7 @@ def test_prc_action_no_instrument(): False, ["Process Run Crate Action"], ["The Action MUST have an instrument property that references the executed tool"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -33,7 +33,7 @@ def test_prc_action_instrument_bad_type(): False, ["Process Run Crate Action"], ["The Action MUST have an instrument property that references the executed tool"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -48,7 +48,7 @@ def test_prc_action_not_mentioned(): False, ["Process Run Crate Action SHOULD"], ["The Action SHOULD be referenced from the Root Data Entity via mentions"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -62,7 +62,7 @@ def test_prc_action_no_name(): False, ["Process Run Crate Action SHOULD"], ["The Action SHOULD have a name"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -76,7 +76,7 @@ def test_prc_action_no_description(): False, ["Process Run Crate Action SHOULD"], ["The Action SHOULD have a description"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -90,7 +90,7 @@ def test_prc_action_no_endtime(): False, ["Process Run Crate Action SHOULD"], ["The Action SHOULD have an endTime in ISO 8601 format"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -104,7 +104,7 @@ def test_prc_action_bad_endtime(): False, ["Process Run Crate Action SHOULD"], ["The Action SHOULD have an endTime in ISO 8601 format"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -118,7 +118,7 @@ def test_prc_action_no_agent(): False, ["Process Run Crate Action SHOULD"], ["The Action SHOULD have an agent that is a Person or Organization"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -133,7 +133,7 @@ def test_prc_action_bad_agent(): False, ["Process Run Crate Action SHOULD"], ["The Action SHOULD have an agent that is a Person or Organization"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -148,5 +148,5 @@ def test_prc_action_no_result(): False, ["Process Run Crate CreateAction UpdateAction SHOULD"], ["The Action SHOULD have a result"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) diff --git a/tests/integration/profiles/process-run-crate/test_prc_application.py b/tests/integration/profiles/process-run-crate/test_prc_application.py index c5a802b7..f81bcda5 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_application.py +++ b/tests/integration/profiles/process-run-crate/test_prc_application.py @@ -18,7 +18,7 @@ def test_prc_application_no_name(): False, ["ProcRC Application"], ["The Application SHOULD have a name"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -32,7 +32,7 @@ def test_prc_application_no_url(): False, ["ProcRC Application"], ["The Application SHOULD have a url"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -47,7 +47,7 @@ def test_prc_application_no_version(): False, ["ProcRC SoftwareApplication"], ["The SoftwareApplication SHOULD have a version or softwareVersion"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -62,7 +62,7 @@ def test_prc_application_version_softwareversion(): False, ["ProcRC SoftwareApplication SingleVersion"], ["Process Run Crate SoftwareApplication should not have both version and softwareVersion"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -77,7 +77,7 @@ def test_prc_softwaresourcecode_no_version(): False, ["ProcRC SoftwareSourceCode or ComputationalWorkflow"], ["The SoftwareSourceCode or ComputationalWorkflow SHOULD have a version"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -92,5 +92,5 @@ def test_prc_application_id_no_absoluteuri(): False, ["ProcRC SoftwareApplication ID"], ["The SoftwareApplication id SHOULD be an absolute URI"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) diff --git a/tests/integration/profiles/process-run-crate/test_prc_root_data_entity.py b/tests/integration/profiles/process-run-crate/test_prc_root_data_entity.py index a35b3eee..dcd68505 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_root_data_entity.py +++ b/tests/integration/profiles/process-run-crate/test_prc_root_data_entity.py @@ -19,7 +19,7 @@ def test_prc_no_conformsto(): False, ["Root Data Entity Metadata"], ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -34,7 +34,7 @@ def test_prc_conformsto_bad_type(): False, ["Root Data Entity Metadata"], ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) @@ -49,5 +49,5 @@ def test_prc_conformsto_bad_profile(): False, ["Root Data Entity Metadata"], ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile"], - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) diff --git a/tests/integration/profiles/process-run-crate/test_valid_prc.py b/tests/integration/profiles/process-run-crate/test_valid_prc.py index 518ee193..7440e262 100644 --- a/tests/integration/profiles/process-run-crate/test_valid_prc.py +++ b/tests/integration/profiles/process-run-crate/test_valid_prc.py @@ -13,5 +13,5 @@ def test_valid_process_run_crate_required(): ValidROC().process_run_crate, Severity.REQUIRED, True, - profile_name="s_process-run-crate" + profile_name="process-run-crate" ) 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 15a25847..e0fd9ce0 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py +++ b/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py @@ -8,7 +8,6 @@ logger = logging.getLogger(__name__) -@pytest.mark.xfail(reason="workflow ro-crate loaded after process run crate") def test_valid_workflow_roc_required(): """Test a valid Workflow RO-Crate.""" do_entity_test( From 681d830aa72ba5f9ddb11bf2b1bce00bbd172a0b Mon Sep 17 00:00:00 2001 From: simleo Date: Fri, 28 Jun 2024 12:49:10 +0200 Subject: [PATCH 634/902] procrc: add check for startTime --- .../process-run-crate/may/1_create_action.ttl | 25 +++++ .../ro-crate-metadata.json | 100 ++++++++++++++++++ .../ro-crate-metadata.json | 99 +++++++++++++++++ .../process-run-crate/ro-crate-metadata.json | 1 + .../process-run-crate/test_prc_action.py | 28 +++++ .../workflow-ro-crate/test_valid_wroc.py | 1 - tests/ro_crates.py | 8 ++ 7 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_bad_starttime/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_no_starttime/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl new file mode 100644 index 00000000..408bd0b8 --- /dev/null +++ b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl @@ -0,0 +1,25 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix rocrate: . +@prefix bioschemas: . + +ro:ProcRCActionOptional a sh:NodeShape ; + sh:name "Process Run Crate Action MAY" ; + sh:description "Recommended properties of the Process Run Crate Action" ; + sh:targetClass schema:CreateAction , + schema:ActivateAction , + schema:UpdateAction ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action startTime" ; + sh:description "The Action MAY have a startTime in ISO 8601 format" ; + sh:path schema:startTime ; + sh:pattern "^(\\d{4}-\\d{2}-\\d{2})(T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?\\+\\d{2}:\\d{2})?$" ; + sh:minCount 1 ; + sh:message "The Action MAY have a startTime in ISO 8601 format" ; + ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/action_bad_starttime/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_bad_starttime/ro-crate-metadata.json new file mode 100644 index 00000000..dd836838 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_bad_starttime/ro-crate-metadata.json @@ -0,0 +1,100 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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": "May 17 2024", + "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" + } + }, + { + "@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/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/invalid/3_process_run_crate/action_no_starttime/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_no_starttime/ro-crate-metadata.json new file mode 100644 index 00000000..0a02e2d6 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_no_starttime/ro-crate-metadata.json @@ -0,0 +1,99 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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", + "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" + } + }, + { + "@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/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/valid/process-run-crate/ro-crate-metadata.json b/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json index 0a02e2d6..76d86fa5 100644 --- a/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json +++ b/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json @@ -54,6 +54,7 @@ "@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/" diff --git a/tests/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index fa9a16fb..f40fb1da 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -150,3 +150,31 @@ def test_prc_action_no_result(): ["The Action SHOULD have a result"], profile_name="process-run-crate" ) + + +def test_prc_action_no_starttime(): + """\ + Test a Process Run Crate where the action does not have an startTime. + """ + do_entity_test( + InvalidProcRC().action_no_starttime, + Severity.OPTIONAL, + False, + ["Process Run Crate Action MAY"], + ["The Action MAY have a startTime in ISO 8601 format"], + profile_name="process-run-crate" + ) + + +def test_prc_action_bad_starttime(): + """\ + Test a Process Run Crate where the action does not have an startTime. + """ + do_entity_test( + InvalidProcRC().action_bad_starttime, + Severity.OPTIONAL, + False, + ["Process Run Crate Action MAY"], + ["The Action MAY have a startTime in ISO 8601 format"], + profile_name="process-run-crate" + ) 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 e0fd9ce0..2b092b94 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py +++ b/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py @@ -1,5 +1,4 @@ import logging -import pytest from rocrate_validator.models import Severity from tests.ro_crates import ValidROC diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 26d58cc8..500c5e67 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -333,3 +333,11 @@ def action_bad_agent(self) -> Path: @property def action_no_result(self) -> Path: return self.base_path / "action_no_result" + + @property + def action_no_starttime(self) -> Path: + return self.base_path / "action_no_starttime" + + @property + def action_bad_starttime(self) -> Path: + return self.base_path / "action_bad_starttime" From 7f1101ade527598d4952c1eb6664545e20ce105f Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 1 Jul 2024 13:45:14 +0200 Subject: [PATCH 635/902] change profile_name to profile_identifier --- .../process-run-crate/test_prc_action.py | 24 +++++++++---------- .../process-run-crate/test_prc_application.py | 12 +++++----- .../test_prc_root_data_entity.py | 6 ++--- .../process-run-crate/test_valid_prc.py | 2 +- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index f40fb1da..cab56ca0 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -18,7 +18,7 @@ def test_prc_action_no_instrument(): False, ["Process Run Crate Action"], ["The Action MUST have an instrument property that references the executed tool"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -33,7 +33,7 @@ def test_prc_action_instrument_bad_type(): False, ["Process Run Crate Action"], ["The Action MUST have an instrument property that references the executed tool"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -48,7 +48,7 @@ def test_prc_action_not_mentioned(): False, ["Process Run Crate Action SHOULD"], ["The Action SHOULD be referenced from the Root Data Entity via mentions"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -62,7 +62,7 @@ def test_prc_action_no_name(): False, ["Process Run Crate Action SHOULD"], ["The Action SHOULD have a name"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -76,7 +76,7 @@ def test_prc_action_no_description(): False, ["Process Run Crate Action SHOULD"], ["The Action SHOULD have a description"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -90,7 +90,7 @@ def test_prc_action_no_endtime(): False, ["Process Run Crate Action SHOULD"], ["The Action SHOULD have an endTime in ISO 8601 format"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -104,7 +104,7 @@ def test_prc_action_bad_endtime(): False, ["Process Run Crate Action SHOULD"], ["The Action SHOULD have an endTime in ISO 8601 format"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -118,7 +118,7 @@ def test_prc_action_no_agent(): False, ["Process Run Crate Action SHOULD"], ["The Action SHOULD have an agent that is a Person or Organization"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -133,7 +133,7 @@ def test_prc_action_bad_agent(): False, ["Process Run Crate Action SHOULD"], ["The Action SHOULD have an agent that is a Person or Organization"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -148,7 +148,7 @@ def test_prc_action_no_result(): False, ["Process Run Crate CreateAction UpdateAction SHOULD"], ["The Action SHOULD have a result"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -162,7 +162,7 @@ def test_prc_action_no_starttime(): False, ["Process Run Crate Action MAY"], ["The Action MAY have a startTime in ISO 8601 format"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -176,5 +176,5 @@ def test_prc_action_bad_starttime(): False, ["Process Run Crate Action MAY"], ["The Action MAY have a startTime in ISO 8601 format"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) diff --git a/tests/integration/profiles/process-run-crate/test_prc_application.py b/tests/integration/profiles/process-run-crate/test_prc_application.py index f81bcda5..9d8c4a51 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_application.py +++ b/tests/integration/profiles/process-run-crate/test_prc_application.py @@ -18,7 +18,7 @@ def test_prc_application_no_name(): False, ["ProcRC Application"], ["The Application SHOULD have a name"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -32,7 +32,7 @@ def test_prc_application_no_url(): False, ["ProcRC Application"], ["The Application SHOULD have a url"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -47,7 +47,7 @@ def test_prc_application_no_version(): False, ["ProcRC SoftwareApplication"], ["The SoftwareApplication SHOULD have a version or softwareVersion"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -62,7 +62,7 @@ def test_prc_application_version_softwareversion(): False, ["ProcRC SoftwareApplication SingleVersion"], ["Process Run Crate SoftwareApplication should not have both version and softwareVersion"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -77,7 +77,7 @@ def test_prc_softwaresourcecode_no_version(): False, ["ProcRC SoftwareSourceCode or ComputationalWorkflow"], ["The SoftwareSourceCode or ComputationalWorkflow SHOULD have a version"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -92,5 +92,5 @@ def test_prc_application_id_no_absoluteuri(): False, ["ProcRC SoftwareApplication ID"], ["The SoftwareApplication id SHOULD be an absolute URI"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) diff --git a/tests/integration/profiles/process-run-crate/test_prc_root_data_entity.py b/tests/integration/profiles/process-run-crate/test_prc_root_data_entity.py index dcd68505..874ee0ae 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_root_data_entity.py +++ b/tests/integration/profiles/process-run-crate/test_prc_root_data_entity.py @@ -19,7 +19,7 @@ def test_prc_no_conformsto(): False, ["Root Data Entity Metadata"], ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -34,7 +34,7 @@ def test_prc_conformsto_bad_type(): False, ["Root Data Entity Metadata"], ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) @@ -49,5 +49,5 @@ def test_prc_conformsto_bad_profile(): False, ["Root Data Entity Metadata"], ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile"], - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) diff --git a/tests/integration/profiles/process-run-crate/test_valid_prc.py b/tests/integration/profiles/process-run-crate/test_valid_prc.py index 7440e262..4d3f4c14 100644 --- a/tests/integration/profiles/process-run-crate/test_valid_prc.py +++ b/tests/integration/profiles/process-run-crate/test_valid_prc.py @@ -13,5 +13,5 @@ def test_valid_process_run_crate_required(): ValidROC().process_run_crate, Severity.REQUIRED, True, - profile_name="process-run-crate" + profile_identifier="process-run-crate" ) From 5ff936165bccd90ae0c8e237f0333746e04bd409 Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 1 Jul 2024 16:24:10 +0200 Subject: [PATCH 636/902] Added check for action with error and no FailedActionStatus --- .../process-run-crate/may/1_create_action.ttl | 24 +++++ .../ro-crate-metadata.json | 102 ++++++++++++++++++ .../process-run-crate/ro-crate-metadata.json | 4 +- .../process-run-crate/test_prc_action.py | 15 +++ tests/ro_crates.py | 4 + 5 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_error_not_failed_status/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl index 408bd0b8..0b8753ed 100644 --- a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl @@ -23,3 +23,27 @@ ro:ProcRCActionOptional a sh:NodeShape ; sh:minCount 1 ; sh:message "The Action MAY have a startTime in ISO 8601 format" ; ] . + +ro:ProcRCActionError a sh:NodeShape ; + sh:name "Process Run Crate Action error" ; + sh:description "error SHOULD NOT be specified unless actionStatus is set to FailedActionStatus" ; + sh:message "error SHOULD NOT be specified unless actionStatus is set to FailedActionStatus" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + { ?this a schema:CreateAction } UNION + { ?this a schema:ActivateAction } UNION + { ?this a schema:UpdateAction } . + ?this schema:actionStatus ?status . + FILTER(?status != schema:FailedActionStatus) + } + """ + ] ; + sh:not [ + a sh:PropertyShape ; + sh:path schema:error ; + sh:minCount 1 ; + ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/action_error_not_failed_status/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_error_not_failed_status/ro-crate-metadata.json new file mode 100644 index 00000000..aadf8849 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_error_not_failed_status/ro-crate-metadata.json @@ -0,0 +1,102 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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": { "@id": "http://schema.org/CompletedActionStatus" }, + "error": "this is just to test the error property" + }, + { + "@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/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/valid/process-run-crate/ro-crate-metadata.json b/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json index 76d86fa5..fdb8df2b 100644 --- a/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json +++ b/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json @@ -67,7 +67,9 @@ }, "agent": { "@id": "https://orcid.org/0000-0001-9842-9718" - } + }, + "actionStatus": { "@id": "http://schema.org/FailedActionStatus" }, + "error": "this is just to test the error property" }, { "@id": "pics/2017-06-11%2012.56.14.jpg", diff --git a/tests/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index cab56ca0..0e865b52 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -178,3 +178,18 @@ def test_prc_action_bad_starttime(): ["The Action MAY have a startTime in ISO 8601 format"], profile_identifier="process-run-crate" ) + + +def test_prc_action_error_not_failed_status(): + """\ + Test a Process Run Crate where the action has an error even though its + actionStatus is not FailedActionStatus. + """ + do_entity_test( + InvalidProcRC().action_error_not_failed_status, + Severity.OPTIONAL, + False, + ["Process Run Crate Action error"], + ["error SHOULD NOT be specified unless actionStatus is set to FailedActionStatus"], + profile_identifier="process-run-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 500c5e67..f033de2d 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -341,3 +341,7 @@ def action_no_starttime(self) -> Path: @property def action_bad_starttime(self) -> Path: return self.base_path / "action_bad_starttime" + + @property + def action_error_not_failed_status(self) -> Path: + return self.base_path / "action_error_not_failed_status" From 4793cceaf2313b1ef0642f7259793dae09275539 Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 10 Jul 2024 12:09:42 +0200 Subject: [PATCH 637/902] fix checks for action starttime --- .../profiles/process-run-crate/may/1_create_action.ttl | 5 ++--- .../process-run-crate/should/1_create_action.ttl | 9 +++++++++ .../profiles/process-run-crate/test_prc_action.py | 8 ++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl index 0b8753ed..65684606 100644 --- a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl @@ -17,11 +17,10 @@ ro:ProcRCActionOptional a sh:NodeShape ; sh:property [ a sh:PropertyShape ; sh:name "Action startTime" ; - sh:description "The Action MAY have a startTime in ISO 8601 format" ; + sh:description "The Action MAY have a startTime" ; sh:path schema:startTime ; - sh:pattern "^(\\d{4}-\\d{2}-\\d{2})(T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?\\+\\d{2}:\\d{2})?$" ; sh:minCount 1 ; - sh:message "The Action MAY have a startTime in ISO 8601 format" ; + sh:message "The Action MAY have a startTime" ; ] . ro:ProcRCActionError a sh:NodeShape ; diff --git a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl index b6886696..5e1a58fa 100644 --- a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl @@ -48,6 +48,15 @@ ro:ProcRCActionRecommended a sh:NodeShape ; sh:minCount 1 ; sh:message "The Action SHOULD have an endTime in ISO 8601 format" ; ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action startTime" ; + sh:description "If present, the Action startTime SHOULD be in ISO 8601 format" ; + sh:path schema:startTime ; + sh:pattern "^(\\d{4}-\\d{2}-\\d{2})(T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?\\+\\d{2}:\\d{2})?$" ; + sh:minCount 0 ; + sh:message "If present, the Action startTime SHOULD be in ISO 8601 format" ; + ] ; sh:property [ a sh:PropertyShape ; sh:name "Action agent" ; diff --git a/tests/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index 0e865b52..a6d47cc5 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -161,7 +161,7 @@ def test_prc_action_no_starttime(): Severity.OPTIONAL, False, ["Process Run Crate Action MAY"], - ["The Action MAY have a startTime in ISO 8601 format"], + ["The Action MAY have a startTime"], profile_identifier="process-run-crate" ) @@ -172,10 +172,10 @@ def test_prc_action_bad_starttime(): """ do_entity_test( InvalidProcRC().action_bad_starttime, - Severity.OPTIONAL, + Severity.RECOMMENDED, False, - ["Process Run Crate Action MAY"], - ["The Action MAY have a startTime in ISO 8601 format"], + ["Process Run Crate Action SHOULD"], + ["If present, the Action startTime SHOULD be in ISO 8601 format"], profile_identifier="process-run-crate" ) From 3e7072b9f171b8e3282497a5ef8caca72f25ea2a Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 10 Jul 2024 14:51:36 +0200 Subject: [PATCH 638/902] procrc: add check for action object and actionstatus --- .../process-run-crate/may/1_create_action.ttl | 16 +++ .../should/1_create_action.ttl | 12 +++ .../ro-crate-metadata.json | 102 ++++++++++++++++++ .../ro-crate-metadata.json | 101 +++++++++++++++++ .../action_no_object/ro-crate-metadata.json | 86 +++++++++++++++ .../process-run-crate/test_prc_action.py | 42 ++++++++ tests/ro_crates.py | 12 +++ 7 files changed, 371 insertions(+) create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_bad_actionstatus/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_no_actionstatus/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_no_object/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl index 65684606..bcd0768a 100644 --- a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl @@ -21,6 +21,22 @@ ro:ProcRCActionOptional a sh:NodeShape ; sh:path schema:startTime ; sh:minCount 1 ; sh:message "The Action MAY have a startTime" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action object" ; + sh:description "The Action MAY have an object" ; + sh:path schema:object ; + sh:minCount 1 ; + sh:message "The Action MAY have an object" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action actionStatus" ; + sh:description "The Action MAY have an actionStatus" ; + sh:path schema:actionStatus ; + sh:minCount 1 ; + sh:message "The Action MAY have an actionStatus" ; ] . ro:ProcRCActionError a sh:NodeShape ; diff --git a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl index 5e1a58fa..3c94fee0 100644 --- a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl @@ -68,6 +68,18 @@ ro:ProcRCActionRecommended a sh:NodeShape ; ) ; sh:minCount 1 ; sh:message "The Action SHOULD have an agent that is a Person or Organization" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action actionStatus" ; + sh:description "If the Action has an actionStatus, it should be http://schema.org/CompletedActionStatus or http://schema.org/FailedActionStatus" ; + sh:path schema:actionStatus ; + sh:or ( + [ sh:hasValue schema:CompletedActionStatus ; ] + [ sh:hasValue schema:FailedActionStatus ; ] + ) ; + sh:minCount 0 ; + sh:message "If the Action has an actionStatus, it should be http://schema.org/CompletedActionStatus or http://schema.org/FailedActionStatus" ; ] . ro:ProcRCCreateUpdateActionRecommended a sh:NodeShape ; diff --git a/tests/data/crates/invalid/3_process_run_crate/action_bad_actionstatus/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_bad_actionstatus/ro-crate-metadata.json new file mode 100644 index 00000000..e0015f1b --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_bad_actionstatus/ro-crate-metadata.json @@ -0,0 +1,102 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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": { "@id": "http://schema.org/Integer" }, + "error": "this is just to test the error property" + }, + { + "@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/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/invalid/3_process_run_crate/action_no_actionstatus/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_no_actionstatus/ro-crate-metadata.json new file mode 100644 index 00000000..9d4e8cbd --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_no_actionstatus/ro-crate-metadata.json @@ -0,0 +1,101 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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" + }, + "error": "this is just to test the error property" + }, + { + "@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/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/invalid/3_process_run_crate/action_no_object/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_no_object/ro-crate-metadata.json new file mode 100644 index 00000000..a4df0b94 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_no_object/ro-crate-metadata.json @@ -0,0 +1,86 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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/" + }, + "result": { + "@id": "pics/sepia_fence.jpg" + }, + "agent": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "actionStatus": { "@id": "http://schema.org/FailedActionStatus" }, + "error": "this is just to test the error property" + }, + { + "@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/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index a6d47cc5..4ef5fecd 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -193,3 +193,45 @@ def test_prc_action_error_not_failed_status(): ["error SHOULD NOT be specified unless actionStatus is set to FailedActionStatus"], profile_identifier="process-run-crate" ) + + +def test_prc_action_no_object(): + """\ + Test a Process Run Crate where the Action does not have an object. + """ + do_entity_test( + InvalidProcRC().action_no_object, + Severity.OPTIONAL, + False, + ["Process Run Crate Action MAY"], + ["The Action MAY have an object"], + profile_identifier="process-run-crate" + ) + + +def test_prc_action_no_actionstatus(): + """\ + Test a Process Run Crate where the Action does not have an actionstatus. + """ + do_entity_test( + InvalidProcRC().action_no_actionstatus, + Severity.OPTIONAL, + False, + ["Process Run Crate Action MAY"], + ["The Action MAY have an actionStatus"], + profile_identifier="process-run-crate" + ) + + +def test_prc_action_bad_actionstatus(): + """\ + Test a Process Run Crate where the Action has an invalid actionstatus. + """ + do_entity_test( + InvalidProcRC().action_bad_actionstatus, + Severity.RECOMMENDED, + False, + ["Process Run Crate Action SHOULD"], + ["If the Action has an actionStatus, it should be http://schema.org/CompletedActionStatus or http://schema.org/FailedActionStatus"], + profile_identifier="process-run-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index f033de2d..3744f7ca 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -345,3 +345,15 @@ def action_bad_starttime(self) -> Path: @property def action_error_not_failed_status(self) -> Path: return self.base_path / "action_error_not_failed_status" + + @property + def action_no_object(self) -> Path: + return self.base_path / "action_no_object" + + @property + def action_no_actionstatus(self) -> Path: + return self.base_path / "action_no_actionstatus" + + @property + def action_bad_actionstatus(self) -> Path: + return self.base_path / "action_bad_actionstatus" From a07b04f8758ba1477f982737459c6036b2445c1a Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 10 Jul 2024 16:21:21 +0200 Subject: [PATCH 639/902] fix checks for action error with no failedactionstatus --- .../process-run-crate/may/1_create_action.ttl | 24 ----- .../should/1_create_action.ttl | 26 +++++ .../ro-crate-metadata.json | 101 ++++++++++++++++++ .../process-run-crate/test_prc_action.py | 17 ++- tests/ro_crates.py | 4 + 5 files changed, 147 insertions(+), 25 deletions(-) create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_error_no_status/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl index bcd0768a..2e881076 100644 --- a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl @@ -38,27 +38,3 @@ ro:ProcRCActionOptional a sh:NodeShape ; sh:minCount 1 ; sh:message "The Action MAY have an actionStatus" ; ] . - -ro:ProcRCActionError a sh:NodeShape ; - sh:name "Process Run Crate Action error" ; - sh:description "error SHOULD NOT be specified unless actionStatus is set to FailedActionStatus" ; - sh:message "error SHOULD NOT be specified unless actionStatus is set to FailedActionStatus" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - { ?this a schema:CreateAction } UNION - { ?this a schema:ActivateAction } UNION - { ?this a schema:UpdateAction } . - ?this schema:actionStatus ?status . - FILTER(?status != schema:FailedActionStatus) - } - """ - ] ; - sh:not [ - a sh:PropertyShape ; - sh:path schema:error ; - sh:minCount 1 ; - ] . diff --git a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl index 3c94fee0..f18cc47c 100644 --- a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl @@ -95,3 +95,29 @@ ro:ProcRCCreateUpdateActionRecommended a sh:NodeShape ; sh:minCount 1 ; sh:message "The Action SHOULD have a result" ; ] . + + +ro:ProcRCActionError a sh:NodeShape ; + sh:name "Process Run Crate Action error" ; + sh:description "error SHOULD NOT be specified unless actionStatus is set to FailedActionStatus" ; + sh:message "error SHOULD NOT be specified unless actionStatus is set to FailedActionStatus" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + { ?this a schema:CreateAction } UNION + { ?this a schema:ActivateAction } UNION + { ?this a schema:UpdateAction } . + { FILTER NOT EXISTS { ?this schema:actionStatus ?status } } UNION + { ?this schema:actionStatus ?status . + FILTER(?status != schema:FailedActionStatus) } + } + """ + ] ; + sh:not [ + a sh:PropertyShape ; + sh:path schema:error ; + sh:minCount 1 ; + ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/action_error_no_status/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_error_no_status/ro-crate-metadata.json new file mode 100644 index 00000000..9d4e8cbd --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_error_no_status/ro-crate-metadata.json @@ -0,0 +1,101 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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" + }, + "error": "this is just to test the error property" + }, + { + "@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/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/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index 4ef5fecd..585c5061 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -187,7 +187,22 @@ def test_prc_action_error_not_failed_status(): """ do_entity_test( InvalidProcRC().action_error_not_failed_status, - Severity.OPTIONAL, + Severity.RECOMMENDED, + False, + ["Process Run Crate Action error"], + ["error SHOULD NOT be specified unless actionStatus is set to FailedActionStatus"], + profile_identifier="process-run-crate" + ) + + +def test_prc_action_error_no_status(): + """\ + Test a Process Run Crate where the action has an error even though it has + no actionStatus. + """ + do_entity_test( + InvalidProcRC().action_error_no_status, + Severity.RECOMMENDED, False, ["Process Run Crate Action error"], ["error SHOULD NOT be specified unless actionStatus is set to FailedActionStatus"], diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 3744f7ca..6e8fbb26 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -346,6 +346,10 @@ def action_bad_starttime(self) -> Path: def action_error_not_failed_status(self) -> Path: return self.base_path / "action_error_not_failed_status" + @property + def action_error_no_status(self) -> Path: + return self.base_path / "action_error_no_status" + @property def action_no_object(self) -> Path: return self.base_path / "action_no_object" From f1180a5ab0eebb1302895b89a68b7cfeb1b41f52 Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 11 Jul 2024 14:25:13 +0200 Subject: [PATCH 640/902] add check for action with no error --- .../process-run-crate/may/1_create_action.ttl | 25 +++++ .../action_no_error/ro-crate-metadata.json | 101 ++++++++++++++++++ .../process-run-crate/test_prc_action.py | 14 +++ tests/ro_crates.py | 4 + 4 files changed, 144 insertions(+) create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_no_error/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl index 2e881076..fc59f655 100644 --- a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl @@ -38,3 +38,28 @@ ro:ProcRCActionOptional a sh:NodeShape ; sh:minCount 1 ; sh:message "The Action MAY have an actionStatus" ; ] . + + +ro:ProcRCActionErrorMay a sh:NodeShape ; + sh:name "Process Run Crate Action MAY have error" ; + sh:description "error MAY be specified if actionStatus is set to FailedActionStatus" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + { ?this a schema:CreateAction } UNION + { ?this a schema:ActivateAction } UNION + { ?this a schema:UpdateAction } . + ?this schema:actionStatus ?status . + FILTER(?status = schema:FailedActionStatus) . + } + """ + ] ; + sh:property [ + a sh:PropertyShape ; + sh:path schema:error ; + sh:minCount 1 ; + sh:message "error MAY be specified if actionStatus is set to FailedActionStatus" ; + ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/action_no_error/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_no_error/ro-crate-metadata.json new file mode 100644 index 00000000..c57045e9 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_no_error/ro-crate-metadata.json @@ -0,0 +1,101 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@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": { "@id": "http://schema.org/FailedActionStatus" } + }, + { + "@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/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/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index 585c5061..65bc0bda 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -250,3 +250,17 @@ def test_prc_action_bad_actionstatus(): ["If the Action has an actionStatus, it should be http://schema.org/CompletedActionStatus or http://schema.org/FailedActionStatus"], profile_identifier="process-run-crate" ) + + +def test_prc_action_no_error(): + """\ + Test a Process Run Crate where the Action does not have an error. + """ + do_entity_test( + InvalidProcRC().action_no_error, + Severity.OPTIONAL, + False, + ["Process Run Crate Action MAY have error"], + ["error MAY be specified if actionStatus is set to FailedActionStatus"], + profile_identifier="process-run-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 6e8fbb26..d13134ea 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -361,3 +361,7 @@ def action_no_actionstatus(self) -> Path: @property def action_bad_actionstatus(self) -> Path: return self.base_path / "action_bad_actionstatus" + + @property + def action_no_error(self) -> Path: + return self.base_path / "action_no_error" From ba9b802bea79612c3bb13070e634875aed89746d Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 16 Jul 2024 12:32:35 +0200 Subject: [PATCH 641/902] allow crate to conformsTo more than just procrc --- .../must/2_root_data_entity_metadata.ttl | 6 +++++- .../process-run-crate/ro-crate-metadata.json | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl index 0d38b9be..de38931f 100644 --- a/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl @@ -17,7 +17,11 @@ ro:ProcRCRootDataEntityMetadata a sh:NodeShape ; sh:description "The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile" ; sh:path dct:conformsTo ; sh:class schema:CreativeWork; - sh:pattern "^https://w3id.org/ro/wfrun/process/.*" ; + # At least one value of conformsTo must match the pattern + sh:qualifiedValueShape [ + sh:pattern "^https://w3id.org/ro/wfrun/process/.*" ; + ] ; + sh:qualifiedMinCount 1 ; sh:minCount 1 ; sh:message "The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile" ; ] . diff --git a/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json b/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json index fdb8df2b..a2a5cc68 100644 --- a/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json +++ b/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json @@ -14,9 +14,14 @@ { "@id": "./", "@type": "Dataset", - "conformsTo": { - "@id": "https://w3id.org/ro/wfrun/process/0.5" - }, + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], "hasPart": [ { "@id": "pics/2017-06-11%2012.56.14.jpg" @@ -42,6 +47,12 @@ "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", From 130061dc3187b8232c8f0837cf388fa1ce1f8e65 Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 16 Jul 2024 14:46:24 +0200 Subject: [PATCH 642/902] procrc: add constraint on object and result entities type --- .../should/1_create_action.ttl | 22 ++++ .../ro-crate-metadata.json | 113 ++++++++++++++++++ .../process-run-crate/test_prc_action.py | 16 +++ tests/ro_crates.py | 4 + 4 files changed, 155 insertions(+) create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_obj_res_bad_type/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl index f18cc47c..b43c6614 100644 --- a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl @@ -121,3 +121,25 @@ ro:ProcRCActionError a sh:NodeShape ; sh:path schema:error ; sh:minCount 1 ; ] . + + +ro:ProcRCActionObjectResultType a sh:NodeShape ; + sh:name "Process Run Crate Action object and result types" ; + sh:description "object and result SHOULD point to entities of type MediaObject, Dataset, Collection, CreativeWork or PropertyValue" ; + sh:targetClass schema:CreateAction , + schema:ActivateAction , + schema:UpdateAction ; + sh:property [ + a sh:PropertyShape ; + sh:path [ + sh:alternativePath (schema:object schema:result) ; + ] ; + sh:or ( + [ sh:class schema:MediaObject ; ] + [ sh:class schema:Dataset ; ] + [ sh:class schema:Collection ; ] + [ sh:class schema:CreativeWork ; ] + [ sh:class schema:PropertyValue ; ] + ); + sh:message "object and result SHOULD point to entities of type MediaObject, Dataset, Collection, CreativeWork or PropertyValue" ; + ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/action_obj_res_bad_type/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_obj_res_bad_type/ro-crate-metadata.json new file mode 100644 index 00000000..0319c70f --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_obj_res_bad_type/ro-crate-metadata.json @@ -0,0 +1,113 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@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" + }, + { + "@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": "https://orcid.org/0000-0001-9842-9718" + }, + "agent": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "actionStatus": { "@id": "http://schema.org/FailedActionStatus" }, + "error": "this is just to test the error property" + }, + { + "@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/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/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index 65bc0bda..eb02ecdf 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -264,3 +264,19 @@ def test_prc_action_no_error(): ["error MAY be specified if actionStatus is set to FailedActionStatus"], profile_identifier="process-run-crate" ) + + +def test_prc_action_obj_res_bad_type(): + """\ + Test a Process Run Crate where the Action's object or result does not + point to a MediaObject, Dataset, Collection, CreativeWork or + PropertyValue. + """ + do_entity_test( + InvalidProcRC().action_obj_res_bad_type, + Severity.RECOMMENDED, + False, + ["Process Run Crate Action object and result types"], + ["object and result SHOULD point to entities of type MediaObject, Dataset, Collection, CreativeWork or PropertyValue"], + profile_identifier="process-run-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index d13134ea..646d9c55 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -365,3 +365,7 @@ def action_bad_actionstatus(self) -> Path: @property def action_no_error(self) -> Path: return self.base_path / "action_no_error" + + @property + def action_obj_res_bad_type(self) -> Path: + return self.base_path / "action_obj_res_bad_type" From a8ba2b86ea421b231abcbba5911001b09788406b Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 18 Jul 2024 12:47:56 +0200 Subject: [PATCH 643/902] procrc: add checks for collections --- .../process-run-crate/should/2_collection.ttl | 53 +++++++ .../ro-crate-metadata.json | 120 ++++++++++++++++ .../ro-crate-metadata.json | 130 +++++++++++++++++ .../ro-crate-metadata.json | 125 +++++++++++++++++ .../ro-crate-metadata.json | 131 ++++++++++++++++++ .../process-run-crate/test_prc_collection.py | 51 +++++++ .../process-run-crate/test_valid_prc.py | 6 + tests/ro_crates.py | 16 +++ 8 files changed, 632 insertions(+) create mode 100644 rocrate_validator/profiles/process-run-crate/should/2_collection.ttl create mode 100644 tests/data/crates/invalid/3_process_run_crate/collection_no_haspart/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/collection_no_mainentity/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/collection_not_mentioned/ro-crate-metadata.json create mode 100644 tests/data/crates/valid/process-run-crate-collections/ro-crate-metadata.json create mode 100644 tests/integration/profiles/process-run-crate/test_prc_collection.py diff --git a/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl b/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl new file mode 100644 index 00000000..2c5eefad --- /dev/null +++ b/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl @@ -0,0 +1,53 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix rocrate: . +@prefix bioschemas: . + +ro:ProcRCCollectionRecommended a sh:NodeShape ; + sh:name "Process Run Crate Collection SHOULD" ; + sh:description "Recommended properties of the Process Run Crate Collection" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:Collection . + { ?action schema:object ?this } UNION + { ?action schema:result ?this } . + { ?action a schema:CreateAction } UNION + { ?action a schema:ActivateAction } UNION + { ?action a schema:UpdateAction } . + } + """ + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Collection SHOULD be referenced via mentions from root" ; + sh:description "The Collection SHOULD be referenced from the Root Data Entity via mentions" ; + sh:path [ sh:inversePath schema:mentions ] ; + sh:node rocrate:RootDataEntity ; + sh:minCount 1 ; + sh:message "The Collection SHOULD be referenced from the Root Data Entity via mentions" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Collection hasPart" ; + sh:description "The Collection SHOULD have a hasPart" ; + sh:path schema:hasPart ; + sh:minCount 1 ; + sh:message "The Collection SHOULD have a hasPart" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Collection mainEntity" ; + sh:description "The Collection SHOULD have a mainEntity" ; + sh:path schema:mainEntity ; + sh:minCount 1 ; + sh:message "The Collection SHOULD have a mainEntity" ; + ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/collection_no_haspart/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/collection_no_haspart/ro-crate-metadata.json new file mode 100644 index 00000000..4305b905 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/collection_no_haspart/ro-crate-metadata.json @@ -0,0 +1,120 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + } + ], + "hasPart": [ + { + "@id": "pics/in_01.jpg" + }, + { + "@id": "pics/in_02.jpg" + }, + { + "@id": "pics/in_main.jpg" + }, + { + "@id": "pics/out_01.jpg" + }, + { + "@id": "pics/out_02.jpg" + }, + { + "@id": "pics/out_main.jpg" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0" + }, + "mentions": [ + { + "@id": "#Conversion" + }, + { + "@id": "#InCollection" + }, + { + "@id": "#OutCollection" + } + ], + "name": "Test Collections" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@id": "#Conversion", + "@type": "CreateAction", + "name": "Convert image collections", + "description": "Convert image collections", + "startTime": "2024-05-17T01:04:50+01:00", + "endTime": "2024-05-17T01:04:52+01:00", + "instrument": { + "@id": "https://www.imagemagick.org/" + }, + "object": { + "@id": "#InCollection" + }, + "result": { + "@id": "#OutCollection" + }, + "agent": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "actionStatus": { "@id": "http://schema.org/FailedActionStatus" }, + "error": "this is just to test the error property" + }, + { + "@id": "#InCollection", + "@type": "Collection", + "mainEntity": "pics/in_main.jpg" + }, + { + "@id": "#OutCollection", + "@type": "Collection", + "mainEntity": "pics/out_main.jpg", + "hasPart": [ + { + "@id": "pics/out_01.jpg" + }, + { + "@id": "pics/out_02.jpg" + }, + { + "@id": "pics/out_main.jpg" + } + ] + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry" + } + ] +} diff --git a/tests/data/crates/invalid/3_process_run_crate/collection_no_mainentity/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/collection_no_mainentity/ro-crate-metadata.json new file mode 100644 index 00000000..3a2d525c --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/collection_no_mainentity/ro-crate-metadata.json @@ -0,0 +1,130 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + } + ], + "hasPart": [ + { + "@id": "pics/in_01.jpg" + }, + { + "@id": "pics/in_02.jpg" + }, + { + "@id": "pics/in_main.jpg" + }, + { + "@id": "pics/out_01.jpg" + }, + { + "@id": "pics/out_02.jpg" + }, + { + "@id": "pics/out_main.jpg" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0" + }, + "mentions": [ + { + "@id": "#Conversion" + }, + { + "@id": "#InCollection" + }, + { + "@id": "#OutCollection" + } + ], + "name": "Test Collections" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@id": "#Conversion", + "@type": "CreateAction", + "name": "Convert image collections", + "description": "Convert image collections", + "startTime": "2024-05-17T01:04:50+01:00", + "endTime": "2024-05-17T01:04:52+01:00", + "instrument": { + "@id": "https://www.imagemagick.org/" + }, + "object": { + "@id": "#InCollection" + }, + "result": { + "@id": "#OutCollection" + }, + "agent": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "actionStatus": { "@id": "http://schema.org/FailedActionStatus" }, + "error": "this is just to test the error property" + }, + { + "@id": "#InCollection", + "@type": "Collection", + "hasPart": [ + { + "@id": "pics/in_01.jpg" + }, + { + "@id": "pics/in_02.jpg" + }, + { + "@id": "pics/in_main.jpg" + } + ] + }, + { + "@id": "#OutCollection", + "@type": "Collection", + "mainEntity": "pics/out_main.jpg", + "hasPart": [ + { + "@id": "pics/out_01.jpg" + }, + { + "@id": "pics/out_02.jpg" + }, + { + "@id": "pics/out_main.jpg" + } + ] + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry" + } + ] +} diff --git a/tests/data/crates/invalid/3_process_run_crate/collection_not_mentioned/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/collection_not_mentioned/ro-crate-metadata.json new file mode 100644 index 00000000..2975fd11 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/collection_not_mentioned/ro-crate-metadata.json @@ -0,0 +1,125 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + } + ], + "hasPart": [ + { + "@id": "pics/in_01.jpg" + }, + { + "@id": "pics/in_02.jpg" + }, + { + "@id": "pics/in_main.jpg" + }, + { + "@id": "pics/out_01.jpg" + }, + { + "@id": "pics/out_02.jpg" + }, + { + "@id": "pics/out_main.jpg" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0" + }, + "mentions": [ + { + "@id": "#Conversion" + } + ], + "name": "Test Collections" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@id": "#Conversion", + "@type": "CreateAction", + "name": "Convert image collections", + "description": "Convert image collections", + "startTime": "2024-05-17T01:04:50+01:00", + "endTime": "2024-05-17T01:04:52+01:00", + "instrument": { + "@id": "https://www.imagemagick.org/" + }, + "object": { + "@id": "#InCollection" + }, + "result": { + "@id": "#OutCollection" + }, + "agent": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "actionStatus": { "@id": "http://schema.org/FailedActionStatus" }, + "error": "this is just to test the error property" + }, + { + "@id": "#InCollection", + "@type": "Collection", + "mainEntity": "pics/in_main.jpg", + "hasPart": [ + { + "@id": "pics/in_01.jpg" + }, + { + "@id": "pics/in_02.jpg" + }, + { + "@id": "pics/in_main.jpg" + } + ] + }, + { + "@id": "#OutCollection", + "@type": "Collection", + "mainEntity": "pics/out_main.jpg", + "hasPart": [ + { + "@id": "pics/out_01.jpg" + }, + { + "@id": "pics/out_02.jpg" + }, + { + "@id": "pics/out_main.jpg" + } + ] + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry" + } + ] +} diff --git a/tests/data/crates/valid/process-run-crate-collections/ro-crate-metadata.json b/tests/data/crates/valid/process-run-crate-collections/ro-crate-metadata.json new file mode 100644 index 00000000..4d3b6c14 --- /dev/null +++ b/tests/data/crates/valid/process-run-crate-collections/ro-crate-metadata.json @@ -0,0 +1,131 @@ +{ + "@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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + } + ], + "hasPart": [ + { + "@id": "pics/in_01.jpg" + }, + { + "@id": "pics/in_02.jpg" + }, + { + "@id": "pics/in_main.jpg" + }, + { + "@id": "pics/out_01.jpg" + }, + { + "@id": "pics/out_02.jpg" + }, + { + "@id": "pics/out_main.jpg" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0" + }, + "mentions": [ + { + "@id": "#Conversion" + }, + { + "@id": "#InCollection" + }, + { + "@id": "#OutCollection" + } + ], + "name": "Test Collections" + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4" + }, + { + "@id": "#Conversion", + "@type": "CreateAction", + "name": "Convert image collections", + "description": "Convert image collections", + "startTime": "2024-05-17T01:04:50+01:00", + "endTime": "2024-05-17T01:04:52+01:00", + "instrument": { + "@id": "https://www.imagemagick.org/" + }, + "object": { + "@id": "#InCollection" + }, + "result": { + "@id": "#OutCollection" + }, + "agent": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "actionStatus": { "@id": "http://schema.org/FailedActionStatus" }, + "error": "this is just to test the error property" + }, + { + "@id": "#InCollection", + "@type": "Collection", + "mainEntity": "pics/in_main.jpg", + "hasPart": [ + { + "@id": "pics/in_01.jpg" + }, + { + "@id": "pics/in_02.jpg" + }, + { + "@id": "pics/in_main.jpg" + } + ] + }, + { + "@id": "#OutCollection", + "@type": "Collection", + "mainEntity": "pics/out_main.jpg", + "hasPart": [ + { + "@id": "pics/out_01.jpg" + }, + { + "@id": "pics/out_02.jpg" + }, + { + "@id": "pics/out_main.jpg" + } + ] + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry" + } + ] +} diff --git a/tests/integration/profiles/process-run-crate/test_prc_collection.py b/tests/integration/profiles/process-run-crate/test_prc_collection.py new file mode 100644 index 00000000..59a08d29 --- /dev/null +++ b/tests/integration/profiles/process-run-crate/test_prc_collection.py @@ -0,0 +1,51 @@ +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import InvalidProcRC +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_prc_collection_not_mentioned(): + """\ + Test a Process Run Crate where the collection is not listed in the Root + Data Entity's mentions. + """ + do_entity_test( + InvalidProcRC().collection_not_mentioned, + Severity.RECOMMENDED, + False, + ["Process Run Crate Collection SHOULD"], + ["The Collection SHOULD be referenced from the Root Data Entity via mentions"], + profile_identifier="process-run-crate" + ) + + +def test_prc_collection_no_haspart(): + """\ + Test a Process Run Crate where the collection does not have a hasPart. + """ + do_entity_test( + InvalidProcRC().collection_no_haspart, + Severity.RECOMMENDED, + False, + ["Process Run Crate Collection SHOULD"], + ["The Collection SHOULD have a hasPart"], + profile_identifier="process-run-crate" + ) + + +def test_prc_collection_no_mainentity(): + """\ + Test a Process Run Crate where the collection does not have a mainEntity. + """ + do_entity_test( + InvalidProcRC().collection_no_mainentity, + Severity.RECOMMENDED, + False, + ["Process Run Crate Collection SHOULD"], + ["The Collection SHOULD have a mainEntity"], + profile_identifier="process-run-crate" + ) diff --git a/tests/integration/profiles/process-run-crate/test_valid_prc.py b/tests/integration/profiles/process-run-crate/test_valid_prc.py index 4d3f4c14..e281402f 100644 --- a/tests/integration/profiles/process-run-crate/test_valid_prc.py +++ b/tests/integration/profiles/process-run-crate/test_valid_prc.py @@ -15,3 +15,9 @@ def test_valid_process_run_crate_required(): True, profile_identifier="process-run-crate" ) + do_entity_test( + ValidROC().process_run_crate_collections, + Severity.REQUIRED, + True, + profile_identifier="process-run-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 646d9c55..54e8530e 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -45,6 +45,10 @@ def sort_and_change_archive(self) -> Path: def process_run_crate(self) -> Path: return VALID_CRATES_DATA_PATH / "process-run-crate" + @property + def process_run_crate_collections(self) -> Path: + return VALID_CRATES_DATA_PATH / "process-run-crate-collections" + class InvalidFileDescriptor: @@ -369,3 +373,15 @@ def action_no_error(self) -> Path: @property def action_obj_res_bad_type(self) -> Path: return self.base_path / "action_obj_res_bad_type" + + @property + def collection_not_mentioned(self) -> Path: + return self.base_path / "collection_not_mentioned" + + @property + def collection_no_haspart(self) -> Path: + return self.base_path / "collection_no_haspart" + + @property + def collection_no_mainentity(self) -> Path: + return self.base_path / "collection_no_mainentity" From c41d07c170d88880a3b2fe7633a8e2d11a38dbd2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 15 Jul 2024 10:39:08 +0200 Subject: [PATCH 644/902] feat(cli): :sparkles: add `utils` module for the `cli` package --- rocrate_validator/cli/utils.py | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 rocrate_validator/cli/utils.py diff --git a/rocrate_validator/cli/utils.py b/rocrate_validator/cli/utils.py new file mode 100644 index 00000000..8c1d30a7 --- /dev/null +++ b/rocrate_validator/cli/utils.py @@ -0,0 +1,37 @@ +import os +import re +import textwrap +from typing import Optional + +from rich.padding import Padding +from rich.rule import Rule +from rich.text import Text + +from rocrate_validator import log as logging +from rocrate_validator.utils import get_version + + +# set up logging +logger = logging.getLogger(__name__) + + +def format_text(text: str, + initial_indent: int = 0, + subsequent_indent: int = 0, + line_width: Optional[int] = None, + skip_initial_indent: bool = False) -> str: + text = re.sub(r"\s+", " ", text).strip() + line_width = line_width or os.get_terminal_size().columns - initial_indent + if line_width: + text = textwrap.fill(text, width=line_width, initial_indent=' ' * + (initial_indent if not skip_initial_indent else 0), + subsequent_indent=' ' * subsequent_indent, + break_long_words=False, + break_on_hyphens=False) + else: + text = textwrap.indent(text, ' ' * initial_indent) + return text + + +def get_app_header_rule() -> Text: + return Padding(Rule(f"\n[bold][green]ROCrate Validator[/green] (ver. [magenta]{get_version()}[/magenta])[/bold]"), (1, 1)) From 75901c67fadb3e31d591045e321e8541aa480fa5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 15 Jul 2024 10:49:38 +0200 Subject: [PATCH 645/902] feat(cli): :sparkles: enable paging by default --- rocrate_validator/cli/commands/profiles.py | 84 +++++++++++------ rocrate_validator/cli/commands/validate.py | 100 ++++++++++++--------- 2 files changed, 113 insertions(+), 71 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 78b06fe7..6d164f75 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -1,12 +1,14 @@ from pathlib import Path from rich.markdown import Markdown +from rich.padding import Padding from rich.panel import Panel from rich.table import Table import rocrate_validator.log as logging from rocrate_validator import services from rocrate_validator.cli.main import cli, click +from rocrate_validator.cli.utils import get_app_header_rule from rocrate_validator.colors import get_severity_color from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER from rocrate_validator.models import LevelCollection, RequirementLevel @@ -38,23 +40,34 @@ def profiles(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH): @profiles.command("list") +@click.option( + '--no-paging', + is_flag=True, + help="Disable paging", + default=False, + show_default=True +) @click.pass_context -def list_profiles(ctx): # , profiles_path: Path = DEFAULT_PROFILES_PATH): +def list_profiles(ctx, no_paging: bool = False): # , profiles_path: Path = DEFAULT_PROFILES_PATH): """ List available profiles """ profiles_path = ctx.obj['profiles_path'] console = ctx.obj['console'] + enable_pager = not no_paging # Get the profiles profiles = services.get_profiles(profiles_path=profiles_path) # console.print("\nAvailable profiles:", style="white bold") console.print("\n", style="white bold") table = Table(show_header=True, - title="Available profiles", + title=" Available profiles", + title_style="italic bold cyan", + title_justify="left", header_style="bold cyan", border_style="bright_black", show_footer=False, + caption_style="italic bold", caption="[cyan](*)[/cyan] Number of requirements by severity") # Define columns @@ -88,7 +101,9 @@ def list_profiles(ctx): # , profiles_path: Path = DEFAULT_PROFILES_PATH): table.add_row() # Print the table - console.print(table) + with console.pager(styles=True) if enable_pager else console: + console.print(get_app_header_rule()) + console.print(Padding(table, (0, 1))) @profiles.command("describe") @@ -101,33 +116,48 @@ def list_profiles(ctx): # , profiles_path: Path = DEFAULT_PROFILES_PATH): show_default=True ) @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 +) @click.pass_context def describe_profile(ctx, profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, profiles_path: Path = DEFAULT_PROFILES_PATH, - verbose: bool = False): + verbose: bool = False, no_paging: bool = False): """ Show a profile """ # Get the console console = ctx.obj['console'] + # Get the no_paging flag + enable_pager = not no_paging + # Get the profile profile = services.get_profile(profiles_path=profiles_path, profile_identifier=profile_identifier) - # Print the profile header - console.print("\n", style="white bold") - title_text = f"[bold cyan]Version:[/bold cyan] [italic green]{profile.version}[/italic green]\n" - title_text += f"[bold cyan]URI:[/bold cyan] [italic yellow]{profile.uri}[/italic yellow]\n\n" - title_text += f"[bold cyan]Description:[/bold cyan] [italic]{profile.description.strip()}[/italic]" - box = Panel( - title_text, title=f"[bold][cyan]Profile:[/cyan] [magenta italic]{profile.identifier}[/magenta italic][/bold]", padding=(1, 1)) - console.print(box) - # Print the profile requirements + + # Set the subheader title + subheader_title = f"[bold][cyan]Profile:[/cyan] [magenta italic]{profile.identifier}[/magenta italic][/bold]" + + # Set the subheader content + subheader_content = f"[bold cyan]Version:[/bold cyan] [italic green]{profile.version}[/italic green]\n" + subheader_content += f"[bold cyan]URI:[/bold cyan] [italic yellow]{profile.uri}[/italic yellow]\n\n" + subheader_content += f"[bold cyan]Description:[/bold cyan] [italic]{profile.description.strip()}[/italic]" + + # Build the profile table if not verbose: - __compacted_describe_profile__(console, profile) + table = __compacted_describe_profile__(profile) else: - __verbose_describe_profile__(console, profile) - # End with a new line - console.print("\n") + table = __verbose_describe_profile__(profile) + + with console.pager(styles=True) if enable_pager else console: + console.print(get_app_header_rule()) + console.print(Padding(Panel(subheader_content, title=subheader_title, padding=(1, 1), + title_align="left", border_style="cyan"), (0, 1, 0, 1))) + console.print(Padding(table, (1, 1))) def __requirement_level_style__(requirement: RequirementLevel): @@ -138,7 +168,7 @@ def __requirement_level_style__(requirement: RequirementLevel): return f"{color} bold" -def __compacted_describe_profile__(console, profile): +def __compacted_describe_profile__(profile): """ Show a profile in a compact way """ @@ -157,14 +187,15 @@ def __compacted_describe_profile__(console, profile): f"{len(requirement.get_checks_by_level(LevelCollection.OPTIONAL))}")) table = Table(show_header=True, + # renderer=renderer, title=f"[cyan]{len(requirements)}[/cyan] Profile Requirements", title_style="italic bold", header_style="bold cyan", border_style="bright_black", show_footer=False, show_lines=True, - caption=f"[cyan](*)[/cyan] number of checks by severity level: {', '.join(levels_list)}", - caption_style="italic bold") + caption_style="italic bold", + caption=f"[cyan](*)[/cyan] number of checks by severity level: {', '.join(levels_list)}") # Define columns table.add_column("#", style="cyan bold", justify="right") @@ -176,11 +207,10 @@ def __compacted_describe_profile__(console, profile): # Add data to the table for row in table_rows: table.add_row(*row) - # Print the table - console.print(table) + return table -def __verbose_describe_profile__(console, profile): +def __verbose_describe_profile__(profile): """ Show a profile in a verbose way """ @@ -203,14 +233,15 @@ def __verbose_describe_profile__(console, profile): count_checks += 1 table = Table(show_header=True, + # renderer=renderer, title=f"[cyan]{count_checks}[/cyan] Profile Requirements Checks", title_style="italic bold", header_style="bold cyan", border_style="bright_black", show_footer=False, show_lines=True, - caption=f"[cyan](*)[/cyan] severity level of requirement check: {', '.join(levels_list)}", - caption_style="italic bold") + caption_style="italic bold", + caption=f"[cyan](*)[/cyan] number of checks by severity level: {', '.join(levels_list)}") # Define columns table.add_column("Identifier", style="cyan bold", justify="right") @@ -221,5 +252,4 @@ def __verbose_describe_profile__(console, profile): # Add data to the table for row in table_rows: table.add_row(*row) - # Print the table - console.print(table) + return table diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index a6798d7d..3d5c42d9 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -95,6 +95,13 @@ def validate_uri(ctx, param, value): default=False, show_default=True ) +@click.option( + '--no-paging', + is_flag=True, + help="Disable paging", + default=False, + show_default=True +) # @click.option( # "-o", # "--ontologies-path", @@ -111,11 +118,14 @@ def validate(ctx, requirement_severity_only: bool = False, rocrate_uri: Path = ".", no_fail_fast: bool = False, - ontologies_path: Optional[Path] = None): + ontologies_path: Optional[Path] = None, + no_paging: bool = False): """ [magenta]rocrate-validator:[/magenta] Validate a RO-Crate against a profile """ console: Console = ctx.obj['console'] + # Get the no_paging flag + enable_pager = not no_paging # Log the input parameters for debugging logger.debug("profiles_path: %s", os.path.abspath(profiles_path)) logger.debug("profile_identifier: %s", profile_identifier) @@ -147,7 +157,7 @@ def validate(ctx, ) # Print the validation result - __print_validation_result__(console, result, result.context.requirement_severity) + __print_validation_result__(console, result, result.context.requirement_severity, enable_pager=enable_pager) # using ctx.exit seems to raise an Exception that gets caught below, # so we use sys.exit instead. @@ -157,57 +167,59 @@ def validate(ctx, def __print_validation_result__( console: Console, result: ValidationResult, - severity: Severity = Severity.RECOMMENDED): + severity: Severity = Severity.RECOMMENDED, + enable_pager: bool = True): """ Print the validation result """ - if result.passed(severity=severity): - console.print( - f"\n\n[bold][[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", - style="white", - ) - else: - console.print( - f"\n\n[bold][[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", - style="white", - ) - - console.print("\n[bold]The following requirements have not meet: [/bold]\n", style="white") - - for requirement in sorted(result.failed_requirements, - key=lambda x: (-x.severity.value, x)): - issue_color = get_severity_color(requirement.severity) + with console.pager(styles=True) if enable_pager else console: + if result.passed(severity=severity): console.print( - Align(f"\n [severity: [{issue_color}]{requirement.severity.name}[/{issue_color}], " - f"profile: [magenta bold]{requirement.profile.name }[/magenta bold]]", align="right") + f"\n\n[bold][[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", + style="white", ) + else: console.print( - f" [bold][cyan][{requirement.order_number}] [u]{Markdown(requirement.name).markup}[/u][/cyan][/bold]", + f"\n\n[bold][[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", style="white", ) - console.print(f"\n{' '*4}{Markdown(requirement.description).markup}\n", style="white italic") - console.print(f"{' '*4}[cyan u]Failed checks[/cyan u]:\n", style="white bold") - for check in sorted(result.get_failed_checks_by_requirement(requirement), - key=lambda x: (-x.severity.value, x)): - issue_color = get_severity_color(check.level.severity) + console.print("\n[bold]The following requirements have not meet: [/bold]\n", style="white") + + for requirement in sorted(result.failed_requirements, + key=lambda x: (-x.severity.value, x)): + issue_color = get_severity_color(requirement.severity) console.print( - f"{' '*4}- " - f"[magenta bold]{check.name}[/magenta bold]: {Markdown(check.description).markup}") - console.print(f"\n{' '*6}[u]Detected issues[/u]:", style="white bold") - for issue in sorted(result.get_issues_by_check(check), + Align(f"\n [severity: [{issue_color}]{requirement.severity.name}[/{issue_color}], " + f"profile: [magenta bold]{requirement.profile.name }[/magenta bold]]", align="right") + ) + console.print( + f" [bold][cyan][{requirement.order_number}] [u]{Markdown(requirement.name).markup}[/u][/cyan][/bold]", + style="white", + ) + console.print(f"\n{' '*4}{Markdown(requirement.description).markup}\n", style="white italic") + + console.print(f"{' '*4}[cyan u]Failed checks[/cyan u]:\n", style="white bold") + for check in sorted(result.get_failed_checks_by_requirement(requirement), key=lambda x: (-x.severity.value, x)): - path = "" - if issue.resultPath and issue.value: - path = f"on [yellow]{issue.resultPath}[/yellow]" - if issue.value: - if issue.resultPath: - path += "=" - path += f"\"[green]{issue.value}[/green]\"" - if issue.focusNode: - path = path + " of " if len(path) > 0 else " on " + f"[cyan]<{issue.focusNode}>[/cyan]" + issue_color = get_severity_color(check.level.severity) console.print( - f"{' ' * 6}- [[red]Violation[/red] of " - f"[{issue_color} bold]{issue.check.identifier}[/{issue_color} bold]{path}]: " - f"{Markdown(issue.message).markup}",) - console.print("\n", style="white") + f"{' '*4}- " + f"[magenta bold]{check.name}[/magenta bold]: {Markdown(check.description).markup}") + console.print(f"\n{' '*6}[u]Detected issues[/u]:", style="white bold") + for issue in sorted(result.get_issues_by_check(check), + key=lambda x: (-x.severity.value, x)): + path = "" + if issue.resultPath and issue.value: + path = f"on [yellow]{issue.resultPath}[/yellow]" + if issue.value: + if issue.resultPath: + path += "=" + path += f"\"[green]{issue.value}[/green]\"" + if issue.focusNode: + path = path + " of " if len(path) > 0 else " on " + f"[cyan]<{issue.focusNode}>[/cyan]" + console.print( + f"{' ' * 6}- [[red]Violation[/red] of " + f"[{issue_color} bold]{issue.check.identifier}[/{issue_color} bold]{path}]: " + f"{Markdown(issue.message).markup}",) + console.print("\n", style="white") From 9fa52b470de931c8b95c332f0775a0639f96d821 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 15 Jul 2024 12:35:41 +0200 Subject: [PATCH 646/902] fix(core): :bug: update detection of ProfileNotFound error --- rocrate_validator/errors.py | 8 +++++++- rocrate_validator/models.py | 6 ++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 02293c9b..c1bafaba 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -45,14 +45,20 @@ def __repr__(self): class ProfileNotFound(ROCValidatorError): """Raised when a profile is not found.""" - def __init__(self, profile_name: Optional[str] = None): + def __init__(self, profile_name: Optional[str] = None, message: Optional[str] = None): self._profile_name = profile_name + self._message = message @property def profile_name(self) -> Optional[str]: """The name of the profile.""" return self._profile_name + @property + def message(self) -> Optional[str]: + """The error message.""" + return self._message + def __str__(self) -> str: return f"Profile not found: {self._profile_name!r}" diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 328a26c4..8aa281d3 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1247,10 +1247,12 @@ def __load_profiles__(self) -> list[Profile]: logger.debug("Profile with the highest version number: %s", profile) # if the profile is found by token, set the profile name to the identifier self.settings["profile_identifier"] = profile.identifier - except Exception as e: + except AttributeError as e: + # raised when the profile is not found if logger.isEnabledFor(logging.DEBUG): logger.exception(e) - raise ProfileNotFound(f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'") + raise ProfileNotFound(self.profile_identifier, + message=f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'") from e # Set the profiles to validate against as the target profile and its inherited profiles profiles = profile.inherited_profiles + [profile] From 0fe7636e0633f4572fd989aef343b578aa253312 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 15 Jul 2024 12:37:18 +0200 Subject: [PATCH 647/902] refactor(cli): :children_crossing: improve error handling --- rocrate_validator/cli/commands/validate.py | 81 +++++++++++++++------- rocrate_validator/cli/main.py | 18 ++--- 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 3d5c42d9..afb250ff 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -8,13 +8,14 @@ from rich.markdown import Markdown import rocrate_validator.log as logging +from rocrate_validator import services +from rocrate_validator.cli.main import cli, click +from rocrate_validator.colors import get_severity_color from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER - -from ... import services -from ...colors import get_severity_color -from ...models import LevelCollection, Severity, ValidationResult -from ...utils import URI, get_profiles_path -from ..main import cli, click +from rocrate_validator.errors import ProfileNotFound, ProfilesDirectoryNotFound +from rocrate_validator.models import (LevelCollection, Severity, + ValidationResult) +from rocrate_validator.utils import URI, get_profiles_path # from rich.markdown import Markdown # from rich.table import Table @@ -143,25 +144,55 @@ def validate(ctx, logger.debug("rocrate_path: %s", os.path.abspath(rocrate_uri)) # Validate the RO-Crate - result: ValidationResult = services.validate( - { - "profiles_path": profiles_path, - "profile_identifier": profile_identifier, - "requirement_severity": requirement_severity, - "requirement_severity_only": requirement_severity_only, - "inherit_profiles": not disable_profile_inheritance, - "data_path": rocrate_uri, - "ontology_path": Path(ontologies_path).absolute() if ontologies_path else None, - "abort_on_first": not no_fail_fast - } - ) - - # Print the validation result - __print_validation_result__(console, result, result.context.requirement_severity, enable_pager=enable_pager) - - # using ctx.exit seems to raise an Exception that gets caught below, - # so we use sys.exit instead. - sys.exit(0 if result.passed(LevelCollection.get(requirement_severity).severity) else 1) + + try: + result: ValidationResult = services.validate( + { + "profiles_path": profiles_path, + "profile_identifier": profile_identifier, + "requirement_severity": requirement_severity, + "requirement_severity_only": requirement_severity_only, + "inherit_profiles": not disable_profile_inheritance, + "data_path": rocrate_uri, + "ontology_path": Path(ontologies_path).absolute() if ontologies_path else None, + "abort_on_first": not no_fail_fast + } + ) + + # Print the validation result + __print_validation_result__(console, result, result.context.requirement_severity, enable_pager=enable_pager) + + # using ctx.exit seems to raise an Exception that gets caught below, + # so we use sys.exit instead. + sys.exit(0 if result.passed(LevelCollection.get(requirement_severity).severity) else 1) + except ProfilesDirectoryNotFound as e: + error_message = f""" + The profile folder could not be located at the specified path: [red]{e.profiles_path}[/red]. + Please ensure that the path is correct and try again. + """ + console.print( + f"\n\n[bold][[red]ERROR[/red]] {error_message} !!![/bold]\n", style="white") + sys.exit(2) + except ProfileNotFound as e: + error_message = f"""The profile with the identifier "[red bold]{e.profile_name}[/red bold]" could not be found. + Please ensure that the profile exists and try again. + + To see the available profiles, run: + [cyan bold]rocrate-validator profiles list[/cyan bold] + """ + console.print( + f"\n\n[bold][[red]ERROR[/red]] {error_message}[/bold]\n", style="white") + sys.exit(2) + except Exception as e: + console.print( + f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", style="white") + if logger.isEnabledFor(logging.DEBUG): + console.print_exception() + 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) def __print_validation_result__( diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index c624a0a1..293b4909 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -5,7 +5,6 @@ from rich.console import Console import rocrate_validator.log as logging -from rocrate_validator.errors import ProfilesDirectoryNotFound from rocrate_validator.utils import get_version # set up logging @@ -54,24 +53,15 @@ def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool): # If no subcommand is provided, invoke the default command from .commands.validate import validate ctx.invoke(validate) - - except ProfilesDirectoryNotFound as e: - error_message = f""" - The profile folder could not be located at the specified path: [red]{e.profiles_path}[/red]. - Please ensure that the path is correct and try again. - """ - console.print( - f"\n\n[bold][[red]ERROR[/red]] {error_message} !!![/bold]\n", style="white") - sys.exit(2) + 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") - if logger.isEnabledFor(logging.DEBUG): - console.print_exception() - console.print(""" - This error may be due to a bug. Please report it to the issue tracker + 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) From 55a3e0953c5d033aec3983ee3b0cb5ac66371117 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 15 Jul 2024 15:28:00 +0200 Subject: [PATCH 648/902] refactor(cli): :recycle: reformat error message --- rocrate_validator/cli/commands/validate.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index afb250ff..67e9b4d9 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -1,5 +1,6 @@ import os import sys +import textwrap from pathlib import Path from typing import Optional @@ -167,17 +168,17 @@ def validate(ctx, sys.exit(0 if result.passed(LevelCollection.get(requirement_severity).severity) else 1) except ProfilesDirectoryNotFound as e: error_message = f""" - The profile folder could not be located at the specified path: [red]{e.profiles_path}[/red]. + The profile folder could not be located at the specified path: [red]{e.profiles_path}[/red]. Please ensure that the path is correct and try again. """ console.print( f"\n\n[bold][[red]ERROR[/red]] {error_message} !!![/bold]\n", style="white") sys.exit(2) except ProfileNotFound as e: - error_message = f"""The profile with the identifier "[red bold]{e.profile_name}[/red bold]" could not be found. + error_message = f"""The profile with the identifier "[red bold]{e.profile_name}[/red bold]" could not be found. Please ensure that the profile exists and try again. - - To see the available profiles, run: + + To see the available profiles, run: [cyan bold]rocrate-validator profiles list[/cyan bold] """ console.print( @@ -188,9 +189,8 @@ def validate(ctx, f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", style="white") if logger.isEnabledFor(logging.DEBUG): console.print_exception() - 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(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)) console.print_exception() sys.exit(2) From b1b3a0dad7d3663eaeda0fb66e7d6b312024b3e9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 15 Jul 2024 17:14:28 +0200 Subject: [PATCH 649/902] style(cli): :lipstick: --- rocrate_validator/cli/commands/validate.py | 31 +++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 67e9b4d9..92d3abb0 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -7,10 +7,12 @@ from rich.align import Align from rich.console import Console from rich.markdown import Markdown +from rich.padding import Padding import rocrate_validator.log as logging from rocrate_validator import services from rocrate_validator.cli.main import cli, click +from rocrate_validator.cli.utils import format_text from rocrate_validator.colors import get_severity_color from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER from rocrate_validator.errors import ProfileNotFound, ProfilesDirectoryNotFound @@ -206,16 +208,16 @@ def __print_validation_result__( with console.pager(styles=True) if enable_pager else console: if result.passed(severity=severity): console.print( - f"\n\n[bold][[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", + Padding(f"\n\n[bold][[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", (0, 2)), style="white", ) else: console.print( - f"\n\n[bold][[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", + Padding(f"\n\n[bold][[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", (0, 2)), style="white", ) - console.print("\n[bold]The following requirements have not meet: [/bold]\n", style="white") + console.print(Padding("\n[bold]The following requirements have not meet: [/bold]", (0, 2)), style="white") for requirement in sorted(result.failed_requirements, key=lambda x: (-x.severity.value, x)): @@ -228,29 +230,28 @@ def __print_validation_result__( f" [bold][cyan][{requirement.order_number}] [u]{Markdown(requirement.name).markup}[/u][/cyan][/bold]", style="white", ) - console.print(f"\n{' '*4}{Markdown(requirement.description).markup}\n", style="white italic") + console.print(Padding(Markdown(requirement.description), (1, 7))) + console.print(Padding("[white bold u] Failed checks [/white bold u]\n", + (0, 8)), style="white bold") - console.print(f"{' '*4}[cyan u]Failed checks[/cyan u]:\n", style="white bold") for check in sorted(result.get_failed_checks_by_requirement(requirement), key=lambda x: (-x.severity.value, x)): issue_color = get_severity_color(check.level.severity) console.print( - f"{' '*4}- " - f"[magenta bold]{check.name}[/magenta bold]: {Markdown(check.description).markup}") - console.print(f"\n{' '*6}[u]Detected issues[/u]:", style="white bold") + Padding(f"[bold][{issue_color}][{check.identifier.center(16)}][/{issue_color}] [magenta]{check.name}[/magenta][/bold]:", (0, 7)), style="white bold") + console.print(Padding(Markdown(check.description), (0, 27))) + console.print(Padding("[u]Detected issues[/u]", (0, 8)), style="white bold") for issue in sorted(result.get_issues_by_check(check), key=lambda x: (-x.severity.value, x)): path = "" if issue.resultPath and issue.value: - path = f"on [yellow]{issue.resultPath}[/yellow]" + path = f"of [yellow]{issue.resultPath}[/yellow]" if issue.value: if issue.resultPath: path += "=" - path += f"\"[green]{issue.value}[/green]\"" - if issue.focusNode: - path = path + " of " if len(path) > 0 else " on " + f"[cyan]<{issue.focusNode}>[/cyan]" + path += f"\"[green]{issue.value}[/green]\" " # keep the ending space + path = path + "on " + f"[cyan]<{issue.focusNode}>[/cyan]" console.print( - f"{' ' * 6}- [[red]Violation[/red] of " - f"[{issue_color} bold]{issue.check.identifier}[/{issue_color} bold]{path}]: " - f"{Markdown(issue.message).markup}",) + Padding(f"- [[red]Violation[/red] {path}]: " + f"{Markdown(issue.message).markup}", (0, 9)), style="white") console.print("\n", style="white") From 345fc296993bb683d86aeecffa73efdfc6ca9627 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 15 Jul 2024 17:27:45 +0200 Subject: [PATCH 650/902] refactor(cli): :wastebasket: clean up --- rocrate_validator/cli/commands/validate.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 92d3abb0..e79e8b0a 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -12,7 +12,6 @@ import rocrate_validator.log as logging from rocrate_validator import services from rocrate_validator.cli.main import cli, click -from rocrate_validator.cli.utils import format_text from rocrate_validator.colors import get_severity_color from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER from rocrate_validator.errors import ProfileNotFound, ProfilesDirectoryNotFound @@ -106,13 +105,6 @@ def validate_uri(ctx, param, value): default=False, show_default=True ) -# @click.option( -# "-o", -# "--ontologies-path", -# type=click.Path(exists=True), -# default="./ontologies", -# help="Path containing the ontology files", -# ) @click.pass_context def validate(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH, @@ -147,7 +139,6 @@ def validate(ctx, logger.debug("rocrate_path: %s", os.path.abspath(rocrate_uri)) # Validate the RO-Crate - try: result: ValidationResult = services.validate( { From 94a288c558697dee43945d5efaf84a4351d9a309 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 15 Jul 2024 17:46:00 +0200 Subject: [PATCH 651/902] refactor(cli): :lipstick: update header style --- rocrate_validator/cli/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/cli/utils.py b/rocrate_validator/cli/utils.py index 8c1d30a7..e723a721 100644 --- a/rocrate_validator/cli/utils.py +++ b/rocrate_validator/cli/utils.py @@ -10,7 +10,6 @@ from rocrate_validator import log as logging from rocrate_validator.utils import get_version - # set up logging logger = logging.getLogger(__name__) @@ -34,4 +33,5 @@ def format_text(text: str, def get_app_header_rule() -> Text: - return Padding(Rule(f"\n[bold][green]ROCrate Validator[/green] (ver. [magenta]{get_version()}[/magenta])[/bold]"), (1, 1)) + return Padding(Rule(f"\n[bold][cyan]ROCrate Validator[/cyan] (ver. [magenta]{get_version()}[/magenta])[/bold]", + style="bold cyan"), (1, 1)) From 12a88503633022a431b5c776a01ecd48ef5c5981 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 15 Jul 2024 17:55:57 +0200 Subject: [PATCH 652/902] refactor(cli): :lipstick: update layout of the list profiles cmd --- rocrate_validator/cli/commands/profiles.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 6d164f75..90ffe80a 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -57,11 +57,9 @@ def list_profiles(ctx, no_paging: bool = False): # , profiles_path: Path = DEFA enable_pager = not no_paging # Get the profiles profiles = services.get_profiles(profiles_path=profiles_path) - # console.print("\nAvailable profiles:", style="white bold") - console.print("\n", style="white bold") table = Table(show_header=True, - title=" Available profiles", + title=" Available profiles", title_style="italic bold cyan", title_justify="left", header_style="bold cyan", From 1fd6a4e767b4eaea9bf8fa57b654132cc9cf205c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 15 Jul 2024 18:02:32 +0200 Subject: [PATCH 653/902] refactor(cli): :lipstick: minor changes to the layout of validate cmd --- rocrate_validator/cli/commands/validate.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index e79e8b0a..7966f989 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -12,6 +12,7 @@ import rocrate_validator.log as logging from rocrate_validator import services from rocrate_validator.cli.main import cli, click +from rocrate_validator.cli.utils import get_app_header_rule from rocrate_validator.colors import get_severity_color from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER from rocrate_validator.errors import ProfileNotFound, ProfilesDirectoryNotFound @@ -197,14 +198,17 @@ def __print_validation_result__( Print the validation result """ with console.pager(styles=True) if enable_pager else console: + # Print the header + console.print(get_app_header_rule()) + if result.passed(severity=severity): console.print( - Padding(f"\n\n[bold][[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", (0, 2)), + Padding(f"[bold][[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", (0, 2)), style="white", ) else: console.print( - Padding(f"\n\n[bold][[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", (0, 2)), + Padding(f"[bold][[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", (0, 2)), style="white", ) From abbfecaefedb20d7221d2beee12a642379331df8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 19 Jul 2024 16:22:13 +0200 Subject: [PATCH 654/902] feat(core): :sparkles: support for validation events Allows registering subscribers for a predefined set of validation events --- rocrate_validator/events.py | 59 +++++++++++++++++++++++++++++ rocrate_validator/models.py | 75 ++++++++++++++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 rocrate_validator/events.py diff --git a/rocrate_validator/events.py b/rocrate_validator/events.py new file mode 100644 index 00000000..773e9b71 --- /dev/null +++ b/rocrate_validator/events.py @@ -0,0 +1,59 @@ +import enum +from abc import ABC, abstractmethod +from functools import total_ordering +from typing import Optional, Union + + +@enum.unique +@total_ordering +class EventType(enum.Enum): + """Enum ordering "strength" of conditions to be verified""" + VALIDATION_START = 0 + VALIDATION_END = 1 + PROFILE_VALIDATION_START = 2 + PROFILE_VALIDATION_END = 3 + REQUIREMENT_VALIDATION_START = 4 + REQUIREMENT_VALIDATION_END = 5 + REQUIREMENT_CHECK_VALIDATION_START = 6 + REQUIREMENT_CHECK_VALIDATION_END = 7 + + def __lt__(self, other): + if self.__class__ is other.__class__: + return self.value < other.value + return NotImplemented + + +class Event: + def __init__(self, event_type: EventType, message: Optional[str] = None): + self.event_type = event_type + self.message = message + + +class Subscriber(ABC): + def __init__(self, name): + self.name = name + + @abstractmethod + def update(self, event: Event): + pass + + +class Publisher: + def __init__(self): + self.__subscribers = set() + + @property + def subscribers(self): + return self.__subscribers + + def add_subscriber(self, subscriber): + self.subscribers.add(subscriber) + + def remove_subscriber(self, subscriber): + self.subscribers.remove(subscriber) + + def notify(self, event: Union[Event, EventType]): + if isinstance(event, EventType): + event = Event(event) + for subscriber in self.subscribers: + subscriber.update(event) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 8aa281d3..b8eb35f5 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -28,6 +28,7 @@ ProfileSpecificationError, ProfileSpecificationNotFound, ROCrateMetadataNotFoundError) +from rocrate_validator.events import Event, EventType, Publisher from rocrate_validator.rocrate import ROCrate from rocrate_validator.utils import (URI, MapIndex, MultiIndexMap, get_profiles_path, @@ -560,7 +561,11 @@ def __do_validate__(self, context: ValidationContext) -> bool: if check.overridden: logger.debug("Skipping check '%s' because overridden by '%s'", check.name, check.overridden_by.name) continue + context.validator.notify(RequirementCheckValidationEvent( + EventType.REQUIREMENT_CHECK_VALIDATION_START, check)) check_result = check.execute_check(context) + context.validator.notify(RequirementCheckValidationEvent( + EventType.REQUIREMENT_CHECK_VALIDATION_END, check, validation_result=check_result)) logger.debug("Ran check '%s'. Got result %s", check.name, 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) @@ -1048,7 +1053,64 @@ def parse(cls, settings: Union[dict, ValidationSettings]) -> ValidationSettings: raise ValueError(f"Invalid settings type: {type(settings)}") -class Validator: +class ValidationEvent(Event): + def __init__(self, event_type: EventType, validation_result: Optional[ValidationResult] = None, message: Optional[str] = None): + super().__init__(event_type, message) + self._validation_result = validation_result + + @property + def validation_result(self) -> Optional[ValidationResult]: + return self._validation_result + + +class ProfileValidationEvent(Event): + def __init__(self, event_type: EventType, profile: Profile, message: Optional[str] = None): + assert event_type in (EventType.PROFILE_VALIDATION_START, EventType.PROFILE_VALIDATION_END) + super().__init__(event_type, message) + self._profile = profile + + @property + def profile(self) -> Profile: + return self._profile + + +class RequirementValidationEvent(Event): + def __init__(self, + event_type: EventType, + requirement: Requirement, + validation_result: Optional[bool] = None, + message: Optional[str] = None): + assert event_type in (EventType.REQUIREMENT_VALIDATION_START, EventType.REQUIREMENT_VALIDATION_END) + super().__init__(event_type, message) + self._requirement = requirement + self._validation_result = validation_result + + @property + def requirement(self) -> Requirement: + return self._requirement + + @property + def validation_result(self) -> Optional[bool]: + return self._validation_result + + +class RequirementCheckValidationEvent(Event): + def __init__(self, event_type: EventType, requirement_check: RequirementCheck, validation_result: Optional[bool] = None, message: Optional[str] = None): + assert event_type in (EventType.REQUIREMENT_CHECK_VALIDATION_START, EventType.REQUIREMENT_CHECK_VALIDATION_END) + super().__init__(event_type, message) + self._requirement_check = requirement_check + self._validation_result = validation_result + + @property + def requirement_check(self) -> RequirementCheck: + return self._requirement_check + + @property + def validation_result(self) -> Optional[bool]: + return self._validation_result + + +class Validator(Publisher): """ Can validate conformance to a single Profile (including any requirements inherited by parent profiles). @@ -1056,6 +1118,7 @@ class Validator: def __init__(self, settings: Union[str, ValidationSettings]): self._validation_settings = ValidationSettings.parse(settings) + super().__init__() @property def validation_settings(self) -> ValidationSettings: @@ -1080,9 +1143,10 @@ def __do_validate__(self, # set the profiles to validate against profiles = context.profiles assert len(profiles) > 0, "No profiles to validate" - + self.notify(EventType.PROFILE_VALIDATION_START) for profile in profiles: logger.debug("Validating profile %s (id: %s)", profile.name, profile.identifier) + 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) @@ -1090,8 +1154,12 @@ def __do_validate__(self, logger.debug("For profile %s, validating these %s requirements: %s", profile.identifier, len(requirements), requirements) for requirement in requirements: + logger.debug("Validating Requirement %s", requirement) + self.notify(RequirementValidationEvent(EventType.REQUIREMENT_VALIDATION_START, requirement=requirement)) passed = requirement.__do_validate__(context) logger.debug("Number of issues: %s", len(context.result.issues)) + self.notify(RequirementValidationEvent( + EventType.REQUIREMENT_VALIDATION_END, requirement=requirement, validation_result=passed)) if passed: logger.debug("Validation Requirement passed") else: @@ -1099,6 +1167,9 @@ def __do_validate__(self, if context.settings.get("abort_on_first") is True and context.profile_identifier == profile.name: logger.debug("Aborting on first requirement failure") return context.result + self.notify(ProfileValidationEvent(EventType.PROFILE_VALIDATION_END, profile=profile)) + self.notify(ValidationEvent(EventType.VALIDATION_END, + validation_result=context.result)) return context.result From bfc5b1398114757832d8aeb10d7406c2d3c45922 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 19 Jul 2024 16:26:01 +0200 Subject: [PATCH 655/902] feat(core): :sparkles: build Severity from string --- rocrate_validator/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index b8eb35f5..359fad04 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -56,6 +56,10 @@ def __lt__(self, other: object) -> bool: else: raise TypeError(f"Comparison not supported between instances of {type(self)} and {type(other)}") + @staticmethod + def get(name: str) -> Severity: + return getattr(Severity, name.upper()) + @total_ordering @dataclass From 20dba6416625f36340b8399173bbf484eb4a62d9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 19 Jul 2024 16:29:29 +0200 Subject: [PATCH 656/902] feat(cli): :sparkles: provide more info about the validation --- rocrate_validator/cli/commands/validate.py | 361 +++++++++++++++++++-- 1 file changed, 335 insertions(+), 26 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 7966f989..1ecb1fc4 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -5,20 +5,26 @@ from typing import Optional from rich.align import Align -from rich.console import Console +from rich.console import Console, Group +from rich.layout import Layout +from rich.live import Live from rich.markdown import Markdown from rich.padding import Padding +from rich.panel import Panel +from rich.progress import BarColumn, Progress, TextColumn, TimeElapsedColumn +from rich.prompt import Confirm +from rich.rule import Rule import rocrate_validator.log as logging from rocrate_validator import services from rocrate_validator.cli.main import cli, click -from rocrate_validator.cli.utils import get_app_header_rule from rocrate_validator.colors import get_severity_color from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER from rocrate_validator.errors import ProfileNotFound, ProfilesDirectoryNotFound +from rocrate_validator.events import Event, EventType, Subscriber from rocrate_validator.models import (LevelCollection, Severity, ValidationResult) -from rocrate_validator.utils import URI, get_profiles_path +from rocrate_validator.utils import URI, get_profiles_path, get_version # from rich.markdown import Markdown # from rich.table import Table @@ -189,31 +195,278 @@ def validate(ctx, sys.exit(2) -def __print_validation_result__( - console: Console, - result: ValidationResult, - severity: Severity = Severity.RECOMMENDED, - enable_pager: bool = True): - """ - Print the validation result - """ - with console.pager(styles=True) if enable_pager else console: - # Print the header - console.print(get_app_header_rule()) - - if result.passed(severity=severity): - console.print( - Padding(f"[bold][[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", (0, 2)), - style="white", +class ProgressMonitor(Subscriber): + + PROFILE_VALIDATION = "Profiles" + REQUIREMENT_VALIDATION = "Requirements" + REQUIREMENT_CHECK_VALIDATION = "Requirements Checks" + + def __init__(self, layout: ValidationReportLayout, stats: dict): + self.__progress = Progress( + TextColumn("[progress.description]{task.description}"), + BarColumn(), + TextColumn("{task.completed}/{task.total}"), + TimeElapsedColumn(), + expand=True) + self._stats = stats + self.profile_validation = self.progress.add_task( + self.PROFILE_VALIDATION, total=len(stats.get("profiles"))) + self.requirement_validation = self.progress.add_task( + self.REQUIREMENT_VALIDATION, total=stats.get("total_requirements")) + self.requirement_check_validation = self.progress.add_task( + self.REQUIREMENT_CHECK_VALIDATION, total=stats.get("total_checks")) + self.__layout = layout + super().__init__("ProgressMonitor") + + def start(self): + self.progress.start() + + def stop(self): + self.progress.stop() + + @property + def layout(self) -> ValidationReportLayout: + return self.__layout + + @property + def progress(self) -> Progress: + return self.__progress + + def update(self, event: Event): + # logger.debug("Event: %s", event.event_type) + if event.event_type == EventType.PROFILE_VALIDATION_START: + logger.debug("Profile validation start") + elif event.event_type == EventType.REQUIREMENT_VALIDATION_START: + logger.debug("Requirement validation start") + elif event.event_type == EventType.REQUIREMENT_CHECK_VALIDATION_START: + logger.debug("Requirement check validation start") + elif event.event_type == EventType.REQUIREMENT_CHECK_VALIDATION_END: + if not event.requirement_check.requirement.hidden: + self.progress.update(task_id=self.requirement_check_validation, advance=1) + if event.validation_result is not None: + if event.validation_result: + self._stats["passed_checks"].append(event.requirement_check) + else: + self._stats["failed_checks"].append(event.requirement_check) + self.layout.update(self._stats) + elif event.event_type == EventType.REQUIREMENT_VALIDATION_END: + if not event.requirement.hidden: + self.progress.update(task_id=self.requirement_validation, advance=1) + if event.validation_result: + self._stats["passed_requirements"].append(event.requirement) + else: + self._stats["failed_requirements"].append(event.requirement) + self.layout.update(self._stats) + elif event.event_type == EventType.PROFILE_VALIDATION_END: + self.progress.update(task_id=self.profile_validation, advance=1) + elif event.event_type == EventType.VALIDATION_END: + self.layout.set_overall_result(event.validation_result) + + +class ValidationReportLayout(Layout): + + def __init__(self, console: Console, validation_settings: dict, profile_stats: dict, result: ValidationResult): + super().__init__() + self.console = console + self.validation_settings = validation_settings + self.profile_stats = profile_stats + self.result = result + self.__layout = None + self._validation_checks_progress = None + self.__progress_monitor = None + self.requirement_checks_container_layout = None + self.passed_checks = None + self.failed_checks = None + self.report_details_container = None + + @property + def layout(self): + if not self.__layout: + self.__init_layout__() + return self.__layout + + @property + def validation_checks_progress(self): + return self._validation_checks_progress + + @property + def progress_monitor(self) -> ProgressMonitor: + if not self.__progress_monitor: + self.__progress_monitor = ProgressMonitor(self, self.profile_stats) + return self.__progress_monitor + + def live(self, update_callable: callable) -> any: + assert update_callable, "Update callable must be provided" + # Start live rendering + result = None + with Live(self.layout, console=self.console, refresh_per_second=10, transient=False): + result = update_callable() + return result + + def __init_layout__(self): + + # Get the validation settings + settings = self.validation_settings + + # Set the console height + self.console.height = 27 + + # Create the layout of the base info of the validation report + base_info_layout = Layout( + Align( + f"\n[bold cyan]RO-Crate:[/bold cyan] [bold]{URI(settings['data_path']).uri}[/bold]" + f"\n[bold cyan]Target Profile:[/bold cyan][bold magenta] {settings['profile_identifier']}[/bold magenta]", + style="white", align="left"), + name="Base Info", size=4) + + # + self.passed_checks = Layout(name="PASSED") + self.failed_checks = Layout(name="FAILED") + # 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 + ) + + # Create the layout of the requirement checks section + requirement_checks_container_layout = Layout(name="Requirement Checks") + requirement_checks_container_layout.split_column( + self.requirement_checks_container_layout, + validated_checks_container + ) + + # Create the layout of the requirement checks section + self.requirement_checks_container_layout = Layout(name="Requirement Checks Validation", size=5) + self.requirement_checks_container_layout.split_row( + Layout(name="required"), + Layout(name="recommended"), + Layout(name="optional") + ) + + # Create the layout of the validation checks progress + self._validation_checks_progress = Layout( + Panel(Align(self.progress_monitor.progress, align="center"), + border_style="white", padding=(0, 1), title="Overall Progress"), + name="Validation Progress", size=5) + + # Create the layout of the report container + report_container_layout = Layout(name="Report Container Layout") + report_container_layout.split_column( + base_info_layout, + Layout(Panel(requirement_checks_container_layout, + title="[bold]Requirements Checks Validation[/bold]", border_style="white", padding=(1, 1))), + self._validation_checks_progress + ) + + # Update the layout with the profile stats + self.update(self.profile_stats) + + # Create the main layout + layout = Layout( + Panel(report_container_layout, title=f"[bold]RO-Crate Validator[/bold] [white](ver. [magenta]{get_version()}[/magenta])[/white]", + border_style="cyan", title_align="center", padding=(1, 1)), size=25) + + # Create the overall result layout + self.overall_result = Layout(Padding(Rule(f"\n[italic][cyan]Validating ROCrate...[/cyan][/italic]"), (1, 1))) + + # Set the layout as a group container + self.__layout = Group(layout, self.overall_result) + + def update(self, profile_stats: dict = None): + assert profile_stats, "Profile stats must be provided" + self.profile_stats = profile_stats + self.requirement_checks_container_layout["required"].update( + Panel( + Align( + str(profile_stats['check_count_by_severity'][Severity.REQUIRED]) if profile_stats else "0", + align="center" + ), + padding=(1, 1), + title="Severity: REQUIRED", + title_align="center", + border_style="RED" ) - else: - console.print( - Padding(f"[bold][[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", (0, 2)), - style="white", + ) + self.requirement_checks_container_layout["recommended"].update( + Panel( + Align( + str(profile_stats['check_count_by_severity'][Severity.RECOMMENDED]) if profile_stats else "0", + align="center" + ), + padding=(1, 1), + title="Severity: RECOMMENDED", + title_align="center", + border_style="orange1" + ) + ) + self.requirement_checks_container_layout["optional"].update( + Panel( + Align( + str(profile_stats['check_count_by_severity'][Severity.OPTIONAL]) if profile_stats else "0", + align="center" + ), + padding=(1, 1), + title="Severity: OPTIONAL", + title_align="center", + border_style="yellow" + ) + ) + + self.passed_checks.update( + Panel( + Align( + str(len(self.profile_stats["passed_checks"])), + align="center" + ), + padding=(1, 1), + title="PASSED Checks", + title_align="center", + border_style="green" + ) + ) + + self.failed_checks.update( + Panel( + Align( + str(len(self.profile_stats["failed_checks"])), + align="center" + ), + padding=(1, 1), + title="FAILED Checks", + title_align="center", + border_style="red" ) + ) + + def set_overall_result(self, result: ValidationResult): + assert result, "Validation result must be provided" + self.result = result + if result.passed(): + self.overall_result.update( + Padding(Rule(f"[bold][[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", + style="bold green"), (1, 0))) + else: + self.overall_result.update( + Padding(Rule(f"[bold][[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", + style="bold red"), (0, 0))) - console.print(Padding("\n[bold]The following requirements have not meet: [/bold]", (0, 2)), style="white") + def show_validation_details(self, enable_pager: bool = True): + """ + Print the validation result + """ + if not self.result: + raise ValueError("Validation result is not available") + # init references to the console, result and severity + console = self.console + result = self.result + + # Print validation details + with console.pager(styles=True) if enable_pager else console: + # Print the list of failed requirements + console.print( + Padding("\n[bold]The following requirements have not meet: [/bold]", (0, 0)), style="white") for requirement in sorted(result.failed_requirements, key=lambda x: (-x.severity.value, x)): issue_color = get_severity_color(requirement.severity) @@ -227,7 +480,7 @@ def __print_validation_result__( ) console.print(Padding(Markdown(requirement.description), (1, 7))) console.print(Padding("[white bold u] Failed checks [/white bold u]\n", - (0, 8)), style="white bold") + (0, 8)), style="white bold") for check in sorted(result.get_failed_checks_by_requirement(requirement), key=lambda x: (-x.severity.value, x)): @@ -235,7 +488,7 @@ def __print_validation_result__( console.print( Padding(f"[bold][{issue_color}][{check.identifier.center(16)}][/{issue_color}] [magenta]{check.name}[/magenta][/bold]:", (0, 7)), style="white bold") console.print(Padding(Markdown(check.description), (0, 27))) - console.print(Padding("[u]Detected issues[/u]", (0, 8)), style="white bold") + console.print(Padding("[u] Detected issues [/u]", (0, 8)), style="white bold") for issue in sorted(result.get_issues_by_check(check), key=lambda x: (-x.severity.value, x)): path = "" @@ -250,3 +503,59 @@ def __print_validation_result__( Padding(f"- [[red]Violation[/red] {path}]: " f"{Markdown(issue.message).markup}", (0, 9)), style="white") console.print("\n", style="white") + + +def __compute_profile_stats__(validation_settings: dict): + """ + Compute the statistics of the profile + """ + profiles = services.get_profiles(validation_settings.get("profiles_path")) + profile = services.get_profile(validation_settings.get("profiles_path"), + validation_settings.get("profile_identifier")) + severity_validation = Severity.get(validation_settings.get("requirement_severity")) + profiles = [profile] + + if validation_settings.get("inherit_profiles"): + profiles.extend(profile.inherited_profiles) + + total_requirements = 0 + total_checks = 0 + requirement_count_by_severity = {} + check_count_by_severity = {} + + for profile in profiles: + for requirement in profile.requirements: + if requirement.hidden: + continue + + severity = requirement.severity + + # Count the number of requirements by severity + if severity not in requirement_count_by_severity: + requirement_count_by_severity[severity] = 0 + + if severity_validation <= severity: + requirement_count_by_severity[severity] += 1 + total_requirements += 1 + + # Count the number of checks by severity + if severity not in check_count_by_severity: + check_count_by_severity[severity] = 0 + if severity_validation <= severity: + num_checks = len( + requirement.get_checks_by_level(LevelCollection.get(severity.name))) + check_count_by_severity[severity] += num_checks + total_checks += num_checks + + return { + "profile": profile, + "profiles": profiles, + "requirement_count_by_severity": requirement_count_by_severity, + "check_count_by_severity": check_count_by_severity, + "total_requirements": total_requirements, + "total_checks": total_checks, + "failed_requirements": [], + "failed_checks": [], + "passed_requirements": [], + "passed_checks": [] + } From 4ba25e86f89f0fde5322c3aee11d8c408e1a71f1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 19 Jul 2024 16:30:47 +0200 Subject: [PATCH 657/902] feat(cli): :sparkles: add details CLI option --- rocrate_validator/cli/commands/validate.py | 73 +++++++++++++++++----- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 1ecb1fc4..a37a4c11 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -1,6 +1,10 @@ +from __future__ import annotations + import os import sys +import termios import textwrap +import tty from pathlib import Path from typing import Optional @@ -12,7 +16,6 @@ from rich.padding import Padding from rich.panel import Panel from rich.progress import BarColumn, Progress, TextColumn, TimeElapsedColumn -from rich.prompt import Confirm from rich.rule import Rule import rocrate_validator.log as logging @@ -56,6 +59,17 @@ def validate_uri(ctx, param, value): return value +def get_single_char(): + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + char = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return char + + @cli.command("validate") @click.argument("rocrate-uri", callback=validate_uri, default=".") @click.option( @@ -105,10 +119,17 @@ def validate_uri(ctx, param, value): default=False, show_default=True ) +@click.option( + '--details', + is_flag=True, + help="Output the validation details without prompting", + default=False, + show_default=True +) @click.option( '--no-paging', is_flag=True, - help="Disable paging", + help="Disable pagination of the validation details", default=False, show_default=True ) @@ -122,7 +143,8 @@ def validate(ctx, rocrate_uri: Path = ".", no_fail_fast: bool = False, ontologies_path: Optional[Path] = None, - no_paging: bool = False): + no_paging: bool = False, + details: bool = False): """ [magenta]rocrate-validator:[/magenta] Validate a RO-Crate against a profile """ @@ -145,23 +167,40 @@ def validate(ctx, if rocrate_uri: logger.debug("rocrate_path: %s", os.path.abspath(rocrate_uri)) - # Validate the RO-Crate try: - result: ValidationResult = services.validate( - { - "profiles_path": profiles_path, - "profile_identifier": profile_identifier, - "requirement_severity": requirement_severity, - "requirement_severity_only": requirement_severity_only, - "inherit_profiles": not disable_profile_inheritance, - "data_path": rocrate_uri, - "ontology_path": Path(ontologies_path).absolute() if ontologies_path else None, - "abort_on_first": not no_fail_fast - } + # Validation settings + validation_settings = { + "profiles_path": profiles_path, + "profile_identifier": profile_identifier, + "requirement_severity": requirement_severity, + "requirement_severity_only": requirement_severity_only, + "inherit_profiles": not disable_profile_inheritance, + "show_details": False, + "details": details, + "data_path": rocrate_uri, + "ontology_path": Path(ontologies_path).absolute() if ontologies_path else None, + "abort_on_first": not no_fail_fast + } + + # Compute the profile statistics + profile_stats = __compute_profile_stats__(validation_settings) + + report_layout = ValidationReportLayout(console, validation_settings, profile_stats, None) + + # Validate RO-Crate against the profile and get the validation result + result: ValidationResult = report_layout.live( + lambda: services.validate( + validation_settings, + subscribers=[report_layout.progress_monitor] + ) ) # Print the validation result - __print_validation_result__(console, result, result.context.requirement_severity, enable_pager=enable_pager) + if not details and enable_pager: + console.print("[bold]Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]", end="") + details = get_single_char().lower() == 'y' + if details: + report_layout.show_validation_details(enable_pager=enable_pager) # using ctx.exit seems to raise an Exception that gets caught below, # so we use sys.exit instead. @@ -190,7 +229,7 @@ def validate(ctx, 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)) + "Please report it to the issue tracker along with the following stack trace:\n", ' ' * 9)) console.print_exception() sys.exit(2) From 40d15b0f5f2cc183ef11a8bbe5eea6f3c9e15019 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 19 Jul 2024 16:31:20 +0200 Subject: [PATCH 658/902] refactor(cli): :lipstick: reformat error message --- rocrate_validator/cli/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index 293b4909..e518725b 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -58,7 +58,8 @@ def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool): 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 + 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() From 5e894d290b712e7695a9adc1fad61dea03b48a64 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 19 Jul 2024 16:32:05 +0200 Subject: [PATCH 659/902] feat(services): :sparkles: allow to automatically register subscribers --- rocrate_validator/services.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 65027ae0..afb68fca 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -2,17 +2,17 @@ import tempfile import zipfile from pathlib import Path -from typing import Union +from typing import Optional, Union import requests import requests_cache import rocrate_validator.log as logging from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER - -from .models import (Profile, Severity, ValidationResult, ValidationSettings, - Validator) -from .utils import URI, get_profiles_path +from rocrate_validator.events import Subscriber +from rocrate_validator.models import (Profile, Severity, ValidationResult, + ValidationSettings, Validator) +from rocrate_validator.utils import URI, get_profiles_path # set the default profiles path DEFAULT_PROFILES_PATH = get_profiles_path() @@ -21,7 +21,8 @@ logger = logging.getLogger(__name__) -def validate(settings: Union[dict, ValidationSettings]) -> ValidationResult: +def validate(settings: Union[dict, ValidationSettings], + subscribers: Optional[list[Subscriber]] = None) -> ValidationResult: """ Validate a RO-Crate against a profile """ @@ -54,6 +55,9 @@ def validate(settings: Union[dict, ValidationSettings]) -> ValidationResult: # create a validator validator = Validator(settings) logger.debug("Validator created. Starting validation...") + if subscribers: + for subscriber in subscribers: + validator.add_subscriber(subscriber) # validate the RO-Crate result = validator.validate() logger.debug("Validation completed: %s", result) @@ -63,6 +67,9 @@ def __do_validate__(settings: ValidationSettings): # create a validator validator = Validator(settings) logger.debug("Validator created. Starting validation...") + if subscribers: + for subscriber in subscribers: + validator.add_subscriber(subscriber) # validate the RO-Crate result = validator.validate() logger.debug("Validation completed: %s", result) From 411ee46d889e67b2d458350f3a392afa0403ef14 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 19 Jul 2024 17:14:55 +0200 Subject: [PATCH 660/902] feat(cli): :goal_net: centralise error handling --- rocrate_validator/cli/commands/errors.py | 43 +++++++ rocrate_validator/cli/commands/profiles.py | 134 +++++++++++---------- rocrate_validator/cli/commands/validate.py | 30 +---- 3 files changed, 117 insertions(+), 90 deletions(-) create mode 100644 rocrate_validator/cli/commands/errors.py diff --git a/rocrate_validator/cli/commands/errors.py b/rocrate_validator/cli/commands/errors.py new file mode 100644 index 00000000..1c4db0e8 --- /dev/null +++ b/rocrate_validator/cli/commands/errors.py @@ -0,0 +1,43 @@ +import sys +import textwrap + +from rich.console import Console + +import rocrate_validator.log as logging +from rocrate_validator.errors import InvalidProfilePath, ProfileNotFound, ProfilesDirectoryNotFound + +# Create a logger for this module +logger = logging.getLogger(__name__) + + +def handle_error(e: Exception, console: Console) -> None: + error_message = "" + if isinstance(e, ProfilesDirectoryNotFound): + error_message = f""" + The profile folder could not be located at the specified path: [red]{e.profiles_path}[/red]. + Please ensure that the path is correct and try again. + """ + elif isinstance(e, ProfileNotFound): + error_message = f"""The profile with the identifier "[red bold]{e.profile_name}[/red bold]" could not be found. + Please ensure that the profile exists and try again. + + To see the available profiles, run: + [cyan bold]rocrate-validator profiles list[/cyan bold] + """ + elif isinstance(e, InvalidProfilePath): + error_message = f"""The profile path "[red bold]{e.profile_path}[/red bold]" is not valid. + Please ensure that the profile exists and try again. + + 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)) + console.print_exception() + + console.print(f"\n\n[bold][[red]ERROR[/red]] {error_message}[/bold]\n", style="white") + sys.exit(2) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 90ffe80a..84a29ce5 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -7,6 +7,7 @@ import rocrate_validator.log as logging from rocrate_validator import services +from rocrate_validator.cli.commands.errors import handle_error from rocrate_validator.cli.main import cli, click from rocrate_validator.cli.utils import get_app_header_rule from rocrate_validator.colors import get_severity_color @@ -55,53 +56,58 @@ def list_profiles(ctx, no_paging: bool = False): # , profiles_path: Path = DEFA profiles_path = ctx.obj['profiles_path'] console = ctx.obj['console'] enable_pager = not no_paging - # Get the profiles - profiles = services.get_profiles(profiles_path=profiles_path) - table = Table(show_header=True, - title=" Available profiles", - title_style="italic bold cyan", - title_justify="left", - header_style="bold cyan", - border_style="bright_black", - show_footer=False, - caption_style="italic bold", - caption="[cyan](*)[/cyan] Number of requirements by severity") + try: + # Get the profiles + profiles = services.get_profiles(profiles_path=profiles_path) - # Define columns - table.add_column("Identifier", style="magenta bold", justify="center") - table.add_column("URI", style="yellow bold", justify="center") - table.add_column("Version", style="green bold", justify="center") - table.add_column("Name", style="white bold", justify="center") - table.add_column("Description", style="white italic") - table.add_column("Based on", style="white", justify="center") - table.add_column("Requirements (*)", style="white", justify="center") + table = Table(show_header=True, + title=" Available profiles", + title_style="italic bold cyan", + title_justify="left", + header_style="bold cyan", + border_style="bright_black", + show_footer=False, + caption_style="italic bold", + caption="[cyan](*)[/cyan] Number of requirements by severity") - # Add data to the table - for profile in profiles: - # Count requirements by severity - requirements = {} - logger.debug("Requirements: %s", requirements) - for req in profile.requirements: - if not requirements.get(req.severity.name, None): - requirements[req.severity.name] = 0 - requirements[req.severity.name] += 1 - requirements = "\n".join( - [f"[bold][{get_severity_color(severity)}]{severity}: " - f"{count}[/{get_severity_color(severity)}][/bold]" - for severity, count in requirements.items() if count > 0]) - - # Add the row to the table - table.add_row(profile.identifier, profile.uri, profile.version, - profile.name, Markdown(profile.description.strip()), - "\n".join([p.identifier for p in profile.inherited_profiles]), - requirements) - table.add_row() - - # Print the table - with console.pager(styles=True) if enable_pager else console: - console.print(get_app_header_rule()) - console.print(Padding(table, (0, 1))) + # Define columns + table.add_column("Identifier", style="magenta bold", justify="center") + table.add_column("URI", style="yellow bold", justify="center") + table.add_column("Version", style="green bold", justify="center") + table.add_column("Name", style="white bold", justify="center") + table.add_column("Description", style="white italic") + table.add_column("Based on", style="white", justify="center") + table.add_column("Requirements (*)", style="white", justify="center") + + # Add data to the table + for profile in profiles: + # Count requirements by severity + requirements = {} + logger.debug("Requirements: %s", requirements) + for req in profile.requirements: + if not requirements.get(req.severity.name, None): + requirements[req.severity.name] = 0 + requirements[req.severity.name] += 1 + requirements = "\n".join( + [f"[bold][{get_severity_color(severity)}]{severity}: " + f"{count}[/{get_severity_color(severity)}][/bold]" + for severity, count in requirements.items() if count > 0]) + + # Add the row to the table + table.add_row(profile.identifier, profile.uri, profile.version, + profile.name, Markdown(profile.description.strip()), + "\n".join([p.identifier for p in profile.inherited_profiles]), + requirements) + table.add_row() + + # Print the table + with console.pager(styles=True) if enable_pager else console: + console.print(get_app_header_rule()) + console.print(Padding(table, (0, 1))) + + except Exception as e: + handle_error(e, console) @profiles.command("describe") @@ -134,28 +140,32 @@ def describe_profile(ctx, # Get the no_paging flag enable_pager = not no_paging - # Get the profile - profile = services.get_profile(profiles_path=profiles_path, profile_identifier=profile_identifier) + try: + # Get the profile + profile = services.get_profile(profiles_path=profiles_path, profile_identifier=profile_identifier) + + # Set the subheader title + subheader_title = f"[bold][cyan]Profile:[/cyan] [magenta italic]{profile.identifier}[/magenta italic][/bold]" - # Set the subheader title - subheader_title = f"[bold][cyan]Profile:[/cyan] [magenta italic]{profile.identifier}[/magenta italic][/bold]" + # Set the subheader content + subheader_content = f"[bold cyan]Version:[/bold cyan] [italic green]{profile.version}[/italic green]\n" + subheader_content += f"[bold cyan]URI:[/bold cyan] [italic yellow]{profile.uri}[/italic yellow]\n\n" + subheader_content += f"[bold cyan]Description:[/bold cyan] [italic]{profile.description.strip()}[/italic]" - # Set the subheader content - subheader_content = f"[bold cyan]Version:[/bold cyan] [italic green]{profile.version}[/italic green]\n" - subheader_content += f"[bold cyan]URI:[/bold cyan] [italic yellow]{profile.uri}[/italic yellow]\n\n" - subheader_content += f"[bold cyan]Description:[/bold cyan] [italic]{profile.description.strip()}[/italic]" + # Build the profile table + if not verbose: + table = __compacted_describe_profile__(profile) + else: + table = __verbose_describe_profile__(profile) - # Build the profile table - if not verbose: - table = __compacted_describe_profile__(profile) - else: - table = __verbose_describe_profile__(profile) + with console.pager(styles=True) if enable_pager else console: + console.print(get_app_header_rule()) + console.print(Padding(Panel(subheader_content, title=subheader_title, padding=(1, 1), + title_align="left", border_style="cyan"), (0, 1, 0, 1))) + console.print(Padding(table, (1, 1))) - with console.pager(styles=True) if enable_pager else console: - console.print(get_app_header_rule()) - console.print(Padding(Panel(subheader_content, title=subheader_title, padding=(1, 1), - title_align="left", border_style="cyan"), (0, 1, 0, 1))) - console.print(Padding(table, (1, 1))) + except Exception as e: + handle_error(e, console) def __requirement_level_style__(requirement: RequirementLevel): diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index a37a4c11..095bcd2e 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -3,7 +3,6 @@ import os import sys import termios -import textwrap import tty from pathlib import Path from typing import Optional @@ -20,10 +19,10 @@ import rocrate_validator.log as logging from rocrate_validator import services +from rocrate_validator.cli.commands.errors import handle_error from rocrate_validator.cli.main import cli, click from rocrate_validator.colors import get_severity_color from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER -from rocrate_validator.errors import ProfileNotFound, ProfilesDirectoryNotFound from rocrate_validator.events import Event, EventType, Subscriber from rocrate_validator.models import (LevelCollection, Severity, ValidationResult) @@ -205,33 +204,8 @@ def validate(ctx, # using ctx.exit seems to raise an Exception that gets caught below, # so we use sys.exit instead. sys.exit(0 if result.passed(LevelCollection.get(requirement_severity).severity) else 1) - except ProfilesDirectoryNotFound as e: - error_message = f""" - The profile folder could not be located at the specified path: [red]{e.profiles_path}[/red]. - Please ensure that the path is correct and try again. - """ - console.print( - f"\n\n[bold][[red]ERROR[/red]] {error_message} !!![/bold]\n", style="white") - sys.exit(2) - except ProfileNotFound as e: - error_message = f"""The profile with the identifier "[red bold]{e.profile_name}[/red bold]" could not be found. - Please ensure that the profile exists and try again. - - To see the available profiles, run: - [cyan bold]rocrate-validator profiles list[/cyan bold] - """ - console.print( - f"\n\n[bold][[red]ERROR[/red]] {error_message}[/bold]\n", style="white") - sys.exit(2) except Exception as e: - console.print( - f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", style="white") - 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)) - console.print_exception() - sys.exit(2) + handle_error(e, console) class ProgressMonitor(Subscriber): From 5daf654f22a4cce60681628034c7f19b0ccd6915 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 19 Jul 2024 17:15:48 +0200 Subject: [PATCH 661/902] feat(services): :goal_net: fix exception type --- rocrate_validator/services.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index afb68fca..e8a79b8d 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -9,6 +9,7 @@ import rocrate_validator.log as logging from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER +from rocrate_validator.errors import ProfileNotFound from rocrate_validator.events import Subscriber from rocrate_validator.models import (Profile, Severity, ValidationResult, ValidationSettings, Validator) @@ -142,8 +143,8 @@ def get_profile(profiles_path: Path = DEFAULT_PROFILES_PATH, Load the profiles from the given path """ profile_path = profiles_path / profile_identifier - if not Path(profiles_path).exists(): - raise FileNotFoundError(f"Profile not found: {profile_path}") + if not Path(profile_path).exists(): + raise ProfileNotFound(profile_identifier) profile = Profile.load(profiles_path, f"{profiles_path}/{profile_identifier}", publicID=publicID, severity=Severity.OPTIONAL) logger.debug("Profile loaded: %s", profile) From ff4cafe1b8003f1bee4fbefa9db64887efcde7f4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 19 Jul 2024 17:41:59 +0200 Subject: [PATCH 662/902] fix(cli): :lipstick: fix layout --- rocrate_validator/cli/commands/validate.py | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 095bcd2e..e68ad537 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -343,20 +343,20 @@ def __init_layout__(self): ) # Create the layout of the requirement checks section - requirement_checks_container_layout = Layout(name="Requirement Checks") - requirement_checks_container_layout.split_column( - self.requirement_checks_container_layout, - validated_checks_container - ) - - # Create the layout of the requirement checks section - self.requirement_checks_container_layout = Layout(name="Requirement Checks Validation", size=5) - self.requirement_checks_container_layout.split_row( + self.requirement_checks_by_severity_container_layout = Layout(name="Requirement Checks Validation", size=5) + self.requirement_checks_by_severity_container_layout.split_row( Layout(name="required"), Layout(name="recommended"), Layout(name="optional") ) + # Create the layout of the requirement checks section + requirement_checks_container_layout = Layout(name="Requirement Checks") + requirement_checks_container_layout.split_column( + self.requirement_checks_by_severity_container_layout, + validated_checks_container + ) + # Create the layout of the validation checks progress self._validation_checks_progress = Layout( Panel(Align(self.progress_monitor.progress, align="center"), @@ -389,7 +389,7 @@ def __init_layout__(self): def update(self, profile_stats: dict = None): assert profile_stats, "Profile stats must be provided" self.profile_stats = profile_stats - self.requirement_checks_container_layout["required"].update( + self.requirement_checks_by_severity_container_layout["required"].update( Panel( Align( str(profile_stats['check_count_by_severity'][Severity.REQUIRED]) if profile_stats else "0", @@ -401,7 +401,7 @@ def update(self, profile_stats: dict = None): border_style="RED" ) ) - self.requirement_checks_container_layout["recommended"].update( + self.requirement_checks_by_severity_container_layout["recommended"].update( Panel( Align( str(profile_stats['check_count_by_severity'][Severity.RECOMMENDED]) if profile_stats else "0", @@ -413,7 +413,7 @@ def update(self, profile_stats: dict = None): border_style="orange1" ) ) - self.requirement_checks_container_layout["optional"].update( + self.requirement_checks_by_severity_container_layout["optional"].update( Panel( Align( str(profile_stats['check_count_by_severity'][Severity.OPTIONAL]) if profile_stats else "0", From c22a1042c9ac9d611eab00b526b65cedbfaa729d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 19 Jul 2024 18:02:31 +0200 Subject: [PATCH 663/902] fix(cli): :lipstick: fix input reader --- rocrate_validator/cli/commands/validate.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index e68ad537..7bc527dc 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -58,7 +58,10 @@ def validate_uri(ctx, param, value): return value -def get_single_char(): +def get_single_char(console: Optional[Console] = None, end: str = "\n") -> str: + """ + Get a single character from the console + """ fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: @@ -66,6 +69,8 @@ def get_single_char(): char = sys.stdin.read(1) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + if console: + console.print(char, end=end) return char @@ -197,7 +202,7 @@ def validate(ctx, # Print the validation result if not details and enable_pager: console.print("[bold]Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]", end="") - details = get_single_char().lower() == 'y' + details = get_single_char(console).lower() == 'y' if details: report_layout.show_validation_details(enable_pager=enable_pager) From f5eb429386ac9f609a81b7517c2029c16da80d2e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 19 Jul 2024 18:33:15 +0200 Subject: [PATCH 664/902] feat(core): :sparkles: add methods `to_dict` and `to_json` methods on `ValidationResult` --- rocrate_validator/models.py | 64 ++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 359fad04..9e8c0f7d 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -3,6 +3,7 @@ import bisect import enum import inspect +import json import re from abc import ABC, abstractmethod from collections.abc import Collection @@ -541,7 +542,7 @@ def get_check(self, name: str) -> Optional[RequirementCheck]: return None def get_checks_by_level(self, level: RequirementLevel) -> list[RequirementCheck]: - return [check for check in self._checks if check.level.severity == level.severity] + return list({check for check in self._checks if check.level.severity == level.severity}) def __reorder_checks__(self) -> None: for i, check in enumerate(self._checks): @@ -876,6 +877,28 @@ def __lt__(self, other: object) -> bool: raise TypeError(f"Cannot compare {type(self)} with {type(other)}") return (self._check, self._severity, self._message) < (other._check, other._severity, other._message) + def __hash__(self) -> int: + return hash((self._check, self._severity, self._message)) + + def __repr__(self) -> str: + return f'CheckIssue(severity={self.severity}, check={self.check}, message={self.message})' + + def __str__(self) -> str: + return f"{self.severity}: {self.message} ({self.check})" + + def to_dict(self) -> dict: + return { + "severity": self.severity.name, + "message": self.message, + "check": self.check.name, + "resultPath": self.resultPath, + "focusNode": self.focusNode, + "value": self.value + } + + def to_json(self) -> str: + return json.dumps(self.to_dict(), indent=4, cls=CustomEncoder) + # @property # def code(self) -> int: # breakpoint() @@ -989,6 +1012,45 @@ def __str__(self) -> str: def __repr__(self): return f"ValidationResult(issues={self._issues})" + 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 + + def to_dict(self) -> dict: + return { + "rocrate": str(self.rocrate_path), + "validation_settings": self.validation_settings, + "passed": self.passed(self.context.settings["requirement_severity"]), + "issues": [issue.to_dict() for issue in self.issues] + } + + def to_json(self, path: Optional[Path] = None) -> str: + + result = json.dumps(self.to_dict(), indent=4, cls=CustomEncoder) + if path: + with open(path, "w") as f: + f.write(result) + return result + + +class CustomEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, CheckIssue): + return obj.__dict__ + if isinstance(obj, Path): + return str(obj) + if isinstance(obj, Severity): + return obj.name + if isinstance(obj, RequirementCheck): + return obj.identifier + if isinstance(obj, Requirement): + return obj.identifier + if isinstance(obj, RequirementLevel): + return obj.name + + return super().default(obj) + @dataclass class ValidationSettings: From 08a60280f37f850fb883a0ce113e06a1b0980b9a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 19 Jul 2024 18:43:15 +0200 Subject: [PATCH 665/902] feat(cli): :sparkles: add options to write the validation output to a file Both text and JSON format are supported --- rocrate_validator/cli/commands/validate.py | 33 +++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 7bc527dc..839efa78 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import os import sys import termios @@ -137,6 +138,22 @@ def get_single_char(console: Optional[Console] = None, end: str = "\n") -> str: default=False, show_default=True ) +@click.option( + '-f', + '--format', + type=click.Choice(["json", "text"], case_sensitive=False), + default="text", + show_default=True, + help="Output format of the validation report" +) +@click.option( + '-o', + '--output-file', + type=click.Path(), + default=None, + show_default=True, + help="Path to the output file for the validation report", +) @click.pass_context def validate(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH, @@ -148,7 +165,9 @@ def validate(ctx, no_fail_fast: bool = False, ontologies_path: Optional[Path] = None, no_paging: bool = False, - details: bool = False): + details: bool = False, + format: str = "text", + output_file: Optional[Path] = None): """ [magenta]rocrate-validator:[/magenta] Validate a RO-Crate against a profile """ @@ -206,6 +225,18 @@ def validate(ctx, if details: report_layout.show_validation_details(enable_pager=enable_pager) + if output_file: + # Print the validation report to a file + if format == "json": + with open(output_file, "w") as f: + f.write(result.to_json()) + elif format == "text": + with open(output_file, "w") as f: + c = Console(file=f, color_system=None) + c.print(report_layout.layout) + report_layout.console = c + report_layout.show_validation_details(enable_pager=False) + # using ctx.exit seems to raise an Exception that gets caught below, # so we use sys.exit instead. sys.exit(0 if result.passed(LevelCollection.get(requirement_severity).severity) else 1) From 00fb7fbf9c811ecd17a651e8504d2606ca32780d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 19 Jul 2024 19:08:26 +0200 Subject: [PATCH 666/902] fix(cli): :bug: skip details when when the validation is ok --- rocrate_validator/cli/commands/validate.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 839efa78..c6fafa81 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -219,11 +219,12 @@ def validate(ctx, ) # Print the validation result - if not details and enable_pager: - console.print("[bold]Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]", end="") - details = get_single_char(console).lower() == 'y' - if details: - report_layout.show_validation_details(enable_pager=enable_pager) + if not result.passed(LevelCollection.get(requirement_severity).severity): + if not details and enable_pager: + console.print("[bold]Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]", end="") + details = get_single_char(console).lower() == 'y' + if details: + report_layout.show_validation_details(enable_pager=enable_pager) if output_file: # Print the validation report to a file From 1adaf6c4f2ee0061e4dfd492243c39c543a3eb29 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 19 Jul 2024 20:01:56 +0200 Subject: [PATCH 667/902] fix(services): :recycle: extend `get_profile`service --- rocrate_validator/services.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index e8a79b8d..2ae52eec 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -142,10 +142,9 @@ def get_profile(profiles_path: Path = DEFAULT_PROFILES_PATH, """ Load the profiles from the given path """ - profile_path = profiles_path / profile_identifier - if not Path(profile_path).exists(): - raise ProfileNotFound(profile_identifier) - profile = Profile.load(profiles_path, f"{profiles_path}/{profile_identifier}", - publicID=publicID, severity=Severity.OPTIONAL) - logger.debug("Profile loaded: %s", profile) + profiles = get_profiles(profiles_path, publicID=publicID) + 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) + if not profile: + raise ProfileNotFound(f"Profile not found: {profile_identifier}") return profile From bcdcb160ec8ac5b9c88c9d175322755bd143409a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 19 Jul 2024 20:02:57 +0200 Subject: [PATCH 668/902] test(cli): :white_check_mark: update CLI tests --- tests/test_cli.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index ee896b6e..273a4eb4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -25,25 +25,26 @@ def test_version(cli_runner: CliRunner): def test_validate_subcmd_invalid_rocrate1(cli_runner: CliRunner): - result = cli_runner.invoke(cli, ['validate', str(InvalidFileDescriptor().invalid_json_format)]) + result = cli_runner.invoke(cli, ['validate', str( + InvalidFileDescriptor().invalid_json_format), '--details', '--no-paging']) + logger.error(result.output) assert result.exit_code == 1 def test_validate_subcmd_valid_local_folder_rocrate(cli_runner: CliRunner): - result = cli_runner.invoke(cli, ['validate', str(ValidROC().wrroc_paper_long_date)]) + result = cli_runner.invoke(cli, ['validate', str(ValidROC().wrroc_paper_long_date), '--details', '--no-paging']) assert result.exit_code == 0 assert re.search(r'RO-Crate.*is valid', result.output) def test_validate_subcmd_valid_remote_rocrate(cli_runner: CliRunner): result = cli_runner.invoke( - cli, ['validate', str(ValidROC().sort_and_change_remote)]) + cli, ['validate', str(ValidROC().sort_and_change_remote), '--details', '--no-paging']) assert result.exit_code == 0 - logger.error(result.output) assert re.search(r'RO-Crate.*is valid', result.output) def test_validate_subcmd_invalid_local_archive_rocrate(cli_runner: CliRunner): - result = cli_runner.invoke(cli, ['validate', str(ValidROC().sort_and_change_archive)]) + result = cli_runner.invoke(cli, ['validate', str(ValidROC().sort_and_change_archive), '--details', '--no-paging']) assert result.exit_code == 0 assert re.search(r'RO-Crate.*is valid', result.output) From de23ca8e2fbbf1643824be25c6c1cf7d72421f82 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 22 Jul 2024 11:03:58 +0200 Subject: [PATCH 669/902] fix(core): :sparkles: use context severity by default --- rocrate_validator/cli/commands/validate.py | 2 +- rocrate_validator/models.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index c6fafa81..3fe956e0 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -219,7 +219,7 @@ def validate(ctx, ) # Print the validation result - if not result.passed(LevelCollection.get(requirement_severity).severity): + if not result.passed(): if not details and enable_pager: console.print("[bold]Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]", end="") details = get_single_char(console).lower() == 'y' diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 9e8c0f7d..d678b4c5 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -956,16 +956,19 @@ def get_issues(self, min_severity: Severity) -> list[CheckIssue]: def get_issues_by_check(self, check: RequirementCheck, - min_severity: Severity = Severity.OPTIONAL) -> list[CheckIssue]: + min_severity: Optional[Severity] = None) -> list[CheckIssue]: + min_severity = min_severity or self.context.requirement_severity return [issue for issue in self._issues if issue.check == check and issue.severity >= min_severity] # def get_issues_by_check_and_severity(self, check: RequirementCheck, severity: Severity) -> list[CheckIssue]: # return [issue for issue in self.issues if issue.check == check and issue.severity == severity] - def has_issues(self, severity: Severity = Severity.OPTIONAL) -> bool: + def has_issues(self, severity: Optional[Severity] = None) -> bool: + severity = severity or self.context.requirement_severity return any(issue.severity >= severity for issue in self._issues) - def passed(self, severity: Severity = Severity.OPTIONAL) -> bool: + def passed(self, severity: Optional[Severity] = None) -> bool: + severity = severity or self.context.requirement_severity return not any(issue.severity >= severity for issue in self._issues) def add_issue(self, issue: CheckIssue): @@ -1298,7 +1301,12 @@ def profiles_path(self) -> Path: @property def requirement_severity(self) -> Severity: - return self.settings.get("requirement_severity", Severity.REQUIRED) + severity = self.settings.get("requirement_severity", Severity.REQUIRED) + if isinstance(severity, str): + severity = Severity[severity] + elif not isinstance(severity, Severity): + raise ValueError(f"Invalid severity type: {type(severity)}") + return severity @property def requirement_severity_only(self) -> bool: From 4c44fd66eaa719df34ed76bd455ff6de8eade134 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 22 Jul 2024 11:08:10 +0200 Subject: [PATCH 670/902] test(test-conf): :white_check_mark: update remote crate URL --- tests/ro_crates.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 70f1de5b..2a819f69 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -34,8 +34,7 @@ def workflow_roc_string_license(self) -> Path: @property def sort_and_change_remote(self) -> Path: - # TODO: replace with a stable remote URL; this one might be deleted - return "https://dev.workflowhub.eu/workflows/161/ro_crate?version=1" + return "https://raw.githubusercontent.com/lifemonitor/validator-test-data/main/sortchangecase.crate.zip" @property def sort_and_change_archive(self) -> Path: From e003ac5c6a2bd909bf02205c077cd4deddb2fee3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 22 Jul 2024 11:17:06 +0200 Subject: [PATCH 671/902] fix(core): :bug: fix issue with the exception instantiation --- rocrate_validator/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 2ae52eec..b6d09533 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -146,5 +146,5 @@ def get_profile(profiles_path: Path = DEFAULT_PROFILES_PATH, 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) if not profile: - raise ProfileNotFound(f"Profile not found: {profile_identifier}") + raise ProfileNotFound(profile_identifier) return profile From 9b2f3f2fd84bf637870ab9734e558710125f071a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 22 Jul 2024 11:49:12 +0200 Subject: [PATCH 672/902] fix(cli): :lipstick: fix output layout --- rocrate_validator/cli/commands/validate.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 3fe956e0..031e99d0 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -359,7 +359,7 @@ def __init_layout__(self): settings = self.validation_settings # Set the console height - self.console.height = 27 + self.console.height = 30 # Create the layout of the base info of the validation report base_info_layout = Layout( @@ -413,15 +413,19 @@ def __init_layout__(self): self.update(self.profile_stats) # Create the main layout - layout = Layout( + self.checks_stats_layout = Layout( Panel(report_container_layout, title=f"[bold]RO-Crate Validator[/bold] [white](ver. [magenta]{get_version()}[/magenta])[/white]", - border_style="cyan", title_align="center", padding=(1, 1)), size=25) + border_style="cyan", title_align="center", padding=(1, 1))) # Create the overall result layout - self.overall_result = Layout(Padding(Rule(f"\n[italic][cyan]Validating ROCrate...[/cyan][/italic]"), (1, 1))) + self.overall_result = Layout( + Padding(Rule(f"\n[italic][cyan]Validating ROCrate...[/cyan][/italic]"), (1, 1)), size=3) - # Set the layout as a group container - self.__layout = Group(layout, self.overall_result) + group_layout = Layout() + group_layout.add_split(self.checks_stats_layout) + group_layout.add_split(self.overall_result) + + self.__layout = group_layout def update(self, profile_stats: dict = None): assert profile_stats, "Profile stats must be provided" From b66b1a47458773c2afd72bb6a0bc67509eff249d Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 22 Jul 2024 17:52:13 +0200 Subject: [PATCH 673/902] procrc: fix actionStatus; add checks for environment --- .../process-run-crate/may/1_create_action.ttl | 11 +- .../should/1_create_action.ttl | 20 ++- .../ro-crate-metadata.json | 2 +- .../ro-crate-metadata.json | 134 ++++++++++++++++++ .../ro-crate-metadata.json | 2 +- .../ro-crate-metadata.json | 116 +++++++++++++++ .../action_no_error/ro-crate-metadata.json | 2 +- .../action_no_object/ro-crate-metadata.json | 2 +- .../ro-crate-metadata.json | 2 +- .../ro-crate-metadata.json | 2 +- .../ro-crate-metadata.json | 2 +- .../ro-crate-metadata.json | 2 +- .../ro-crate-metadata.json | 2 +- .../process-run-crate/ro-crate-metadata.json | 29 +++- .../process-run-crate/test_prc_action.py | 29 ++++ tests/ro_crates.py | 8 ++ 16 files changed, 346 insertions(+), 19 deletions(-) create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_bad_environment/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_no_environment/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl index fc59f655..c8352cab 100644 --- a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl @@ -7,6 +7,7 @@ @prefix xsd: . @prefix rocrate: . @prefix bioschemas: . +@prefix wfrun: . ro:ProcRCActionOptional a sh:NodeShape ; sh:name "Process Run Crate Action MAY" ; @@ -37,6 +38,14 @@ ro:ProcRCActionOptional a sh:NodeShape ; sh:path schema:actionStatus ; sh:minCount 1 ; sh:message "The Action MAY have an actionStatus" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action environment" ; + sh:description "The Action MAY have an environment" ; + sh:path wfrun:environment ; + sh:minCount 1 ; + sh:message "The Action MAY have an environment" ; ] . @@ -53,7 +62,7 @@ ro:ProcRCActionErrorMay a sh:NodeShape ; { ?this a schema:ActivateAction } UNION { ?this a schema:UpdateAction } . ?this schema:actionStatus ?status . - FILTER(?status = schema:FailedActionStatus) . + FILTER(?status = "http://schema.org/FailedActionStatus") . } """ ] ; diff --git a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl index b43c6614..8de676e8 100644 --- a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl @@ -7,6 +7,7 @@ @prefix xsd: . @prefix rocrate: . @prefix bioschemas: . +@prefix wfrun: . ro:ProcRCActionRecommended a sh:NodeShape ; sh:name "Process Run Crate Action SHOULD" ; @@ -54,7 +55,6 @@ ro:ProcRCActionRecommended a sh:NodeShape ; sh:description "If present, the Action startTime SHOULD be in ISO 8601 format" ; sh:path schema:startTime ; sh:pattern "^(\\d{4}-\\d{2}-\\d{2})(T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?\\+\\d{2}:\\d{2})?$" ; - sh:minCount 0 ; sh:message "If present, the Action startTime SHOULD be in ISO 8601 format" ; ] ; sh:property [ @@ -74,14 +74,22 @@ ro:ProcRCActionRecommended a sh:NodeShape ; sh:name "Action actionStatus" ; sh:description "If the Action has an actionStatus, it should be http://schema.org/CompletedActionStatus or http://schema.org/FailedActionStatus" ; sh:path schema:actionStatus ; - sh:or ( - [ sh:hasValue schema:CompletedActionStatus ; ] - [ sh:hasValue schema:FailedActionStatus ; ] + sh:in ( + "http://schema.org/CompletedActionStatus" + "http://schema.org/FailedActionStatus" ) ; - sh:minCount 0 ; sh:message "If the Action has an actionStatus, it should be http://schema.org/CompletedActionStatus or http://schema.org/FailedActionStatus" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action environment" ; + sh:description "If the Action has an environment, it should point to entities of type PropertyValue" ; + sh:path wfrun:environment ; + sh:class schema:PropertyValue ; + sh:message "If the Action has an environment, it should point to entities of type PropertyValue" ; ] . + ro:ProcRCCreateUpdateActionRecommended a sh:NodeShape ; sh:name "Process Run Crate CreateAction UpdateAction SHOULD" ; sh:description "Recommended properties of the Process Run Crate CreateAction or UpdateAction" ; @@ -112,7 +120,7 @@ ro:ProcRCActionError a sh:NodeShape ; { ?this a schema:UpdateAction } . { FILTER NOT EXISTS { ?this schema:actionStatus ?status } } UNION { ?this schema:actionStatus ?status . - FILTER(?status != schema:FailedActionStatus) } + FILTER(?status != "http://schema.org/FailedActionStatus") } } """ ] ; diff --git a/tests/data/crates/invalid/3_process_run_crate/action_bad_actionstatus/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_bad_actionstatus/ro-crate-metadata.json index e0015f1b..9bf81ab1 100644 --- a/tests/data/crates/invalid/3_process_run_crate/action_bad_actionstatus/ro-crate-metadata.json +++ b/tests/data/crates/invalid/3_process_run_crate/action_bad_actionstatus/ro-crate-metadata.json @@ -68,7 +68,7 @@ "agent": { "@id": "https://orcid.org/0000-0001-9842-9718" }, - "actionStatus": { "@id": "http://schema.org/Integer" }, + "actionStatus": "http://schema.org/Integer", "error": "this is just to test the error property" }, { diff --git a/tests/data/crates/invalid/3_process_run_crate/action_bad_environment/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_bad_environment/ro-crate-metadata.json new file mode 100644 index 00000000..471f01fb --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_bad_environment/ro-crate-metadata.json @@ -0,0 +1,134 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@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" + }, + { + "@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" + } + ] + }, + { + "@id": "#width-limit-pv", + "@type": "CreativeWork" + }, + { + "@id": "#height-limit-pv", + "@type": "PropertyValue", + "name": "MAGICK_HEIGHT_LIMIT", + "value": "3072" + }, + { + "@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/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/invalid/3_process_run_crate/action_error_not_failed_status/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_error_not_failed_status/ro-crate-metadata.json index aadf8849..49d624bf 100644 --- a/tests/data/crates/invalid/3_process_run_crate/action_error_not_failed_status/ro-crate-metadata.json +++ b/tests/data/crates/invalid/3_process_run_crate/action_error_not_failed_status/ro-crate-metadata.json @@ -68,7 +68,7 @@ "agent": { "@id": "https://orcid.org/0000-0001-9842-9718" }, - "actionStatus": { "@id": "http://schema.org/CompletedActionStatus" }, + "actionStatus": "http://schema.org/CompletedActionStatus", "error": "this is just to test the error property" }, { diff --git a/tests/data/crates/invalid/3_process_run_crate/action_no_environment/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_no_environment/ro-crate-metadata.json new file mode 100644 index 00000000..7804b6c5 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_no_environment/ro-crate-metadata.json @@ -0,0 +1,116 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@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" + }, + { + "@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" + }, + { + "@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/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/invalid/3_process_run_crate/action_no_error/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_no_error/ro-crate-metadata.json index c57045e9..733b2535 100644 --- a/tests/data/crates/invalid/3_process_run_crate/action_no_error/ro-crate-metadata.json +++ b/tests/data/crates/invalid/3_process_run_crate/action_no_error/ro-crate-metadata.json @@ -68,7 +68,7 @@ "agent": { "@id": "https://orcid.org/0000-0001-9842-9718" }, - "actionStatus": { "@id": "http://schema.org/FailedActionStatus" } + "actionStatus": "http://schema.org/FailedActionStatus" }, { "@id": "pics/2017-06-11%2012.56.14.jpg", diff --git a/tests/data/crates/invalid/3_process_run_crate/action_no_object/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_no_object/ro-crate-metadata.json index a4df0b94..7a87f972 100644 --- a/tests/data/crates/invalid/3_process_run_crate/action_no_object/ro-crate-metadata.json +++ b/tests/data/crates/invalid/3_process_run_crate/action_no_object/ro-crate-metadata.json @@ -62,7 +62,7 @@ "agent": { "@id": "https://orcid.org/0000-0001-9842-9718" }, - "actionStatus": { "@id": "http://schema.org/FailedActionStatus" }, + "actionStatus": "http://schema.org/FailedActionStatus", "error": "this is just to test the error property" }, { diff --git a/tests/data/crates/invalid/3_process_run_crate/action_obj_res_bad_type/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_obj_res_bad_type/ro-crate-metadata.json index 0319c70f..87309910 100644 --- a/tests/data/crates/invalid/3_process_run_crate/action_obj_res_bad_type/ro-crate-metadata.json +++ b/tests/data/crates/invalid/3_process_run_crate/action_obj_res_bad_type/ro-crate-metadata.json @@ -79,7 +79,7 @@ "agent": { "@id": "https://orcid.org/0000-0001-9842-9718" }, - "actionStatus": { "@id": "http://schema.org/FailedActionStatus" }, + "actionStatus": "http://schema.org/FailedActionStatus", "error": "this is just to test the error property" }, { diff --git a/tests/data/crates/invalid/3_process_run_crate/collection_no_haspart/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/collection_no_haspart/ro-crate-metadata.json index 4305b905..899652c1 100644 --- a/tests/data/crates/invalid/3_process_run_crate/collection_no_haspart/ro-crate-metadata.json +++ b/tests/data/crates/invalid/3_process_run_crate/collection_no_haspart/ro-crate-metadata.json @@ -87,7 +87,7 @@ "agent": { "@id": "https://orcid.org/0000-0002-1825-0097" }, - "actionStatus": { "@id": "http://schema.org/FailedActionStatus" }, + "actionStatus": "http://schema.org/FailedActionStatus", "error": "this is just to test the error property" }, { diff --git a/tests/data/crates/invalid/3_process_run_crate/collection_no_mainentity/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/collection_no_mainentity/ro-crate-metadata.json index 3a2d525c..5a38e1b7 100644 --- a/tests/data/crates/invalid/3_process_run_crate/collection_no_mainentity/ro-crate-metadata.json +++ b/tests/data/crates/invalid/3_process_run_crate/collection_no_mainentity/ro-crate-metadata.json @@ -87,7 +87,7 @@ "agent": { "@id": "https://orcid.org/0000-0002-1825-0097" }, - "actionStatus": { "@id": "http://schema.org/FailedActionStatus" }, + "actionStatus": "http://schema.org/FailedActionStatus", "error": "this is just to test the error property" }, { diff --git a/tests/data/crates/invalid/3_process_run_crate/collection_not_mentioned/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/collection_not_mentioned/ro-crate-metadata.json index 2975fd11..3d668e58 100644 --- a/tests/data/crates/invalid/3_process_run_crate/collection_not_mentioned/ro-crate-metadata.json +++ b/tests/data/crates/invalid/3_process_run_crate/collection_not_mentioned/ro-crate-metadata.json @@ -81,7 +81,7 @@ "agent": { "@id": "https://orcid.org/0000-0002-1825-0097" }, - "actionStatus": { "@id": "http://schema.org/FailedActionStatus" }, + "actionStatus": "http://schema.org/FailedActionStatus", "error": "this is just to test the error property" }, { diff --git a/tests/data/crates/valid/process-run-crate-collections/ro-crate-metadata.json b/tests/data/crates/valid/process-run-crate-collections/ro-crate-metadata.json index 4d3b6c14..736785b3 100644 --- a/tests/data/crates/valid/process-run-crate-collections/ro-crate-metadata.json +++ b/tests/data/crates/valid/process-run-crate-collections/ro-crate-metadata.json @@ -87,7 +87,7 @@ "agent": { "@id": "https://orcid.org/0000-0002-1825-0097" }, - "actionStatus": { "@id": "http://schema.org/FailedActionStatus" }, + "actionStatus": "http://schema.org/FailedActionStatus", "error": "this is just to test the error property" }, { diff --git a/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json b/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json index a2a5cc68..d3e5bffa 100644 --- a/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json +++ b/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json @@ -1,5 +1,8 @@ { - "@context": "https://w3id.org/ro/crate/1.1/context", + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/workflow-run/context" + ], "@graph": [ { "@id": "ro-crate-metadata.json", @@ -79,8 +82,28 @@ "agent": { "@id": "https://orcid.org/0000-0001-9842-9718" }, - "actionStatus": { "@id": "http://schema.org/FailedActionStatus" }, - "error": "this is just to test the error property" + "actionStatus": "http://schema.org/FailedActionStatus", + "error": "this is just to test the error property", + "environment": [ + { + "@id": "#height-limit-pv" + }, + { + "@id": "#width-limit-pv" + } + ] + }, + { + "@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": "pics/2017-06-11%2012.56.14.jpg", diff --git a/tests/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index eb02ecdf..29c29eb5 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -280,3 +280,32 @@ def test_prc_action_obj_res_bad_type(): ["object and result SHOULD point to entities of type MediaObject, Dataset, Collection, CreativeWork or PropertyValue"], profile_identifier="process-run-crate" ) + + +def test_prc_action_no_environment(): + """\ + Test a Process Run Crate where the Action does not have an environment. + """ + do_entity_test( + InvalidProcRC().action_no_environment, + Severity.OPTIONAL, + False, + ["Process Run Crate Action MAY"], + ["The Action MAY have an environment"], + profile_identifier="process-run-crate" + ) + + +def test_prc_action_bad_environment(): + """\ + Test a Process Run Crate where the Action has an environment that does not + point to PropertyValues. + """ + do_entity_test( + InvalidProcRC().action_bad_environment, + Severity.RECOMMENDED, + False, + ["Process Run Crate Action SHOULD"], + ["If the Action has an environment, it should point to entities of type PropertyValue"], + profile_identifier="process-run-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 54e8530e..8b1e4563 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -385,3 +385,11 @@ def collection_no_haspart(self) -> Path: @property def collection_no_mainentity(self) -> Path: return self.base_path / "collection_no_mainentity" + + @property + def action_no_environment(self) -> Path: + return self.base_path / "action_no_environment" + + @property + def action_bad_environment(self) -> Path: + return self.base_path / "action_bad_environment" From fd25fcb9facae91d189b2a0a7edc8186a52159c2 Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 23 Jul 2024 14:42:25 +0200 Subject: [PATCH 674/902] procrc: action: add checks for containerImage property --- .../process-run-crate/may/1_create_action.ttl | 8 + .../should/1_create_action.ttl | 11 ++ .../ro-crate-metadata.json | 139 ++++++++++++++++ .../ro-crate-metadata.json | 137 ++++++++++++++++ .../ro-crate-metadata.json | 136 ++++++++++++++++ .../ro-crate-metadata.json | 150 ++++++++++++++++++ .../process-run-crate/ro-crate-metadata.json | 3 +- .../process-run-crate/test_prc_action.py | 31 ++++ .../process-run-crate/test_valid_prc.py | 6 + tests/ro_crates.py | 16 ++ 10 files changed, 636 insertions(+), 1 deletion(-) create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_bad_containerimage_type/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_bad_containerimage_url/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/action_no_containerimage/ro-crate-metadata.json create mode 100644 tests/data/crates/valid/process-run-crate-containerimage/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl index c8352cab..2f7a124f 100644 --- a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl @@ -46,6 +46,14 @@ ro:ProcRCActionOptional a sh:NodeShape ; sh:path wfrun:environment ; sh:minCount 1 ; sh:message "The Action MAY have an environment" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action containerImage" ; + sh:description "The Action MAY have a containerImage" ; + sh:path wfrun:containerImage ; + sh:minCount 1 ; + sh:message "The Action MAY have a containerImage" ; ] . diff --git a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl index 8de676e8..c440077a 100644 --- a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl @@ -87,6 +87,17 @@ ro:ProcRCActionRecommended a sh:NodeShape ; sh:path wfrun:environment ; sh:class schema:PropertyValue ; sh:message "If the Action has an environment, it should point to entities of type PropertyValue" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Action containerImage" ; + sh:description "If the Action has a containerImage, it should point to a ContainerImage or a URL" ; + sh:path wfrun:containerImage ; + sh:or ( + [ sh:class wfrun:ContainerImage ; ] + [ sh:pattern "^http.*" ; ] + ) ; + sh:message "If the Action has a containerImage, it should point to a ContainerImage or a URL" ; ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/action_bad_containerimage_type/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_bad_containerimage_type/ro-crate-metadata.json new file mode 100644 index 00000000..b448c7c8 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_bad_containerimage_type/ro-crate-metadata.json @@ -0,0 +1,139 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@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" + }, + { + "@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": { + "@id": "pics/sepia_fence.jpg" + } + }, + { + "@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": "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/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/invalid/3_process_run_crate/action_bad_containerimage_url/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_bad_containerimage_url/ro-crate-metadata.json new file mode 100644 index 00000000..4b258f0d --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_bad_containerimage_url/ro-crate-metadata.json @@ -0,0 +1,137 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@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" + }, + { + "@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": "imagemagick" + }, + { + "@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": "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/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/invalid/3_process_run_crate/action_no_containerimage/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/action_no_containerimage/ro-crate-metadata.json new file mode 100644 index 00000000..d3e5bffa --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/action_no_containerimage/ro-crate-metadata.json @@ -0,0 +1,136 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@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" + }, + { + "@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" + } + ] + }, + { + "@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": "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/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/valid/process-run-crate-containerimage/ro-crate-metadata.json b/tests/data/crates/valid/process-run-crate-containerimage/ro-crate-metadata.json new file mode 100644 index 00000000..941b133f --- /dev/null +++ b/tests/data/crates/valid/process-run-crate-containerimage/ro-crate-metadata.json @@ -0,0 +1,150 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@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" + }, + { + "@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": { + "@id": "#imagemagick-image" + } + }, + { + "@id": "#imagemagick-image", + "@type": "ContainerImage", + "additionalType": { + "@id": "https://w3id.org/ro/terms/workflow-run#DockerImage" + }, + "registry": "docker.io", + "name": "simleo/imagemagick", + "tag": "6.9.12-98", + "sha256": "dc6161af5a230ec48b7c9469c81c32b2e7cc2ae510b32dacc43bfc658a2bca1a" + }, + { + "@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": "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/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/valid/process-run-crate/ro-crate-metadata.json b/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json index d3e5bffa..7d636aa5 100644 --- a/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json +++ b/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json @@ -91,7 +91,8 @@ { "@id": "#width-limit-pv" } - ] + ], + "containerImage": "https://example.com/imagemagick.sif" }, { "@id": "#width-limit-pv", diff --git a/tests/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_prc_action.py index 29c29eb5..d06b0667 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_prc_action.py @@ -309,3 +309,34 @@ def test_prc_action_bad_environment(): ["If the Action has an environment, it should point to entities of type PropertyValue"], profile_identifier="process-run-crate" ) + + +def test_prc_action_no_containerimage(): + """\ + Test a Process Run Crate where the Action does not have a containerimage. + """ + do_entity_test( + InvalidProcRC().action_no_containerimage, + Severity.OPTIONAL, + False, + ["Process Run Crate Action MAY"], + ["The Action MAY have a containerImage"], + profile_identifier="process-run-crate" + ) + + +def test_prc_action_bad_containerimage(): + """\ + Test a Process Run Crate where the Action has a containerImage that does + not point to a URL or to a ContainerImage object. + """ + for crate in (InvalidProcRC().action_bad_containerimage_url, + InvalidProcRC().action_bad_containerimage_type): + do_entity_test( + InvalidProcRC().action_bad_containerimage_url, + Severity.RECOMMENDED, + False, + ["Process Run Crate Action SHOULD"], + ["If the Action has a containerImage, it should point to a ContainerImage or a URL"], + profile_identifier="process-run-crate" + ) diff --git a/tests/integration/profiles/process-run-crate/test_valid_prc.py b/tests/integration/profiles/process-run-crate/test_valid_prc.py index e281402f..fe467fa0 100644 --- a/tests/integration/profiles/process-run-crate/test_valid_prc.py +++ b/tests/integration/profiles/process-run-crate/test_valid_prc.py @@ -21,3 +21,9 @@ def test_valid_process_run_crate_required(): True, profile_identifier="process-run-crate" ) + do_entity_test( + ValidROC().process_run_crate_containerimage, + Severity.REQUIRED, + True, + profile_identifier="process-run-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 8b1e4563..d9c23821 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -49,6 +49,10 @@ def process_run_crate(self) -> Path: def process_run_crate_collections(self) -> Path: return VALID_CRATES_DATA_PATH / "process-run-crate-collections" + @property + def process_run_crate_containerimage(self) -> Path: + return VALID_CRATES_DATA_PATH / "process-run-crate-containerimage" + class InvalidFileDescriptor: @@ -393,3 +397,15 @@ def action_no_environment(self) -> Path: @property def action_bad_environment(self) -> Path: return self.base_path / "action_bad_environment" + + @property + def action_no_containerimage(self) -> Path: + return self.base_path / "action_no_containerimage" + + @property + def action_bad_containerimage_url(self) -> Path: + return self.base_path / "action_bad_containerimage_url" + + @property + def action_bad_containerimage_type(self) -> Path: + return self.base_path / "action_bad_containerimage_type" From 2b7e9c39b09e7de76c17e75e8574b9e37ae8c5be Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Jul 2024 13:49:51 +0200 Subject: [PATCH 675/902] feat(core): :sparkles: keep track of performed and skipped checks --- rocrate_validator/models.py | 45 +++++++++++++++++++ .../requirements/shacl/checks.py | 13 +++--- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index d678b4c5..456e58c8 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -455,6 +455,15 @@ def all(cls) -> list[Profile]: return cls.__profiles_map.values() +class SkipRequirementCheck(Exception): + def __init__(self, check: RequirementCheck, message: str = ""): + self.check = check + self.message = message + + def __str__(self): + return f"SkipRequirementCheck(check={self.check})" + + @total_ordering class Requirement(ABC): @@ -569,6 +578,7 @@ def __do_validate__(self, context: ValidationContext) -> bool: context.validator.notify(RequirementCheckValidationEvent( EventType.REQUIREMENT_CHECK_VALIDATION_START, check)) check_result = check.execute_check(context) + context.result.add_executed_check(check, check_result) context.validator.notify(RequirementCheckValidationEvent( EventType.REQUIREMENT_CHECK_VALIDATION_END, check, validation_result=check_result)) logger.debug("Ran check '%s'. Got result %s", check.name, check_result) @@ -576,6 +586,10 @@ def __do_validate__(self, context: ValidationContext) -> bool: logger.warning("Ignoring the check %s as it returned the value %r instead of a boolean", check.name) raise RuntimeError(f"Ignoring invalid result from check {check.name}") all_passed = all_passed and check_result + except SkipRequirementCheck as e: + logger.debug("Skipping check '%s' because: %s", check.name, e) + context.result.add_skipped_check(check) + continue except Exception as e: # Ignore the fact that the check failed as far as the validation result is concerned. logger.warning("Unexpected error during check %s. Exception: %s", check, e) @@ -933,6 +947,11 @@ def __init__(self, context: ValidationContext): self._validation_settings: dict[str, BaseTypes] = context.settings # keep track of the issues found during the validation self._issues: list[CheckIssue] = [] + # keep track of the checks that have been executed + self._executed_checks: set[RequirementCheck] = set() + self._executed_checks_results: dict[str, bool] = {} + # keep track of the checks that have been skipped + self._skipped_checks: set[RequirementCheck] = set() @property def context(self) -> ValidationContext: @@ -946,6 +965,32 @@ def rocrate_path(self): def validation_settings(self): return self._validation_settings + # --- Checks --- + @property + def executed_checks(self) -> set[RequirementCheck]: + return self._executed_checks + + def add_executed_check(self, check: RequirementCheck, result: bool): + 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) + + def get_executed_check_result(self, check: RequirementCheck) -> Optional[bool]: + return self._executed_checks_results.get(check.identifier) + + @property + def skipped_checks(self) -> set[RequirementCheck]: + return self._skipped_checks + + def add_skipped_check(self, check: RequirementCheck): + self._skipped_checks.add(check) + + def remove_skipped_check(self, check: RequirementCheck): + self._skipped_checks.remove(check) + # --- Issues --- @property def issues(self) -> list[CheckIssue]: diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 360bd51d..8ba35b51 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -5,7 +5,8 @@ import rocrate_validator.log as logging from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.models import (Requirement, RequirementCheck, - ValidationContext) + RequirementCheckValidationEvent, + SkipRequirementCheck, ValidationContext) from rocrate_validator.requirements.shacl.models import Shape from rocrate_validator.requirements.shacl.utils import make_uris_relative @@ -54,10 +55,12 @@ def execute_check(self, context: ValidationContext): logger.debug("SHACL Validation of profile %s already processed", self.requirement.profile) return e.result except SHACLValidationSkip as e: - logger.debug("SHACL Validation of profile %s skipped", self.requirement.profile) - # the validation is postponed to the subsequent profiles - # ย so we return True to continue the validation - return True + logger.debug("SHACL Validation of profile %s requirement %s skipped", self.requirement.profile, self) + # The validation is postponed to the more specific profiles + # so the check is not considered as failed. + # We assume that the main algorithm catches the issue + # and the check is marked as skipped withing the context.result + raise SkipRequirementCheck(self, str(e)) except ROCrateMetadataNotFoundError as e: logger.debug("Unable to perform metadata validation due to missing metadata file: %s", e) return False From fa6ddafb77bc35682b8b837d3e1f801c3fc3ed29 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Jul 2024 13:55:29 +0200 Subject: [PATCH 676/902] feat(shacl): :sparkles: improve handling of SHACL check results --- .../requirements/shacl/checks.py | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 8ba35b51..6358d7fc 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -45,15 +45,20 @@ def shape(self) -> Shape: return self._shape def execute_check(self, context: ValidationContext): + logger.debug("Starting check %s", self) try: - result = None + logger.debug("SHACL Validation of profile %s requirement %s started", self.requirement.profile, self) with SHACLValidationContextManager(self, context) as ctx: + # The check is executed only if the profile is the most specific one + logger.debug("SHACL Validation of profile %s requirement %s started", self.requirement.profile, self) result = self.__do_execute_check__(ctx) - ctx.current_validation_result = result - return result + ctx.current_validation_result = not self in result + return ctx.current_validation_result except SHACLValidationAlreadyProcessed as e: logger.debug("SHACL Validation of profile %s already processed", self.requirement.profile) - return e.result + # 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 not self in [i.check for i in context.result.get_issues()] except SHACLValidationSkip as e: logger.debug("SHACL Validation of profile %s requirement %s skipped", self.requirement.profile, self) # The validation is postponed to the more specific profiles @@ -113,33 +118,41 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): # store the validation result in the context start_time = timer() result = shacl_result.conforms - # if the validation failed, add the issues to the context + # if the validation fails, process the failed checks + failed_requirements_checks = [] if not shacl_result.conforms: - logger.debug("Validation failed") - logger.debug("Parsing Validation result: %s", result) + logger.debug("Parsing Validation with result: %s", result) + # process the failed checks to extract the requirement checks involved for violation in shacl_result.violations: shape = shapes_registry.get_shape(Shape.compute_key(shapes_graph, violation.sourceShape)) assert shape is not None, "Unable to map the violation to a shape" requirementCheck = SHACLCheck.get_instance(shape) assert requirementCheck is not None, "The requirement check cannot be None" - # add only the issues for the current profile when the `target_profile_only` mode is disabled - # (issues related to other profiles will be added by the corresponding profile validation) - if requirementCheck.requirement.profile == shacl_context.current_validation_profile or \ - shacl_context.settings.get("target_only_validation", False): - c = shacl_context.result.add_check_issue(message=violation.get_result_message(shacl_context.rocrate_path), - check=requirementCheck, - severity=violation.get_result_severity(), - resultPath=violation.resultPath.toPython() if violation.resultPath else None, - focusNode=make_uris_relative( - violation.focusNode.toPython(), shacl_context.publicID), - value=violation.value) + failed_requirements_checks.append(requirementCheck) + # sort the failed checks by identifier and severity + # to ensure a consistent order of the issues + # and to make the fail fast mode deterministic + for requirementCheck in sorted(failed_requirements_checks, key=lambda x: (x.identifier, x.severity)): + # add only the issues for the current profile when the `target_profile_only` mode is disabled + # (issues related to other profiles will be added by the corresponding profile validation) + # failed_check_ids.append(requirementCheck.identifier) + if requirementCheck.requirement.profile == shacl_context.current_validation_profile or \ + shacl_context.settings.get("target_only_validation", False): + # failed_check_ids.append(requirementCheck.identifier) + c = shacl_context.result.add_check_issue(message=violation.get_result_message(shacl_context.rocrate_path), + check=requirementCheck, + severity=violation.get_result_severity(), + resultPath=violation.resultPath.toPython() if violation.resultPath else None, + focusNode=make_uris_relative( + violation.focusNode.toPython(), shacl_context.publicID), + value=violation.value) logger.debug("Added validation issue to the context: %s", c) if shacl_context.base_context.fail_fast: break end_time = timer() logger.debug(f"Execution time for parsing the validation result: {end_time - start_time} seconds") - return result + return failed_requirements_checks def __str__(self) -> str: return super().__str__() + (f" - {self._shape}" if self._shape else "") From 51fd3485641f703b493c2316480f654cc2e0ba3a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Jul 2024 13:57:33 +0200 Subject: [PATCH 677/902] perf(core): :zap: improve fail fast mode --- rocrate_validator/models.py | 12 +++++++++--- rocrate_validator/requirements/shacl/checks.py | 4 ++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 456e58c8..2f4351bd 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -586,6 +586,8 @@ def __do_validate__(self, context: ValidationContext) -> bool: logger.warning("Ignoring the check %s as it returned the value %r instead of a boolean", check.name) raise RuntimeError(f"Ignoring invalid result from check {check.name}") all_passed = all_passed and check_result + if not all_passed and context.fail_fast: + break except SkipRequirementCheck as e: logger.debug("Skipping check '%s' because: %s", check.name, e) context.result.add_skipped_check(check) @@ -1267,6 +1269,7 @@ def __do_validate__(self, logger.debug("Validating profile %s with %s requirements", profile.identifier, len(requirements)) logger.debug("For profile %s, validating these %s requirements: %s", profile.identifier, len(requirements), requirements) + terminate = False for requirement in requirements: logger.debug("Validating Requirement %s", requirement) self.notify(RequirementValidationEvent(EventType.REQUIREMENT_VALIDATION_START, requirement=requirement)) @@ -1277,11 +1280,14 @@ def __do_validate__(self, if passed: logger.debug("Validation Requirement passed") else: - logger.debug(f"Validation Requirement {requirement} failed ") - if context.settings.get("abort_on_first") is True and context.profile_identifier == profile.name: + logger.debug(f"Validation Requirement {requirement} failed (profile: {profile.identifier})") + if context.fail_fast: logger.debug("Aborting on first requirement failure") - return context.result + terminate = True + break self.notify(ProfileValidationEvent(EventType.PROFILE_VALIDATION_END, profile=profile)) + if terminate: + break self.notify(ValidationEvent(EventType.VALIDATION_END, validation_result=context.result)) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 6358d7fc..ce03b309 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -146,6 +146,10 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): focusNode=make_uris_relative( violation.focusNode.toPython(), shacl_context.publicID), value=violation.value) + # if the fail fast mode is enabled, stop the validation after the first issue + if shacl_context.fail_fast: + break + logger.debug("Added validation issue to the context: %s", c) if shacl_context.base_context.fail_fast: break From 667dba8c9686b9bae5da89ffa0b12c1a4a6c1920 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Jul 2024 14:01:07 +0200 Subject: [PATCH 678/902] fix(core): :ambulance: fix missing checks notifications --- rocrate_validator/models.py | 7 ++--- .../requirements/shacl/checks.py | 31 +++++++++++++++++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 2f4351bd..c358122d 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1259,7 +1259,7 @@ def __do_validate__(self, # set the profiles to validate against profiles = context.profiles assert len(profiles) > 0, "No profiles to validate" - self.notify(EventType.PROFILE_VALIDATION_START) + self.notify(EventType.VALIDATION_START) for profile in profiles: logger.debug("Validating profile %s (id: %s)", profile.name, profile.identifier) self.notify(ProfileValidationEvent(EventType.PROFILE_VALIDATION_START, profile=profile)) @@ -1271,10 +1271,9 @@ def __do_validate__(self, profile.identifier, len(requirements), requirements) terminate = False for requirement in requirements: - logger.debug("Validating Requirement %s", requirement) - self.notify(RequirementValidationEvent(EventType.REQUIREMENT_VALIDATION_START, requirement=requirement)) + self.notify(RequirementValidationEvent( + EventType.REQUIREMENT_VALIDATION_START, requirement=requirement)) passed = requirement.__do_validate__(context) - logger.debug("Number of issues: %s", len(context.result.issues)) self.notify(RequirementValidationEvent( EventType.REQUIREMENT_VALIDATION_END, requirement=requirement, validation_result=passed)) if passed: diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index ce03b309..93c8e340 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -4,6 +4,7 @@ import rocrate_validator.log as logging from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.events import EventType from rocrate_validator.models import (Requirement, RequirementCheck, RequirementCheckValidationEvent, SkipRequirementCheck, ValidationContext) @@ -120,6 +121,7 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): result = shacl_result.conforms # if the validation fails, process the failed checks failed_requirements_checks = [] + failed_requirement_checks_notified = [] if not shacl_result.conforms: logger.debug("Parsing Validation with result: %s", result) # process the failed checks to extract the requirement checks involved @@ -150,9 +152,34 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): if shacl_context.fail_fast: break + # 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 + if not shacl_context.fail_fast: + if requirementCheck.requirement.profile != shacl_context.current_validation_profile and \ + not requirementCheck.identifier in failed_requirement_checks_notified: + # + failed_requirement_checks_notified.append(requirementCheck.identifier) + + shacl_context.validator.notify(RequirementCheckValidationEvent( + EventType.REQUIREMENT_CHECK_VALIDATION_END, requirementCheck, validation_result=False)) logger.debug("Added validation issue to the context: %s", c) - if shacl_context.base_context.fail_fast: - break + + # As above, but for skipped checks which are not failed + if not shacl_context.fail_fast: + for requirementCheck in list(shacl_context.result.skipped_checks): + if not isinstance(requirementCheck, SHACLCheck): + continue + if requirementCheck.requirement.profile != shacl_context.current_validation_profile and \ + not requirementCheck in failed_requirements_checks and \ + not requirementCheck.identifier in failed_requirement_checks_notified: + failed_requirement_checks_notified.append(requirementCheck.identifier) + shacl_context.result.add_executed_check(requirementCheck, True) + shacl_context.validator.notify(RequirementCheckValidationEvent( + EventType.REQUIREMENT_CHECK_VALIDATION_END, requirementCheck, validation_result=True)) + end_time = timer() logger.debug(f"Execution time for parsing the validation result: {end_time - start_time} seconds") From e04363a195faf0e7454de258e36726ee9fe5b128 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Jul 2024 14:02:45 +0200 Subject: [PATCH 679/902] feat(core): :sparkles: use context severity by default --- rocrate_validator/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index c358122d..05121ea3 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -998,12 +998,13 @@ def remove_skipped_check(self, check: RequirementCheck): def issues(self) -> list[CheckIssue]: return self._issues - def get_issues(self, min_severity: Severity) -> list[CheckIssue]: + def get_issues(self, min_severity: Optional[Severity] = None) -> list[CheckIssue]: + min_severity = min_severity or self.context.requirement_severity return [issue for issue in self._issues if issue.severity >= min_severity] def get_issues_by_check(self, check: RequirementCheck, - min_severity: Optional[Severity] = None) -> list[CheckIssue]: + min_severity: Severity = None) -> list[CheckIssue]: min_severity = min_severity or self.context.requirement_severity return [issue for issue in self._issues if issue.check == check and issue.severity >= min_severity] From e660f50931015b47cf488fde9dd5ea1507b6f0fd Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Jul 2024 14:03:52 +0200 Subject: [PATCH 680/902] fix(profiles/ro-crate): :bug: fix inverted returned values --- .../ro-crate/should/2_root_data_entity_relative_uri.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py b/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py index bfc4a80d..26421f1e 100644 --- a/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py +++ b/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py @@ -20,8 +20,8 @@ def check_relative_uris(self, context: ValidationContext) -> bool: if not context.ro_crate.metadata.get_root_data_entity().id == './': context.result.add_error( 'Root Data Entity URI is not denoted by the string `./`', self) - return True - return False + return False + return True except Exception as e: context.result.add_error( f'Error checking Root Data Entity URI: {str(e)}', self) From d89b52376632d855c9152a575b0e1dcb169000f0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Jul 2024 14:07:12 +0200 Subject: [PATCH 681/902] refactor(cli): :wastebasket: clean up --- rocrate_validator/cli/commands/validate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 031e99d0..a3a8b39c 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -1,6 +1,5 @@ from __future__ import annotations -import json import os import sys import termios @@ -9,7 +8,7 @@ from typing import Optional from rich.align import Align -from rich.console import Console, Group +from rich.console import Console from rich.layout import Layout from rich.live import Live from rich.markdown import Markdown From 8ae51312cef6535a0fa441fa8603368af869661a Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 24 Jul 2024 10:58:47 +0200 Subject: [PATCH 682/902] procrc: ContainerImage SHOULD checks --- .../should/3_container_image.ttl | 43 +++++ .../ro-crate-metadata.json | 150 ++++++++++++++++++ .../ro-crate-metadata.json | 147 +++++++++++++++++ .../ro-crate-metadata.json | 149 +++++++++++++++++ .../ro-crate-metadata.json | 149 +++++++++++++++++ .../test_prc_containerimage.py | 65 ++++++++ tests/ro_crates.py | 16 ++ 7 files changed, 719 insertions(+) create mode 100644 rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl create mode 100644 tests/data/crates/invalid/3_process_run_crate/containerimage_bad_additionaltype/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/containerimage_no_additionaltype/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/containerimage_no_name/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/containerimage_no_registry/ro-crate-metadata.json create mode 100644 tests/integration/profiles/process-run-crate/test_prc_containerimage.py diff --git a/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl b/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl new file mode 100644 index 00000000..d0813c8e --- /dev/null +++ b/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl @@ -0,0 +1,43 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix rocrate: . +@prefix bioschemas: . +@prefix wfrun: . + +ro:ProcRCContainerImageRecommended a sh:NodeShape ; + sh:name "Process Run Crate ContainerImage SHOULD" ; + sh:description "Recommended properties of the Process Run Crate ContainerImage" ; + sh:targetClass wfrun:ContainerImage ; + sh:property [ + a sh:PropertyShape ; + sh:name "ContainerImage additionalType" ; + sh:description "The ContainerImage SHOULD have an additionalType pointing to or " ; + sh:path schema:additionalType ; + sh:or ( + [ sh:hasValue wfrun:DockerImage ; ] + [ sh:hasValue wfrun:SIFImage ; ] + ) ; + sh:minCount 1 ; + sh:message "The ContainerImage SHOULD have an additionalType pointing to or " ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "ContainerImage registry" ; + sh:description "The ContainerImage SHOULD have a registry" ; + sh:path wfrun:registry ; + sh:minCount 1 ; + sh:message "The ContainerImage SHOULD have a registry" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "ContainerImage name" ; + sh:description "The ContainerImage SHOULD have a name" ; + sh:path schema:name ; + sh:minCount 1 ; + sh:message "The ContainerImage SHOULD have a name" ; + ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/containerimage_bad_additionaltype/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/containerimage_bad_additionaltype/ro-crate-metadata.json new file mode 100644 index 00000000..8ba88a1f --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/containerimage_bad_additionaltype/ro-crate-metadata.json @@ -0,0 +1,150 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@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" + }, + { + "@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": { + "@id": "#imagemagick-image" + } + }, + { + "@id": "#imagemagick-image", + "@type": "ContainerImage", + "additionalType": { + "@id": "http://schema.org/Integer" + }, + "registry": "docker.io", + "name": "simleo/imagemagick", + "tag": "6.9.12-98", + "sha256": "dc6161af5a230ec48b7c9469c81c32b2e7cc2ae510b32dacc43bfc658a2bca1a" + }, + { + "@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": "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/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/invalid/3_process_run_crate/containerimage_no_additionaltype/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/containerimage_no_additionaltype/ro-crate-metadata.json new file mode 100644 index 00000000..34a44682 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/containerimage_no_additionaltype/ro-crate-metadata.json @@ -0,0 +1,147 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@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" + }, + { + "@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": { + "@id": "#imagemagick-image" + } + }, + { + "@id": "#imagemagick-image", + "@type": "ContainerImage", + "registry": "docker.io", + "name": "simleo/imagemagick", + "tag": "6.9.12-98", + "sha256": "dc6161af5a230ec48b7c9469c81c32b2e7cc2ae510b32dacc43bfc658a2bca1a" + }, + { + "@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": "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/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/invalid/3_process_run_crate/containerimage_no_name/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/containerimage_no_name/ro-crate-metadata.json new file mode 100644 index 00000000..d91b9340 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/containerimage_no_name/ro-crate-metadata.json @@ -0,0 +1,149 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@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" + }, + { + "@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": { + "@id": "#imagemagick-image" + } + }, + { + "@id": "#imagemagick-image", + "@type": "ContainerImage", + "additionalType": { + "@id": "https://w3id.org/ro/terms/workflow-run#DockerImage" + }, + "registry": "docker.io", + "tag": "6.9.12-98", + "sha256": "dc6161af5a230ec48b7c9469c81c32b2e7cc2ae510b32dacc43bfc658a2bca1a" + }, + { + "@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": "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/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/invalid/3_process_run_crate/containerimage_no_registry/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/containerimage_no_registry/ro-crate-metadata.json new file mode 100644 index 00000000..5e12cc7c --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/containerimage_no_registry/ro-crate-metadata.json @@ -0,0 +1,149 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@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" + }, + { + "@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": { + "@id": "#imagemagick-image" + } + }, + { + "@id": "#imagemagick-image", + "@type": "ContainerImage", + "additionalType": { + "@id": "https://w3id.org/ro/terms/workflow-run#DockerImage" + }, + "name": "simleo/imagemagick", + "tag": "6.9.12-98", + "sha256": "dc6161af5a230ec48b7c9469c81c32b2e7cc2ae510b32dacc43bfc658a2bca1a" + }, + { + "@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": "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/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/integration/profiles/process-run-crate/test_prc_containerimage.py b/tests/integration/profiles/process-run-crate/test_prc_containerimage.py new file mode 100644 index 00000000..756a6045 --- /dev/null +++ b/tests/integration/profiles/process-run-crate/test_prc_containerimage.py @@ -0,0 +1,65 @@ +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import InvalidProcRC +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_prc_containerimage_no_additionaltype(): + """\ + Test a Process Run Crate where the ContainerImage has no additionalType. + """ + do_entity_test( + InvalidProcRC().containerimage_no_additionaltype, + Severity.RECOMMENDED, + False, + ["Process Run Crate ContainerImage SHOULD"], + ["The ContainerImage SHOULD have an additionalType pointing to or "], + profile_identifier="process-run-crate" + ) + + +def test_prc_containerimage_bad_additionaltype(): + """\ + Test a Process Run Crate where the ContainerImage additionalType does not + point to one of the allowed values. + """ + do_entity_test( + InvalidProcRC().containerimage_bad_additionaltype, + Severity.RECOMMENDED, + False, + ["Process Run Crate ContainerImage SHOULD"], + ["The ContainerImage SHOULD have an additionalType pointing to or "], + profile_identifier="process-run-crate" + ) + + +def test_prc_containerimage_no_registry(): + """\ + Test a Process Run Crate where the ContainerImage has no registry. + """ + do_entity_test( + InvalidProcRC().containerimage_no_registry, + Severity.RECOMMENDED, + False, + ["Process Run Crate ContainerImage SHOULD"], + ["The ContainerImage SHOULD have a registry"], + profile_identifier="process-run-crate" + ) + + +def test_prc_containerimage_no_name(): + """\ + Test a Process Run Crate where the ContainerImage has no name. + """ + do_entity_test( + InvalidProcRC().containerimage_no_name, + Severity.RECOMMENDED, + False, + ["Process Run Crate ContainerImage SHOULD"], + ["The ContainerImage SHOULD have a name"], + profile_identifier="process-run-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index d9c23821..9331db8b 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -409,3 +409,19 @@ def action_bad_containerimage_url(self) -> Path: @property def action_bad_containerimage_type(self) -> Path: return self.base_path / "action_bad_containerimage_type" + + @property + def containerimage_no_additionaltype(self) -> Path: + return self.base_path / "containerimage_no_additionaltype" + + @property + def containerimage_bad_additionaltype(self) -> Path: + return self.base_path / "containerimage_bad_additionaltype" + + @property + def containerimage_no_registry(self) -> Path: + return self.base_path / "containerimage_no_registry" + + @property + def containerimage_no_name(self) -> Path: + return self.base_path / "containerimage_no_name" From 74cb84cb6fa65f0e73652a0603c40de3bce68cb3 Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 24 Jul 2024 14:54:23 +0200 Subject: [PATCH 683/902] procrc: ContainerImage MAY checks --- .../may/3_container_image.ttl | 31 ++++ .../ro-crate-metadata.json | 149 ++++++++++++++++++ .../ro-crate-metadata.json | 149 ++++++++++++++++++ .../test_prc_containerimage.py | 28 ++++ tests/ro_crates.py | 8 + 5 files changed, 365 insertions(+) create mode 100644 rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl create mode 100644 tests/data/crates/invalid/3_process_run_crate/containerimage_no_sha256/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/containerimage_no_tag/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl b/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl new file mode 100644 index 00000000..c43df33f --- /dev/null +++ b/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl @@ -0,0 +1,31 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix rocrate: . +@prefix bioschemas: . +@prefix wfrun: . + +ro:ProcRCContainerImageOptional a sh:NodeShape ; + sh:name "Process Run Crate ContainerImage MAY" ; + sh:description "Optional properties of the Process Run Crate ContainerImage" ; + sh:targetClass wfrun:ContainerImage ; + sh:property [ + a sh:PropertyShape ; + sh:name "ContainerImage tag" ; + sh:description "The ContainerImage MAY have a tag" ; + sh:path wfrun:tag ; + sh:minCount 1 ; + sh:message "The ContainerImage MAY have a tag" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "ContainerImage sha256" ; + sh:description "The ContainerImage MAY have a sha256" ; + sh:path wfrun:sha256 ; + sh:minCount 1 ; + sh:message "The ContainerImage MAY have a sha256" ; + ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/containerimage_no_sha256/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/containerimage_no_sha256/ro-crate-metadata.json new file mode 100644 index 00000000..0b54c68b --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/containerimage_no_sha256/ro-crate-metadata.json @@ -0,0 +1,149 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@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" + }, + { + "@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": { + "@id": "#imagemagick-image" + } + }, + { + "@id": "#imagemagick-image", + "@type": "ContainerImage", + "additionalType": { + "@id": "https://w3id.org/ro/terms/workflow-run#DockerImage" + }, + "registry": "docker.io", + "name": "simleo/imagemagick", + "tag": "6.9.12-98" + }, + { + "@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": "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/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/invalid/3_process_run_crate/containerimage_no_tag/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/containerimage_no_tag/ro-crate-metadata.json new file mode 100644 index 00000000..fce440f0 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/containerimage_no_tag/ro-crate-metadata.json @@ -0,0 +1,149 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@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" + }, + { + "@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": { + "@id": "#imagemagick-image" + } + }, + { + "@id": "#imagemagick-image", + "@type": "ContainerImage", + "additionalType": { + "@id": "https://w3id.org/ro/terms/workflow-run#DockerImage" + }, + "registry": "docker.io", + "name": "simleo/imagemagick", + "sha256": "dc6161af5a230ec48b7c9469c81c32b2e7cc2ae510b32dacc43bfc658a2bca1a" + }, + { + "@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": "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/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/integration/profiles/process-run-crate/test_prc_containerimage.py b/tests/integration/profiles/process-run-crate/test_prc_containerimage.py index 756a6045..7397abdd 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_containerimage.py +++ b/tests/integration/profiles/process-run-crate/test_prc_containerimage.py @@ -63,3 +63,31 @@ def test_prc_containerimage_no_name(): ["The ContainerImage SHOULD have a name"], profile_identifier="process-run-crate" ) + + +def test_prc_containerimage_no_tag(): + """\ + Test a Process Run Crate where the ContainerImage has no tag. + """ + do_entity_test( + InvalidProcRC().containerimage_no_tag, + Severity.OPTIONAL, + False, + ["Process Run Crate ContainerImage MAY"], + ["The ContainerImage MAY have a tag"], + profile_identifier="process-run-crate" + ) + + +def test_prc_containerimage_no_sha256(): + """\ + Test a Process Run Crate where the ContainerImage has no sha256. + """ + do_entity_test( + InvalidProcRC().containerimage_no_sha256, + Severity.OPTIONAL, + False, + ["Process Run Crate ContainerImage MAY"], + ["The ContainerImage MAY have a sha256"], + profile_identifier="process-run-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 9331db8b..86f545c6 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -425,3 +425,11 @@ def containerimage_no_registry(self) -> Path: @property def containerimage_no_name(self) -> Path: return self.base_path / "containerimage_no_name" + + @property + def containerimage_no_tag(self) -> Path: + return self.base_path / "containerimage_no_tag" + + @property + def containerimage_no_sha256(self) -> Path: + return self.base_path / "containerimage_no_sha256" From a03de9bb4982f5229d8ed2499e8243fb726a44b1 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Jul 2024 15:46:41 +0200 Subject: [PATCH 684/902] refactor(shacl): :wastebasket: clean up --- rocrate_validator/requirements/shacl/checks.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 93c8e340..65247a2e 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -137,10 +137,8 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): for requirementCheck in sorted(failed_requirements_checks, key=lambda x: (x.identifier, x.severity)): # add only the issues for the current profile when the `target_profile_only` mode is disabled # (issues related to other profiles will be added by the corresponding profile validation) - # failed_check_ids.append(requirementCheck.identifier) if requirementCheck.requirement.profile == shacl_context.current_validation_profile or \ shacl_context.settings.get("target_only_validation", False): - # failed_check_ids.append(requirementCheck.identifier) c = shacl_context.result.add_check_issue(message=violation.get_result_message(shacl_context.rocrate_path), check=requirementCheck, severity=violation.get_result_severity(), From 331625aff29f4161a81407974fb14366329173af Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Jul 2024 15:47:08 +0200 Subject: [PATCH 685/902] fix(shacl): :ambulance: use the right violation object --- rocrate_validator/requirements/shacl/checks.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 65247a2e..e4350a6a 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -13,7 +13,7 @@ from .validator import (SHACLValidationAlreadyProcessed, SHACLValidationContext, SHACLValidationContextManager, - SHACLValidationSkip, SHACLValidator) + SHACLValidationSkip, SHACLValidator, SHACLViolation) logger = logging.getLogger(__name__) @@ -121,6 +121,7 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): result = shacl_result.conforms # if the validation fails, process the failed checks failed_requirements_checks = [] + failed_requirements_checks_violations: dict[str, SHACLViolation] = {} failed_requirement_checks_notified = [] if not shacl_result.conforms: logger.debug("Parsing Validation with result: %s", result) @@ -131,12 +132,14 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): requirementCheck = SHACLCheck.get_instance(shape) assert requirementCheck is not None, "The requirement check cannot be None" failed_requirements_checks.append(requirementCheck) + failed_requirements_checks_violations[requirementCheck.identifier] = violation # sort the failed checks by identifier and severity # to ensure a consistent order of the issues # and to make the fail fast mode deterministic for requirementCheck in sorted(failed_requirements_checks, key=lambda x: (x.identifier, x.severity)): # add only the issues for the current profile when the `target_profile_only` mode is disabled # (issues related to other profiles will be added by the corresponding profile validation) + violation = failed_requirements_checks_violations[requirementCheck.identifier] if requirementCheck.requirement.profile == shacl_context.current_validation_profile or \ shacl_context.settings.get("target_only_validation", False): c = shacl_context.result.add_check_issue(message=violation.get_result_message(shacl_context.rocrate_path), From 5008baaee3d370ab5a911b6b56714442e7a267ec Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Jul 2024 16:49:38 +0200 Subject: [PATCH 686/902] fix(shacl): :bug: fix issue reporting --- .../requirements/shacl/checks.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index e4350a6a..7c2b50ff 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -120,7 +120,7 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): start_time = timer() result = shacl_result.conforms # if the validation fails, process the failed checks - failed_requirements_checks = [] + failed_requirements_checks = set() failed_requirements_checks_violations: dict[str, SHACLViolation] = {} failed_requirement_checks_notified = [] if not shacl_result.conforms: @@ -131,27 +131,30 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): assert shape is not None, "Unable to map the violation to a shape" requirementCheck = SHACLCheck.get_instance(shape) assert requirementCheck is not None, "The requirement check cannot be None" - failed_requirements_checks.append(requirementCheck) - failed_requirements_checks_violations[requirementCheck.identifier] = violation + failed_requirements_checks.add(requirementCheck) + violations = failed_requirements_checks_violations.get(requirementCheck.identifier, None) + if violations is None: + failed_requirements_checks_violations[requirementCheck.identifier] = violations = [] + violations.append(violation) # sort the failed checks by identifier and severity # to ensure a consistent order of the issues # and to make the fail fast mode deterministic for requirementCheck in sorted(failed_requirements_checks, key=lambda x: (x.identifier, x.severity)): # add only the issues for the current profile when the `target_profile_only` mode is disabled # (issues related to other profiles will be added by the corresponding profile validation) - violation = failed_requirements_checks_violations[requirementCheck.identifier] if requirementCheck.requirement.profile == shacl_context.current_validation_profile or \ shacl_context.settings.get("target_only_validation", False): - c = shacl_context.result.add_check_issue(message=violation.get_result_message(shacl_context.rocrate_path), - check=requirementCheck, - severity=violation.get_result_severity(), - resultPath=violation.resultPath.toPython() if violation.resultPath else None, - focusNode=make_uris_relative( - violation.focusNode.toPython(), shacl_context.publicID), - value=violation.value) - # if the fail fast mode is enabled, stop the validation after the first issue - if shacl_context.fail_fast: - break + for violation in failed_requirements_checks_violations[requirementCheck.identifier]: + c = shacl_context.result.add_check_issue(message=violation.get_result_message(shacl_context.rocrate_path), + check=requirementCheck, + severity=violation.get_result_severity(), + resultPath=violation.resultPath.toPython() if violation.resultPath else None, + focusNode=make_uris_relative( + violation.focusNode.toPython(), shacl_context.publicID), + value=violation.value) + # if the fail fast mode is enabled, stop the validation after the first issue + if shacl_context.fail_fast: + break # If the fail fast mode is disabled, notify all the validation issues # related to profiles other than the current one. From b5cce1584530a2dd96782f084b8624601ad5bafe Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Jul 2024 16:58:01 +0200 Subject: [PATCH 687/902] fix(cli): :lipstick: fix spaces on CLI output --- rocrate_validator/cli/commands/validate.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index a3a8b39c..79c790f9 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -546,14 +546,15 @@ def show_validation_details(self, enable_pager: bool = True): key=lambda x: (-x.severity.value, x)): path = "" if issue.resultPath and issue.value: - path = f"of [yellow]{issue.resultPath}[/yellow]" + path = f" of [yellow]{issue.resultPath}[/yellow]" if issue.value: if issue.resultPath: path += "=" path += f"\"[green]{issue.value}[/green]\" " # keep the ending space - path = path + "on " + f"[cyan]<{issue.focusNode}>[/cyan]" + if issue.focusNode: + path = f"{path} on [cyan]<{issue.focusNode}>[/cyan]" console.print( - Padding(f"- [[red]Violation[/red] {path}]: " + Padding(f"- [[red]Violation[/red]{path}]: " f"{Markdown(issue.message).markup}", (0, 9)), style="white") console.print("\n", style="white") From 96fdf52f209e3c9a0a0bf8d2490013842548a643 Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 24 Jul 2024 17:25:53 +0200 Subject: [PATCH 688/902] procrc: software dependencies checks --- .../may/0_software-application.ttl | 35 +++++ .../ro-crate-metadata.json | 144 ++++++++++++++++++ .../ro-crate-metadata.json | 137 +++++++++++++++++ .../process-run-crate/ro-crate-metadata.json | 11 +- .../process-run-crate/test_prc_application.py | 30 ++++ tests/ro_crates.py | 8 + 6 files changed, 364 insertions(+), 1 deletion(-) create mode 100644 rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl create mode 100644 tests/data/crates/invalid/3_process_run_crate/softwareapplication_bad_softwarerequirements/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/3_process_run_crate/softwareapplication_no_softwarerequirements/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl b/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl new file mode 100644 index 00000000..682b4247 --- /dev/null +++ b/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl @@ -0,0 +1,35 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . +@prefix rocrate: . +@prefix bioschemas: . + + +ro:ProcRCSoftwareApplicationOptional a sh:NodeShape ; + sh:name "ProcRC SoftwareApplication MAY" ; + sh:description "Optional properties of a Process Run Crate SoftwareApplication" ; + # Avoid performing checks on dependencies + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:SoftwareApplication . + FILTER NOT EXISTS { ?other schema:softwareRequirements ?this } . + } + """ + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "SoftwareApplication softwareRequirements" ; + sh:description "The SoftwareApplication MAY have a softwareRequirements that points to a SoftwareApplication" ; + sh:message "The SoftwareApplication MAY have a softwareRequirements that points to a SoftwareApplication" ; + sh:path schema:softwareRequirements ; + sh:class schema:SoftwareApplication ; + sh:minCount 1 ; + ] . diff --git a/tests/data/crates/invalid/3_process_run_crate/softwareapplication_bad_softwarerequirements/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/softwareapplication_bad_softwarerequirements/ro-crate-metadata.json new file mode 100644 index 00000000..61391f69 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/softwareapplication_bad_softwarerequirements/ro-crate-metadata.json @@ -0,0 +1,144 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@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/" + } + }, + { + "@id": "https://example.com/foobar/", + "@type": "CreativeWork" + }, + { + "@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": "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/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/invalid/3_process_run_crate/softwareapplication_no_softwarerequirements/ro-crate-metadata.json b/tests/data/crates/invalid/3_process_run_crate/softwareapplication_no_softwarerequirements/ro-crate-metadata.json new file mode 100644 index 00000000..7d636aa5 --- /dev/null +++ b/tests/data/crates/invalid/3_process_run_crate/softwareapplication_no_softwarerequirements/ro-crate-metadata.json @@ -0,0 +1,137 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/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.1" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/sepia_fence.jpg" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + }, + "name": "My Pictures" + }, + { + "@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" + }, + { + "@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": "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/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/valid/process-run-crate/ro-crate-metadata.json b/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json index 7d636aa5..5beab1c1 100644 --- a/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json +++ b/tests/data/crates/valid/process-run-crate/ro-crate-metadata.json @@ -61,7 +61,16 @@ "@type": "SoftwareApplication", "url": "https://www.imagemagick.org/", "name": "ImageMagick", - "softwareVersion": "6.9.7-4" + "softwareVersion": "6.9.7-4", + "softwareRequirements": { + "@id": "https://example.com/foobar/1.0.0/" + } + }, + { + "@id": "https://example.com/foobar/1.0.0/", + "@type": "SoftwareApplication", + "name": "foobar", + "softwareVersion": "1.0.0" }, { "@id": "#SepiaConversion_1", diff --git a/tests/integration/profiles/process-run-crate/test_prc_application.py b/tests/integration/profiles/process-run-crate/test_prc_application.py index 9d8c4a51..bc8a6c56 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_application.py +++ b/tests/integration/profiles/process-run-crate/test_prc_application.py @@ -94,3 +94,33 @@ def test_prc_application_id_no_absoluteuri(): ["The SoftwareApplication id SHOULD be an absolute URI"], profile_identifier="process-run-crate" ) + + +def test_prc_softwareapplication_no_softwarerequirements(): + """\ + Test a Process Run Crate where the SoftwareApplication does not have a + SoftwareRequirements. + """ + do_entity_test( + InvalidProcRC().softwareapplication_no_softwarerequirements, + Severity.OPTIONAL, + False, + ["ProcRC SoftwareApplication MAY"], + ["The SoftwareApplication MAY have a softwareRequirements that points to a SoftwareApplication"], + profile_identifier="process-run-crate" + ) + + +def test_prc_softwareapplication_bad_softwarerequirements(): + """\ + Test a Process Run Crate where the SoftwareApplication has a + SoftwareRequirements that does not point to a SoftwareApplication. + """ + do_entity_test( + InvalidProcRC().softwareapplication_bad_softwarerequirements, + Severity.OPTIONAL, + False, + ["ProcRC SoftwareApplication MAY"], + ["The SoftwareApplication MAY have a softwareRequirements that points to a SoftwareApplication"], + profile_identifier="process-run-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 86f545c6..4fe088b4 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -433,3 +433,11 @@ def containerimage_no_tag(self) -> Path: @property def containerimage_no_sha256(self) -> Path: return self.base_path / "containerimage_no_sha256" + + @property + def softwareapplication_no_softwarerequirements(self) -> Path: + return self.base_path / "softwareapplication_no_softwarerequirements" + + @property + def softwareapplication_bad_softwarerequirements(self) -> Path: + return self.base_path / "softwareapplication_bad_softwarerequirements" From 4a3b0eb7ce96bad943b65f8abcc0e1bc688e95d5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 24 Jul 2024 17:44:37 +0200 Subject: [PATCH 689/902] fix(cli): :lipstick: update padding --- 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 79c790f9..880d82a0 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -414,7 +414,7 @@ def __init_layout__(self): # Create the main layout self.checks_stats_layout = Layout( Panel(report_container_layout, title=f"[bold]RO-Crate Validator[/bold] [white](ver. [magenta]{get_version()}[/magenta])[/white]", - border_style="cyan", title_align="center", padding=(1, 1))) + border_style="cyan", title_align="center", padding=(1, 2))) # Create the overall result layout self.overall_result = Layout( @@ -424,7 +424,7 @@ def __init_layout__(self): group_layout.add_split(self.checks_stats_layout) group_layout.add_split(self.overall_result) - self.__layout = group_layout + self.__layout = Padding(group_layout, (2, 1)) def update(self, profile_stats: dict = None): assert profile_stats, "Profile stats must be provided" From 873520e3ebec192b94e2052f369a6ce7d698e73a Mon Sep 17 00:00:00 2001 From: simleo Date: Fri, 26 Jul 2024 11:54:33 +0200 Subject: [PATCH 690/902] prc -> procrc --- ...st_prc_action.py => test_procrc_action.py} | 46 +++++++++---------- ...lication.py => test_procrc_application.py} | 16 +++---- ...ollection.py => test_procrc_collection.py} | 6 +-- ...image.py => test_procrc_containerimage.py} | 12 ++--- ...ity.py => test_procrc_root_data_entity.py} | 6 +-- 5 files changed, 43 insertions(+), 43 deletions(-) rename tests/integration/profiles/process-run-crate/{test_prc_action.py => test_procrc_action.py} (91%) rename tests/integration/profiles/process-run-crate/{test_prc_application.py => test_procrc_application.py} (89%) rename tests/integration/profiles/process-run-crate/{test_prc_collection.py => test_procrc_collection.py} (91%) rename tests/integration/profiles/process-run-crate/{test_prc_containerimage.py => test_procrc_containerimage.py} (90%) rename tests/integration/profiles/process-run-crate/{test_prc_root_data_entity.py => test_procrc_root_data_entity.py} (93%) diff --git a/tests/integration/profiles/process-run-crate/test_prc_action.py b/tests/integration/profiles/process-run-crate/test_procrc_action.py similarity index 91% rename from tests/integration/profiles/process-run-crate/test_prc_action.py rename to tests/integration/profiles/process-run-crate/test_procrc_action.py index d06b0667..3d06a169 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_action.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_action.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -def test_prc_action_no_instrument(): +def test_procrc_action_no_instrument(): """\ Test a Process Run Crate where the action does not have an instrument. """ @@ -22,7 +22,7 @@ def test_prc_action_no_instrument(): ) -def test_prc_action_instrument_bad_type(): +def test_procrc_action_instrument_bad_type(): """\ Test a Process Run Crate where the instrument does not point to a SoftwareApplication, SoftwareSourceCode or ComputationalWorkflow. @@ -37,7 +37,7 @@ def test_prc_action_instrument_bad_type(): ) -def test_prc_action_not_mentioned(): +def test_procrc_action_not_mentioned(): """\ Test a Process Run Crate where the action is not listed in the Root Data Entity's mentions. @@ -52,7 +52,7 @@ def test_prc_action_not_mentioned(): ) -def test_prc_action_no_name(): +def test_procrc_action_no_name(): """\ Test a Process Run Crate where the action does not have an name. """ @@ -66,7 +66,7 @@ def test_prc_action_no_name(): ) -def test_prc_action_no_description(): +def test_procrc_action_no_description(): """\ Test a Process Run Crate where the action does not have a description. """ @@ -80,7 +80,7 @@ def test_prc_action_no_description(): ) -def test_prc_action_no_endtime(): +def test_procrc_action_no_endtime(): """\ Test a Process Run Crate where the action does not have an endTime. """ @@ -94,7 +94,7 @@ def test_prc_action_no_endtime(): ) -def test_prc_action_bad_endtime(): +def test_procrc_action_bad_endtime(): """\ Test a Process Run Crate where the action does not have an endTime. """ @@ -108,7 +108,7 @@ def test_prc_action_bad_endtime(): ) -def test_prc_action_no_agent(): +def test_procrc_action_no_agent(): """\ Test a Process Run Crate where the action does not have an agent. """ @@ -122,7 +122,7 @@ def test_prc_action_no_agent(): ) -def test_prc_action_bad_agent(): +def test_procrc_action_bad_agent(): """\ Test a Process Run Crate where the agent is neither a Person nor an Organization. @@ -137,7 +137,7 @@ def test_prc_action_bad_agent(): ) -def test_prc_action_no_result(): +def test_procrc_action_no_result(): """\ Test a Process Run Crate where the CreateAction or UpdateAction does not have a result. @@ -152,7 +152,7 @@ def test_prc_action_no_result(): ) -def test_prc_action_no_starttime(): +def test_procrc_action_no_starttime(): """\ Test a Process Run Crate where the action does not have an startTime. """ @@ -166,7 +166,7 @@ def test_prc_action_no_starttime(): ) -def test_prc_action_bad_starttime(): +def test_procrc_action_bad_starttime(): """\ Test a Process Run Crate where the action does not have an startTime. """ @@ -180,7 +180,7 @@ def test_prc_action_bad_starttime(): ) -def test_prc_action_error_not_failed_status(): +def test_procrc_action_error_not_failed_status(): """\ Test a Process Run Crate where the action has an error even though its actionStatus is not FailedActionStatus. @@ -195,7 +195,7 @@ def test_prc_action_error_not_failed_status(): ) -def test_prc_action_error_no_status(): +def test_procrc_action_error_no_status(): """\ Test a Process Run Crate where the action has an error even though it has no actionStatus. @@ -210,7 +210,7 @@ def test_prc_action_error_no_status(): ) -def test_prc_action_no_object(): +def test_procrc_action_no_object(): """\ Test a Process Run Crate where the Action does not have an object. """ @@ -224,7 +224,7 @@ def test_prc_action_no_object(): ) -def test_prc_action_no_actionstatus(): +def test_procrc_action_no_actionstatus(): """\ Test a Process Run Crate where the Action does not have an actionstatus. """ @@ -238,7 +238,7 @@ def test_prc_action_no_actionstatus(): ) -def test_prc_action_bad_actionstatus(): +def test_procrc_action_bad_actionstatus(): """\ Test a Process Run Crate where the Action has an invalid actionstatus. """ @@ -252,7 +252,7 @@ def test_prc_action_bad_actionstatus(): ) -def test_prc_action_no_error(): +def test_procrc_action_no_error(): """\ Test a Process Run Crate where the Action does not have an error. """ @@ -266,7 +266,7 @@ def test_prc_action_no_error(): ) -def test_prc_action_obj_res_bad_type(): +def test_procrc_action_obj_res_bad_type(): """\ Test a Process Run Crate where the Action's object or result does not point to a MediaObject, Dataset, Collection, CreativeWork or @@ -282,7 +282,7 @@ def test_prc_action_obj_res_bad_type(): ) -def test_prc_action_no_environment(): +def test_procrc_action_no_environment(): """\ Test a Process Run Crate where the Action does not have an environment. """ @@ -296,7 +296,7 @@ def test_prc_action_no_environment(): ) -def test_prc_action_bad_environment(): +def test_procrc_action_bad_environment(): """\ Test a Process Run Crate where the Action has an environment that does not point to PropertyValues. @@ -311,7 +311,7 @@ def test_prc_action_bad_environment(): ) -def test_prc_action_no_containerimage(): +def test_procrc_action_no_containerimage(): """\ Test a Process Run Crate where the Action does not have a containerimage. """ @@ -325,7 +325,7 @@ def test_prc_action_no_containerimage(): ) -def test_prc_action_bad_containerimage(): +def test_procrc_action_bad_containerimage(): """\ Test a Process Run Crate where the Action has a containerImage that does not point to a URL or to a ContainerImage object. diff --git a/tests/integration/profiles/process-run-crate/test_prc_application.py b/tests/integration/profiles/process-run-crate/test_procrc_application.py similarity index 89% rename from tests/integration/profiles/process-run-crate/test_prc_application.py rename to tests/integration/profiles/process-run-crate/test_procrc_application.py index bc8a6c56..631ba290 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_application.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_application.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -def test_prc_application_no_name(): +def test_procrc_application_no_name(): """\ Test a Process Run Crate where the application does not have a name. """ @@ -22,7 +22,7 @@ def test_prc_application_no_name(): ) -def test_prc_application_no_url(): +def test_procrc_application_no_url(): """\ Test a Process Run Crate where the application does not have a url. """ @@ -36,7 +36,7 @@ def test_prc_application_no_url(): ) -def test_prc_application_no_version(): +def test_procrc_application_no_version(): """\ Test a Process Run Crate where the application does not have a version or SoftwareVersion (SoftwareApplication). @@ -51,7 +51,7 @@ def test_prc_application_no_version(): ) -def test_prc_application_version_softwareversion(): +def test_procrc_application_version_softwareversion(): """\ Test a Process Run Crate where the application has both a version and a SoftwareVersion (SoftwareApplication). @@ -66,7 +66,7 @@ def test_prc_application_version_softwareversion(): ) -def test_prc_softwaresourcecode_no_version(): +def test_procrc_softwaresourcecode_no_version(): """\ Test a Process Run Crate where the application does not have a version (SoftwareSourceCode). @@ -81,7 +81,7 @@ def test_prc_softwaresourcecode_no_version(): ) -def test_prc_application_id_no_absoluteuri(): +def test_procrc_application_id_no_absoluteuri(): """\ Test a Process Run Crate where the id of the application is not an absolute URI. @@ -96,7 +96,7 @@ def test_prc_application_id_no_absoluteuri(): ) -def test_prc_softwareapplication_no_softwarerequirements(): +def test_procrc_softwareapplication_no_softwarerequirements(): """\ Test a Process Run Crate where the SoftwareApplication does not have a SoftwareRequirements. @@ -111,7 +111,7 @@ def test_prc_softwareapplication_no_softwarerequirements(): ) -def test_prc_softwareapplication_bad_softwarerequirements(): +def test_procrc_softwareapplication_bad_softwarerequirements(): """\ Test a Process Run Crate where the SoftwareApplication has a SoftwareRequirements that does not point to a SoftwareApplication. diff --git a/tests/integration/profiles/process-run-crate/test_prc_collection.py b/tests/integration/profiles/process-run-crate/test_procrc_collection.py similarity index 91% rename from tests/integration/profiles/process-run-crate/test_prc_collection.py rename to tests/integration/profiles/process-run-crate/test_procrc_collection.py index 59a08d29..d76ee275 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_collection.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_collection.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -def test_prc_collection_not_mentioned(): +def test_procrc_collection_not_mentioned(): """\ Test a Process Run Crate where the collection is not listed in the Root Data Entity's mentions. @@ -23,7 +23,7 @@ def test_prc_collection_not_mentioned(): ) -def test_prc_collection_no_haspart(): +def test_procrc_collection_no_haspart(): """\ Test a Process Run Crate where the collection does not have a hasPart. """ @@ -37,7 +37,7 @@ def test_prc_collection_no_haspart(): ) -def test_prc_collection_no_mainentity(): +def test_procrc_collection_no_mainentity(): """\ Test a Process Run Crate where the collection does not have a mainEntity. """ diff --git a/tests/integration/profiles/process-run-crate/test_prc_containerimage.py b/tests/integration/profiles/process-run-crate/test_procrc_containerimage.py similarity index 90% rename from tests/integration/profiles/process-run-crate/test_prc_containerimage.py rename to tests/integration/profiles/process-run-crate/test_procrc_containerimage.py index 7397abdd..800233ab 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_containerimage.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_containerimage.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -def test_prc_containerimage_no_additionaltype(): +def test_procrc_containerimage_no_additionaltype(): """\ Test a Process Run Crate where the ContainerImage has no additionalType. """ @@ -22,7 +22,7 @@ def test_prc_containerimage_no_additionaltype(): ) -def test_prc_containerimage_bad_additionaltype(): +def test_procrc_containerimage_bad_additionaltype(): """\ Test a Process Run Crate where the ContainerImage additionalType does not point to one of the allowed values. @@ -37,7 +37,7 @@ def test_prc_containerimage_bad_additionaltype(): ) -def test_prc_containerimage_no_registry(): +def test_procrc_containerimage_no_registry(): """\ Test a Process Run Crate where the ContainerImage has no registry. """ @@ -51,7 +51,7 @@ def test_prc_containerimage_no_registry(): ) -def test_prc_containerimage_no_name(): +def test_procrc_containerimage_no_name(): """\ Test a Process Run Crate where the ContainerImage has no name. """ @@ -65,7 +65,7 @@ def test_prc_containerimage_no_name(): ) -def test_prc_containerimage_no_tag(): +def test_procrc_containerimage_no_tag(): """\ Test a Process Run Crate where the ContainerImage has no tag. """ @@ -79,7 +79,7 @@ def test_prc_containerimage_no_tag(): ) -def test_prc_containerimage_no_sha256(): +def test_procrc_containerimage_no_sha256(): """\ Test a Process Run Crate where the ContainerImage has no sha256. """ diff --git a/tests/integration/profiles/process-run-crate/test_prc_root_data_entity.py b/tests/integration/profiles/process-run-crate/test_procrc_root_data_entity.py similarity index 93% rename from tests/integration/profiles/process-run-crate/test_prc_root_data_entity.py rename to tests/integration/profiles/process-run-crate/test_procrc_root_data_entity.py index 874ee0ae..6c4f4e57 100644 --- a/tests/integration/profiles/process-run-crate/test_prc_root_data_entity.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_root_data_entity.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -def test_prc_no_conformsto(): +def test_procrc_no_conformsto(): """\ Test a Process Run Crate where the root data entity does not have a conformsTo. @@ -23,7 +23,7 @@ def test_prc_no_conformsto(): ) -def test_prc_conformsto_bad_type(): +def test_procrc_conformsto_bad_type(): """\ Test a Process Run Crate where the root data entity does not conformsTo a CreativeWork. @@ -38,7 +38,7 @@ def test_prc_conformsto_bad_type(): ) -def test_prc_conformsto_bad_profile(): +def test_procrc_conformsto_bad_profile(): """\ Test a Process Run Crate where the root data entity does not conformsTo a Process Run Crate profile. From 30e3db6892213079b1a75acffac42ea87d632385 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 25 Jul 2024 09:36:36 +0200 Subject: [PATCH 691/902] feat(core): :sparkles: add method to autodetect rocrate profile --- rocrate_validator/models.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 05121ea3..29e03230 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1241,6 +1241,38 @@ def __init__(self, settings: Union[str, ValidationSettings]): def validation_settings(self) -> ValidationSettings: return self._validation_settings + def detect_rocrate_profiles(self) -> list[Profile]: + """ + Detect the profiles to validate against + """ + try: + # initialize the validation context + context = ValidationContext(self, self.validation_settings.to_dict()) + candidate_profiles_uris = context.ro_crate.metadata.get_conforms_to() + logger.debug("Candidate profiles: %s", candidate_profiles_uris) + if not candidate_profiles_uris: + logger.debug("Unable to determine the profile to validate against") + return None + # load the profiles + profiles = [] + candidate_profiles = [] + available_profiles = Profile.load_profiles(context.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: + candidate_profiles.remove(inherited_profile) + logger.debug("%d Candidate Profiles found: %s", len(candidate_profiles), candidate_profiles) + return candidate_profiles + + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return None + def validate(self) -> ValidationResult: return self.__do_validate__() From d06e001b46b65621d5658ce8dc4f3be92c060bd7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 25 Jul 2024 09:40:04 +0200 Subject: [PATCH 692/902] refactor(core): :recycle: factorise initialisation of validator object --- rocrate_validator/services.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index b6d09533..cfcd78cd 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -27,6 +27,19 @@ def validate(settings: Union[dict, ValidationSettings], """ Validate a RO-Crate against a profile """ + # initialize the validator + validator = __initialise_validator__(settings, subscribers) + # validate the RO-Crate + result = validator.validate() + logger.debug("Validation completed: %s", result) + return result + + +def __initialise_validator__(settings: Union[dict, ValidationSettings], + subscribers: Optional[list[Subscriber]] = None) -> Validator: + """ + Validate a RO-Crate against a profile + """ # if settings is a dict, convert to ValidationSettings settings = ValidationSettings.parse(settings) @@ -59,22 +72,16 @@ def validate(settings: Union[dict, ValidationSettings], if subscribers: for subscriber in subscribers: validator.add_subscriber(subscriber) - # validate the RO-Crate - result = validator.validate() - logger.debug("Validation completed: %s", result) - return result + return validator - def __do_validate__(settings: ValidationSettings): + def __init_validator__(settings: ValidationSettings) -> Validator: # create a validator validator = Validator(settings) logger.debug("Validator created. Starting validation...") if subscribers: for subscriber in subscribers: validator.add_subscriber(subscriber) - # validate the RO-Crate - result = validator.validate() - logger.debug("Validation completed: %s", result) - return result + return validator def __extract_and_validate_rocrate__(rocrate_path: Path): # store the original data path @@ -89,7 +96,7 @@ def __extract_and_validate_rocrate__(rocrate_path: Path): # update the data path to point to the temporary directory settings.data_path = Path(tmp_dir) # continue with the validation process - return __do_validate__(settings) + return __init_validator__(settings) finally: # restore the original data path settings.data_path = original_data_path @@ -121,7 +128,7 @@ def __extract_and_validate_rocrate__(rocrate_path: Path): elif rocrate_path.is_local_directory(): logger.debug("RO-Crate is a local directory") settings.data_path = rocrate_path.as_path() - return __do_validate__(settings) + return __init_validator__(settings) else: raise ValueError( f"Invalid RO-Crate URI: {rocrate_path}. " From 0f3d44ac6dad2f55989a38a6c17ef7813dd5c92d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 25 Jul 2024 09:41:42 +0200 Subject: [PATCH 693/902] feat(core): :sparkles: expose `conforms_to` property on ROCrate objects --- rocrate_validator/rocrate.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/rocrate.py b/rocrate_validator/rocrate.py index db5888f5..a8a4eb32 100644 --- a/rocrate_validator/rocrate.py +++ b/rocrate_validator/rocrate.py @@ -6,7 +6,7 @@ import zipfile from abc import ABC, abstractmethod from pathlib import Path -from typing import Union +from typing import Optional, Union import requests from rdflib import Graph @@ -190,6 +190,19 @@ def get_web_data_entities(self) -> list[ROCrateEntity]: entities.append(entity) return entities + def get_conforms_to(self) -> Optional[list[str]]: + try: + file_descriptor = self.get_file_descriptor_entity() + result = file_descriptor.get_property('conformsTo', []) + if not isinstance(result, list): + result = [result] + return [_["@id"] for _ in result] + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + logger.exception(e) # TODO: remove + return None + def as_json(self) -> str: if not self._json: self._json = self.ro_crate.get_file_content( From eb85ac7c9e2c30bfcbdc2f82283b949da6128a1b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 25 Jul 2024 09:43:25 +0200 Subject: [PATCH 694/902] feat(services): :sparkles: expose profile detection at service level --- rocrate_validator/services.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index cfcd78cd..a144c229 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -22,6 +22,15 @@ logger = logging.getLogger(__name__) +def detect_profiles(settings: Union[dict, ValidationSettings]) -> list[Profile]: + # initialize the validator + validator = __initialise_validator__(settings) + # detect the profiles + profiles = validator.detect_rocrate_profiles() + logger.debug("Profiles detected: %s", profiles) + return profiles + + def validate(settings: Union[dict, ValidationSettings], subscribers: Optional[list[Subscriber]] = None) -> ValidationResult: """ From 957d6bb9763500902c70c0c44c4ffed25f15d90f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 25 Jul 2024 09:45:32 +0200 Subject: [PATCH 695/902] feat(cli): :sparkles: improve existing prompt function --- rocrate_validator/cli/commands/validate.py | 33 ++++++++++++++-------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 880d82a0..5f294569 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -58,22 +58,33 @@ def validate_uri(ctx, param, value): return value -def get_single_char(console: Optional[Console] = None, end: str = "\n") -> str: +def get_single_char(console: Optional[Console] = None, end: str = "\n", + message: Optional[str] = None, + choices: Optional[list[str]] = None) -> str: """ Get a single character from the console """ fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) - try: - tty.setraw(sys.stdin.fileno()) - char = sys.stdin.read(1) - finally: - termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) - if console: - console.print(char, end=end) + + char = None + while char is None or (choices and char not in choices): + if console and message: + console.print(f"\n{message}", end="") + try: + tty.setraw(sys.stdin.fileno()) + char = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + if console: + console.print(char, end=end if choices and char in choices else "") + if choices and char not in choices: + if console: + console.print(f" [bold red]INVALID CHOICE[/bold red]", end=end) return char + @cli.command("validate") @click.argument("rocrate-uri", callback=validate_uri, default=".") @click.option( @@ -220,9 +231,9 @@ def validate(ctx, # Print the validation result if not result.passed(): if not details and enable_pager: - console.print("[bold]Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]", end="") - details = get_single_char(console).lower() == 'y' - if details: + details = get_single_char(console, choices=['y', 'n'], + message="[bold] > Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]") + if details == "y": report_layout.show_validation_details(enable_pager=enable_pager) if output_file: From fedb733f819037cd0ada6466a776a22380447e35 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 25 Jul 2024 09:46:27 +0200 Subject: [PATCH 696/902] feat(cli): :sparkles: add multiple-choice prompt function --- rocrate_validator/cli/commands/validate.py | 27 +++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 5f294569..44d817e6 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -15,14 +15,15 @@ from rich.padding import Padding from rich.panel import Panel from rich.progress import BarColumn, Progress, TextColumn, TimeElapsedColumn +from rich.prompt import Prompt from rich.rule import Rule +from rich.table import Table import rocrate_validator.log as logging from rocrate_validator import services from rocrate_validator.cli.commands.errors import handle_error from rocrate_validator.cli.main import cli, click from rocrate_validator.colors import get_severity_color -from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER from rocrate_validator.events import Event, EventType, Subscriber from rocrate_validator.models import (LevelCollection, Severity, ValidationResult) @@ -255,6 +256,30 @@ def validate(ctx, handle_error(e, console) +def multiple_choice(console: Console, + choices: list[str], + title: str = "Main Menu", + padding=None) -> str: + """ + Display a multiple choice menu + """ + table = Table(title=title, title_justify="center") + table.add_column("#", justify="center", style="bold cyan", no_wrap=True) + table.add_column("Option", justify="left", style="magenta") + + for index, choice in enumerate(choices, start=1): + table.add_row(str(index), choice) + + if padding: + console.print(Padding(table, padding)) + else: + console.print(table) + + selected_option = Prompt.ask("Please choose an option (enter the number)", + choices=[str(i) for i in range(1, len(choices) + 1)]) + return selected_option + + class ProgressMonitor(Subscriber): PROFILE_VALIDATION = "Profiles" From ef8a911c98c89db18c10abe5bc021622e46b2fd0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 25 Jul 2024 09:51:38 +0200 Subject: [PATCH 697/902] feat(cli): :sparkles: enable autodetection and interactive selection of profiles --- rocrate_validator/cli/commands/validate.py | 31 ++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 44d817e6..70dc4e48 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -107,7 +107,7 @@ def get_single_char(console: Optional[Console] = None, end: str = "\n", "-p", "--profile-identifier", type=click.STRING, - default=DEFAULT_PROFILE_IDENTIFIER, + default=None, show_default=True, help="Identifier of the profile to use for validation", ) @@ -168,7 +168,7 @@ def get_single_char(console: Optional[Console] = None, end: str = "\n", @click.pass_context def validate(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH, - profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, + profile_identifier: Optional[str] = None, disable_profile_inheritance: bool = False, requirement_severity: str = Severity.REQUIRED.name, requirement_severity_only: bool = False, @@ -216,6 +216,33 @@ def validate(ctx, "abort_on_first": not no_fail_fast } + # Detect the profile to use for validation + autodetection = False + selected_profile = profile_identifier + if selected_profile is None: + candidate_profiles = services.detect_profiles(settings=validation_settings) + if len(candidate_profiles) == 1: + logger.info("Profile identifier autodetected: %s", candidate_profiles[0].identifier) + autodetection = True + selected_profile = candidate_profiles[0].identifier + else: + logger.debug("Candidate profiles: %s", candidate_profiles) + available_profiles = services.get_profiles(profiles_path) + # Define the list of choices + choices = [ + f"[bold]{profile.identifier}[/bold]: [white]{profile.name}[/white]" for profile in available_profiles] + selected_option = multiple_choice( + console, choices, "[bold yellow]WARNING: [/bold yellow]" + "[bold]Unable to automatically detect the profile to use for validation.[/bold]\n" + f"{''*10}Please select a profile from the list below:") + selected_profile = available_profiles[int(selected_option) - 1].identifier + logger.error("Profile selected: %s", selected_profile) + # Set the selected profile + validation_settings["profile_identifier"] = selected_profile + validation_settings["profile_autodetected"] = autodetection + logger.debug("Profile selected for validation: %s", validation_settings["profile_identifier"]) + logger.debug("Profile autodetected: %s", autodetection) + # Compute the profile statistics profile_stats = __compute_profile_stats__(validation_settings) From 829865d2562581caae9c8c8d385e9a5dfb0ce5be Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 25 Jul 2024 09:52:47 +0200 Subject: [PATCH 698/902] feat(cli): :lipstick: add severity on short validation report --- rocrate_validator/cli/commands/validate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 70dc4e48..b9c14407 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -427,10 +427,10 @@ def __init_layout__(self): base_info_layout = Layout( Align( f"\n[bold cyan]RO-Crate:[/bold cyan] [bold]{URI(settings['data_path']).uri}[/bold]" - f"\n[bold cyan]Target Profile:[/bold cyan][bold magenta] {settings['profile_identifier']}[/bold magenta]", + f"\n[bold cyan]Target Profile:[/bold cyan][bold magenta] {settings['profile_identifier']}[/bold magenta] { '[italic](autodetected)[/italic]' if settings['profile_autodetected'] else ''}" + f"\n[bold cyan]Validation Severity:[/bold cyan] [bold]{settings['requirement_severity']}[/bold]", style="white", align="left"), - name="Base Info", size=4) - + name="Base Info", size=5) # self.passed_checks = Layout(name="PASSED") self.failed_checks = Layout(name="FAILED") From 589530da95ad5ab7177979caa2a8f437db7024be Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 25 Jul 2024 10:10:26 +0200 Subject: [PATCH 699/902] fix(core): :sparkles: ensure an entity is always returned, even if details are missing --- rocrate_validator/rocrate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/rocrate.py b/rocrate_validator/rocrate.py index a8a4eb32..3faf9a20 100644 --- a/rocrate_validator/rocrate.py +++ b/rocrate_validator/rocrate.py @@ -58,7 +58,10 @@ def has_types(self, entity_types: list[str]) -> bool: def get_property(self, name: str, default=None) -> Union[str, ROCrateEntity]: data = self._raw_data.get(name, default) if isinstance(data, dict) and '@id' in data: - return self.metadata.get_entity(data['@id']) + entity = self.metadata.get_entity(data['@id']) + if entity is None: + return ROCrateEntity(self, data) + return entity return data @property From 47095925f4554dc1a1f5d1dccf43deaa65296c96 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 25 Jul 2024 10:11:32 +0200 Subject: [PATCH 700/902] refactor(core): :recycle: update `get_conforms_to` implementation --- rocrate_validator/rocrate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/rocrate.py b/rocrate_validator/rocrate.py index 3faf9a20..fd45788e 100644 --- a/rocrate_validator/rocrate.py +++ b/rocrate_validator/rocrate.py @@ -197,9 +197,11 @@ def get_conforms_to(self) -> Optional[list[str]]: 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 [_.id for _ in result] except Exception as e: if logger.isEnabledFor(logging.DEBUG): logger.exception(e) From 300b9f5ee8c514ef9b4433841f030d1b2d7e7abf Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 25 Jul 2024 10:12:26 +0200 Subject: [PATCH 701/902] refactor(cli): :art: remote blank line --- rocrate_validator/cli/commands/validate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index b9c14407..438d0f65 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -85,7 +85,6 @@ def get_single_char(console: Optional[Console] = None, end: str = "\n", return char - @cli.command("validate") @click.argument("rocrate-uri", callback=validate_uri, default=".") @click.option( From 4244c9a35ece37e0d9cca48f65bdad959b6670eb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 25 Jul 2024 10:13:03 +0200 Subject: [PATCH 702/902] fix(cli): :bug: check if candidate profiles is defined --- 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 438d0f65..cce12834 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -220,7 +220,7 @@ def validate(ctx, selected_profile = profile_identifier if selected_profile is None: candidate_profiles = services.detect_profiles(settings=validation_settings) - if len(candidate_profiles) == 1: + if candidate_profiles and len(candidate_profiles) == 1: logger.info("Profile identifier autodetected: %s", candidate_profiles[0].identifier) autodetection = True selected_profile = candidate_profiles[0].identifier From d898bec3aa214250e16d0a07f830713d6d95001e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 25 Jul 2024 10:16:54 +0200 Subject: [PATCH 703/902] test(cli): :white_check_mark: fix cli test --- tests/test_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 273a4eb4..9a4630f9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -26,7 +26,7 @@ def test_version(cli_runner: CliRunner): def test_validate_subcmd_invalid_rocrate1(cli_runner: CliRunner): result = cli_runner.invoke(cli, ['validate', str( - InvalidFileDescriptor().invalid_json_format), '--details', '--no-paging']) + InvalidFileDescriptor().invalid_json_format), '--details', '--no-paging', '-p', 'ro-crate']) logger.error(result.output) assert result.exit_code == 1 From 952428cf763fc80ef60f4d60487dcd83e5a0aedf Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 25 Jul 2024 10:23:02 +0200 Subject: [PATCH 704/902] fix(cli): :lipstick: fix height of short report layout --- 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 cce12834..c9346a3e 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -420,7 +420,7 @@ def __init_layout__(self): settings = self.validation_settings # Set the console height - self.console.height = 30 + self.console.height = 31 # Create the layout of the base info of the validation report base_info_layout = Layout( From 53d0dfdd2dbae2af67d1933df2d7f0e2725d5ad6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 25 Jul 2024 10:27:00 +0200 Subject: [PATCH 705/902] feat(cli): :lipstick: fix severity color on short report --- rocrate_validator/cli/commands/validate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index c9346a3e..9707b1a3 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -423,11 +423,12 @@ def __init_layout__(self): self.console.height = 31 # Create the layout of the base info of the validation report + severity_color = get_severity_color(Severity.get(settings["requirement_severity"])) base_info_layout = Layout( Align( f"\n[bold cyan]RO-Crate:[/bold cyan] [bold]{URI(settings['data_path']).uri}[/bold]" f"\n[bold cyan]Target Profile:[/bold cyan][bold magenta] {settings['profile_identifier']}[/bold magenta] { '[italic](autodetected)[/italic]' if settings['profile_autodetected'] else ''}" - f"\n[bold cyan]Validation Severity:[/bold cyan] [bold]{settings['requirement_severity']}[/bold]", + f"\n[bold cyan]Validation Severity:[/bold cyan] [bold {severity_color}]{settings['requirement_severity']}[/bold {severity_color}]", style="white", align="left"), name="Base Info", size=5) # From f8f52540a690cc5dcc1064ecbbdb65db60af208a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 26 Jul 2024 13:13:24 +0200 Subject: [PATCH 706/902] fix(cli): :lipstick: update padding --- rocrate_validator/cli/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/cli/utils.py b/rocrate_validator/cli/utils.py index e723a721..856553da 100644 --- a/rocrate_validator/cli/utils.py +++ b/rocrate_validator/cli/utils.py @@ -34,4 +34,4 @@ def format_text(text: str, def get_app_header_rule() -> Text: return Padding(Rule(f"\n[bold][cyan]ROCrate Validator[/cyan] (ver. [magenta]{get_version()}[/magenta])[/bold]", - style="bold cyan"), (1, 1)) + style="bold cyan"), (1, 2)) From 7c10c7eba2e2a739645e7ccac067289a9770b5ac Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 26 Jul 2024 13:15:58 +0200 Subject: [PATCH 707/902] feat(utils): :sparkles: report logs at the end of its execution --- rocrate_validator/log.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/log.py b/rocrate_validator/log.py index 781b0488..a5782914 100644 --- a/rocrate_validator/log.py +++ b/rocrate_validator/log.py @@ -1,10 +1,16 @@ +import atexit import sys import threading +from io import StringIO from logging import (CRITICAL, DEBUG, ERROR, INFO, WARNING, Logger, StreamHandler) from typing import Optional import colorlog +from rich.console import Console +from rich.padding import Padding +from rich.rule import Rule +from rich.text import Text # set the module to the current module __module__ = sys.modules[__name__] @@ -65,6 +71,28 @@ def _releaseLock(): __handlers__ = {} +# Create a StringIO stream to capture the logs +__log_stream__ = StringIO() + + +# Define the callback function that will be called on exit +def __print_logs_on_exit__(): + log_contents = __log_stream__.getvalue() + if not log_contents: + return + # print the logs + console = Console() + console.print(Padding(Rule("[bold cyan]Log Report[/bold cyan]", style="bold cyan"), (2, 0, 1, 0))) + console.print(Padding(Text(log_contents), (0, 1))) + console.print(Padding(Rule("", style="bold cyan"), (0, 0, 2, 0))) + # close the stream + __log_stream__.close() + + +# Register the callback with atexit +atexit.register(__print_logs_on_exit__) + + def __setup_logger__(logger: Logger): # prevent the logger from propagating the log messages to the root logger @@ -84,10 +112,9 @@ def __setup_logger__(logger: Logger): # configure the logger handler ch = __handlers__.get(logger.name, None) if not ch: - ch = StreamHandler() + ch = StreamHandler(__log_stream__) ch.setLevel(level) ch.setFormatter(colorlog.ColoredFormatter(get_log_format(level))) - logger.addHandler(ch) # enable/disable the logger @@ -138,6 +165,7 @@ def basicConfig(level: int, modules_config: Optional[dict] = None): 'ERROR': 'red', 'CRITICAL': 'red,bg_white', }, + handlers=[StreamHandler(__log_stream__)] ) # reconfigure existing loggers From e47049d7ecaafe52b10d6ccbeb465ff02d959a81 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 26 Jul 2024 13:17:50 +0200 Subject: [PATCH 708/902] refactor(cli): :lipstick: update style of mulitple choice element --- rocrate_validator/cli/commands/validate.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 9707b1a3..595a63d4 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -285,11 +285,11 @@ def validate(ctx, def multiple_choice(console: Console, choices: list[str], title: str = "Main Menu", - padding=None) -> str: + padding=(1, 2)) -> str: """ Display a multiple choice menu """ - table = Table(title=title, title_justify="center") + table = Table(title=title, title_justify="left") table.add_column("#", justify="center", style="bold cyan", no_wrap=True) table.add_column("Option", justify="left", style="magenta") @@ -297,11 +297,18 @@ def multiple_choice(console: Console, table.add_row(str(index), choice) if padding: - console.print(Padding(table, padding)) + console.print(Padding(Align(table, align="left"), padding)) else: console.print(table) - selected_option = Prompt.ask("Please choose an option (enter the number)", + # Build the prompt text + prompt_text = "[bold] > Please select a profile (enter the number)[/bold]" + # console_width = console.size.width + # padding = (console_width - len(prompt_text)) // 2 + # centered_prompt_text = " " * padding + prompt_text + + # Get the selected option + selected_option = Prompt.ask(prompt_text, choices=[str(i) for i in range(1, len(choices) + 1)]) return selected_option From de37320f94c7de39d574860fc412494d2e6e7b33 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 26 Jul 2024 13:19:22 +0200 Subject: [PATCH 709/902] refactor(cli): :lipstick: update layout of validate output --- rocrate_validator/cli/commands/validate.py | 24 ++++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 595a63d4..6a386b90 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -23,6 +23,7 @@ from rocrate_validator import services from rocrate_validator.cli.commands.errors import handle_error from rocrate_validator.cli.main import cli, click +from rocrate_validator.cli.utils import get_app_header_rule from rocrate_validator.colors import get_severity_color from rocrate_validator.events import Event, EventType, Subscriber from rocrate_validator.models import (LevelCollection, Severity, @@ -215,27 +216,32 @@ def validate(ctx, "abort_on_first": not no_fail_fast } + # Print the application header + console.print(get_app_header_rule()) + # Detect the profile to use for validation autodetection = False selected_profile = profile_identifier if selected_profile is None: candidate_profiles = services.detect_profiles(settings=validation_settings) if candidate_profiles and len(candidate_profiles) == 1: - logger.info("Profile identifier autodetected: %s", candidate_profiles[0].identifier) + logger.debug("Profile identifier autodetected: %s", candidate_profiles[0].identifier) autodetection = True selected_profile = candidate_profiles[0].identifier else: logger.debug("Candidate profiles: %s", candidate_profiles) available_profiles = services.get_profiles(profiles_path) # Define the list of choices + # console.print(get_app_header_rule()) choices = [ f"[bold]{profile.identifier}[/bold]: [white]{profile.name}[/white]" for profile in 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))) selected_option = multiple_choice( - console, choices, "[bold yellow]WARNING: [/bold yellow]" - "[bold]Unable to automatically detect the profile to use for validation.[/bold]\n" - f"{''*10}Please select a profile from the list below:") + console, choices, "[italic]Available Profiles[/italic]", padding=(1, 2)) selected_profile = available_profiles[int(selected_option) - 1].identifier - logger.error("Profile selected: %s", selected_profile) + logger.debug("Profile selected: %s", selected_profile) + console.print(Padding(Rule(style="bold yellow"), (1, 2))) # Set the selected profile validation_settings["profile_identifier"] = selected_profile validation_settings["profile_autodetected"] = autodetection @@ -483,7 +489,7 @@ def __init_layout__(self): # Create the main layout self.checks_stats_layout = Layout( - Panel(report_container_layout, title=f"[bold]RO-Crate Validator[/bold] [white](ver. [magenta]{get_version()}[/magenta])[/white]", + Panel(report_container_layout, title=f"[bold]- Validation Report -[/bold]", border_style="cyan", title_align="center", padding=(1, 2))) # Create the overall result layout @@ -494,7 +500,7 @@ def __init_layout__(self): group_layout.add_split(self.checks_stats_layout) group_layout.add_split(self.overall_result) - self.__layout = Padding(group_layout, (2, 1)) + self.__layout = Padding(group_layout, (1, 1)) def update(self, profile_stats: dict = None): assert profile_stats, "Profile stats must be provided" @@ -568,11 +574,11 @@ def set_overall_result(self, result: ValidationResult): if result.passed(): self.overall_result.update( Padding(Rule(f"[bold][[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", - style="bold green"), (1, 0))) + style="bold green"), (1, 1))) else: self.overall_result.update( Padding(Rule(f"[bold][[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", - style="bold red"), (0, 0))) + style="bold red"), (1, 1))) def show_validation_details(self, enable_pager: bool = True): """ From d922e324a2492f88a1c29352dea5e65ad8b5a2df Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 26 Jul 2024 13:21:38 +0200 Subject: [PATCH 710/902] fix(profiles): :loud_sound: update logs on some pycheck --- .../workflow-ro-crate/may/1_main_workflow.py | 47 ++++++++++++------- .../workflow-ro-crate/must/0_main_workflow.py | 4 +- 2 files changed, 31 insertions(+), 20 deletions(-) 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 9218a2d0..d595e7e8 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,27 +14,38 @@ class WorkflowFilesExistence(PyFunctionCheck): @check(name="Workflow diagram existence") def check_workflow_diagram(self, context: ValidationContext) -> bool: """Check if the crate contains the workflow diagram.""" - 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_error(f"main workflow does not have an 'image' property", self) + 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_error(f"main workflow does not have an 'image' property", self) + return False + if not image.is_available(): + context.result.add_error(f"Workflow diagram '{image.id}' not found in crate", self) + return False + return True + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(f"Unexpected error: {e}") return False - if not image.is_available(): - context.result.add_error(f"Workflow diagram '{image.id}' not found in crate", self) - return False - return True @check(name="Workflow description existence") def check_workflow_description(self, context: ValidationContext) -> bool: """Check if the crate contains the workflow CWL description.""" - 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_error("main workflow does not have a 'subjectOf' property", self) - return False - if not main_workflow_subject.is_available(): - context.result.add_error(f"Workflow CWL description {main_workflow_subject.id} not found in crate", self) + 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_error("main workflow does not have a 'subjectOf' property", self) + return False + if not main_workflow_subject.is_available(): + context.result.add_error( + f"Workflow CWL description {main_workflow_subject.id} not found in crate", self) + return False + return True + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(f"Unexpected error: {e}") return False - return True 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 6bb63bb1..cfedb625 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 @@ -22,8 +22,8 @@ def check_workflow(self, context: ValidationContext) -> bool: if not main_workflow.is_available(): context.result.add_error(f"Main Workflow {main_workflow.id} not found in crate", self) return False + return True except ValueError as e: if logger.isEnabledFor(logging.DEBUG): logger.exception(e) - raise ValueError("no metadata file descriptor in crate") - return True + return False From b9fe5725f71c080631870c5b93615a5d76ec87bb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 26 Jul 2024 16:12:39 +0200 Subject: [PATCH 711/902] fix(profile/workflow-ro-crate): :bug: fix missing issue on `MainWorkflowFileExistence` --- .../profiles/workflow-ro-crate/must/0_main_workflow.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 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 cfedb625..3ea472f2 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 @@ -17,13 +17,15 @@ def check_workflow(self, context: ValidationContext) -> bool: try: main_workflow = context.ro_crate.metadata.get_main_workflow() if not main_workflow: - context.result.add_error(f"main workflow does not exist in metadata file", self) + context.result.add_check_issue("main workflow does not exist in metadata file", self) return False if not main_workflow.is_available(): - context.result.add_error(f"Main Workflow {main_workflow.id} not found in crate", self) + context.result.add_check_issue("Main Workflow {main_workflow.id} not found in crate", self) return False return True except ValueError as e: + context.result.add_check_issue("Unable to check the existence of the main workflow file " + "because the metadata file descriptor doesn't contain a `mainEntity`", self) if logger.isEnabledFor(logging.DEBUG): logger.exception(e) return False From 1bdefacf9e5e74532738faf5518389acd95228bc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 26 Jul 2024 16:54:15 +0200 Subject: [PATCH 712/902] refactor(profile/workflow-ro-crate): :recycle: hide the shape that only expand the data graph --- .../profiles/workflow-ro-crate/must/0_main-workflow.ttl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 3fca3eb0..bc8fa5aa 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 @@ -9,9 +9,10 @@ @prefix bioschemas: . -ro:FindMainWorkflow a sh:NodeShape ; + +ro:FindMainWorkflow a sh:NodeShape, sh:hidden ; sh:name "Identify Main Workflow" ; - sh:description "Main Workflow" ; + sh:description "Identify the Main Workflow" ; sh:target [ a sh:SPARQLTarget ; sh:prefixes ro:sparqlPrefixes ; From 2a3e6227b920bbec7ab34b321e576af0262ccd1b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 26 Jul 2024 16:55:20 +0200 Subject: [PATCH 713/902] feat(profiles/workflow-ro-crate): :sparkles: add shape to validate the existence of the `mainEntity` property --- .../workflow-ro-crate/must/0_main-workflow.ttl | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 bc8fa5aa..af458063 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 @@ -9,6 +9,18 @@ @prefix bioschemas: . +ro:MainEntityProperyMustExist 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 rocrate: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" ; + ] . + ro:FindMainWorkflow a sh:NodeShape, sh:hidden ; sh:name "Identify Main Workflow" ; From 9e1561ea70840eb1a90cf99cfbef96848ca88767 Mon Sep 17 00:00:00 2001 From: simleo Date: Fri, 26 Jul 2024 14:44:38 +0200 Subject: [PATCH 714/902] workflow run crate: first stub --- .../profiles/workflow-run-crate/profile.ttl | 71 +++++ .../Galaxy-Workflow-Hello_World.ga | 157 ++++++++++ .../workflow-run-crate/ro-crate-metadata.json | 270 ++++++++++++++++++ 3 files changed, 498 insertions(+) create mode 100644 rocrate_validator/profiles/workflow-run-crate/profile.ttl create mode 100644 tests/data/crates/valid/workflow-run-crate/Galaxy-Workflow-Hello_World.ga create mode 100644 tests/data/crates/valid/workflow-run-crate/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/workflow-run-crate/profile.ttl b/rocrate_validator/profiles/workflow-run-crate/profile.ttl new file mode 100644 index 00000000..addcc275 --- /dev/null +++ b/rocrate_validator/profiles/workflow-run-crate/profile.ttl @@ -0,0 +1,71 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Workflow Run Crate 0.5" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """RO-Crate Metadata Specification."""@en ; + + # URI of the publisher of the Metadata Specification + dct:publisher ; + + # This profile is an extension of Process Run Crate and Workflow RO-Crate + prof:isProfileOf , + ; + + # Explicitly state that this profile is a transitive profile of the RO-Crate Metadata Specification 1.1 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 refered 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 refered 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 Process Run Crate and Workflow RO-Crate + prof:isInheritedFrom , + ; + ] ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "workflow-run-crate" ; +. diff --git a/tests/data/crates/valid/workflow-run-crate/Galaxy-Workflow-Hello_World.ga b/tests/data/crates/valid/workflow-run-crate/Galaxy-Workflow-Hello_World.ga new file mode 100644 index 00000000..8bd9b1ba --- /dev/null +++ b/tests/data/crates/valid/workflow-run-crate/Galaxy-Workflow-Hello_World.ga @@ -0,0 +1,157 @@ +{ + "a_galaxy_workflow": "true", + "annotation": "From https://training.galaxyproject.org/training-material/topics/galaxy-interface/tutorials/workflow-editor/tutorial.html#creating-a-new-workflow", + "creator": [ + { + "class": "Person", + "identifier": "https://orcid.org/0000-0001-9842-9718", + "name": "Stian Soiland-Reyes" + } + ], + "format-version": "0.1", + "license": "CC0-1.0", + "name": "Hello World", + "steps": { + "0": { + "annotation": "A simple set of lines in a text file", + "content_id": null, + "errors": null, + "id": 0, + "input_connections": {}, + "inputs": [ + { + "description": "A simple set of lines in a text file", + "name": "simple_input" + } + ], + "label": "simple_input", + "name": "Input dataset", + "outputs": [], + "position": { + "bottom": 519.227779812283, + "height": 55.616668701171875, + "left": 626.0000271267361, + "right": 806.0000271267361, + "top": 463.6111111111111, + "width": 180, + "x": 626.0000271267361, + "y": 463.6111111111111 + }, + "tool_id": null, + "tool_state": "{\"optional\": false}", + "tool_version": null, + "type": "data_input", + "uuid": "75e4b93c-1b01-4332-8e2d-974bc03870b2", + "workflow_outputs": [] + }, + "1": { + "annotation": "Return all the lines of a text file reversed, last to first", + "content_id": "toolshed.g2.bx.psu.edu/repos/bgruening/text_processing/tp_tac/1.1.0", + "errors": null, + "id": 1, + "input_connections": { + "infile": { + "id": 0, + "output_name": "output" + } + }, + "inputs": [ + { + "description": "runtime parameter for tool tac", + "name": "infile" + } + ], + "label": "Reverse dataset", + "name": "tac", + "outputs": [ + { + "name": "outfile", + "type": "input" + } + ], + "position": { + "bottom": 669.8444400363499, + "height": 102.23332214355469, + "left": 883.9999728732639, + "right": 1063.999972873264, + "top": 567.6111178927952, + "width": 180, + "x": 883.9999728732639, + "y": 567.6111178927952 + }, + "post_job_actions": {}, + "tool_id": "toolshed.g2.bx.psu.edu/repos/bgruening/text_processing/tp_tac/1.1.0", + "tool_shed_repository": { + "changeset_revision": "ddf54b12c295", + "name": "text_processing", + "owner": "bgruening", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"infile\": {\"__class__\": \"RuntimeValue\"}, \"separator\": {\"separator_select\": \"no\", \"__current_case__\": 0}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.1.0", + "type": "tool", + "uuid": "1e2bcc37-edad-4d9d-9ae8-a27e183ee55a", + "workflow_outputs": [ + { + "label": "reversed", + "output_name": "outfile", + "uuid": "bb56259b-0460-4187-a4a1-2b7b3a868d6d" + } + ] + }, + "2": { + "annotation": "The last lines of workflow input are the first lines of the reversed input.", + "content_id": "Show beginning1", + "errors": null, + "id": 2, + "input_connections": { + "input": { + "id": 1, + "output_name": "outfile" + } + }, + "inputs": [ + { + "description": "runtime parameter for tool Select first", + "name": "input" + } + ], + "label": "Select last lines", + "name": "Select first", + "outputs": [ + { + "name": "out_file1", + "type": "input" + } + ], + "position": { + "bottom": 819.8444061279297, + "height": 102.23332214355469, + "left": 1168.999972873264, + "right": 1348.999972873264, + "top": 717.611083984375, + "width": 180, + "x": 1168.999972873264, + "y": 717.611083984375 + }, + "post_job_actions": {}, + "tool_id": "Show beginning1", + "tool_state": "{\"header\": \"false\", \"input\": {\"__class__\": \"RuntimeValue\"}, \"lineNum\": \"2\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.0.1", + "type": "tool", + "uuid": "b378a19a-2126-4302-aace-c3311b7ef64e", + "workflow_outputs": [ + { + "label": "last_lines", + "output_name": "out_file1", + "uuid": "8fe82179-555b-4ace-ad8b-ab3a6587aea8" + } + ] + } + }, + "tags": [ + "example" + ], + "uuid": "576ba0e9-b112-47f0-845e-32d8af3a1f35", + "version": 3 +} \ No newline at end of file diff --git a/tests/data/crates/valid/workflow-run-crate/ro-crate-metadata.json b/tests/data/crates/valid/workflow-run-crate/ro-crate-metadata.json new file mode 100644 index 00000000..7583a63e --- /dev/null +++ b/tests/data/crates/valid/workflow-run-crate/ro-crate-metadata.json @@ -0,0 +1,270 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/workflow-run/context" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/wfrun/process/0.1" + }, + { + "@id": "https://w3id.org/ro/wfrun/workflow/0.1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0" + } + ], + "hasPart": [ + { + "@id": "Galaxy-Workflow-Hello_World.ga" + }, + { + "@id": "inputs/abcdef.txt" + }, + { + "@id": "outputs/Select_first_on_data_1_2.txt" + }, + { + "@id": "outputs/tac_on_data_360_1.txt" + } + ], + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mainEntity": { + "@id": "Galaxy-Workflow-Hello_World.ga" + }, + "mentions": { + "@id": "#wfrun-5a5970ab-4375-444d-9a87-a764a66e3a47" + } + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.1", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.1" + }, + { + "@id": "https://w3id.org/ro/wfrun/workflow/0.1", + "@type": "CreativeWork", + "name": "Workflow Run Crate", + "version": "0.1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.0", + "@type": "CreativeWork", + "name": "Workflow RO-Crate", + "version": "1.0" + }, + { + "@id": "Galaxy-Workflow-Hello_World.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "name": "Hello World (Galaxy Workflow)", + "author": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "creator": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "input": [ + { + "@id": "#simple_input" + }, + { + "@id": "#verbose-param" + } + ], + "output": [ + { + "@id": "#reversed" + }, + { + "@id": "#last_lines" + } + ] + }, + { + "@id": "#simple_input", + "@type": "FormalParameter", + "additionalType": "File", + "conformsTo": { + "@id": "https://bioschemas.org/profiles/FormalParameter/1.0-RELEASE" + }, + "description": "A simple set of lines in a text file", + "encodingFormat": [ + "text/plain", + { + "@id": "http://edamontology.org/format_2330" + } + ], + "workExample": { + "@id": "inputs/abcdef.txt" + }, + "name": "simple_input", + "valueRequired": "True" + }, + { + "@id": "#verbose-param", + "@type": "FormalParameter", + "additionalType": "Boolean", + "conformsTo": { + "@id": "https://bioschemas.org/profiles/FormalParameter/1.0-RELEASE" + }, + "description": "Increase logging output", + "workExample": { + "@id": "#verbose-pv" + }, + "name": "verbose", + "valueRequired": "False" + }, + { + "@id": "#reversed", + "@type": "FormalParameter", + "additionalType": "File", + "conformsTo": { + "@id": "https://bioschemas.org/profiles/FormalParameter/1.0-RELEASE" + }, + "description": "All the lines, reversed", + "encodingFormat": [ + "text/plain", + { + "@id": "http://edamontology.org/format_2330" + } + ], + "name": "reversed", + "workExample": { + "@id": "outputs/tac_on_data_360_1.txt" + } + }, + { + "@id": "#last_lines", + "@type": "FormalParameter", + "additionalType": "File", + "conformsTo": { + "@id": "https://bioschemas.org/profiles/FormalParameter/1.0-RELEASE" + }, + "description": "The last lines of workflow input are the first lines of the reversed input", + "encodingFormat": [ + "text/plain", + { + "@id": "http://edamontology.org/format_2330" + } + ], + "name": "last_lines", + "workExample": { + "@id": "outputs/Select_first_on_data_1_2.txt" + } + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718", + "@type": "Person", + "name": "Stian Soiland-Reyes" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": "https://galaxyproject.org/", + "name": "Galaxy", + "url": "https://galaxyproject.org/" + }, + { + "@id": "#wfrun-5a5970ab-4375-444d-9a87-a764a66e3a47", + "@type": "CreateAction", + "name": "Galaxy workflow run 5a5970ab-4375-444d-9a87-a764a66e3a47", + "endTime": "2018-09-19T17:01:07+10:00", + "instrument": { + "@id": "Galaxy-Workflow-Hello_World.ga" + }, + "subjectOf": { + "@id": "https://usegalaxy.eu/u/5dbf7f05329e49c98b31243b5f35045c/p/invocation-report-a3a1d27edb703e5c" + }, + "object": [ + { + "@id": "inputs/abcdef.txt" + }, + { + "@id": "#verbose-pv" + } + ], + "result": [ + { + "@id": "outputs/Select_first_on_data_1_2.txt" + }, + { + "@id": "outputs/tac_on_data_360_1.txt" + } + ] + }, + { + "@id": "inputs/abcdef.txt", + "@type": "File", + "description": "Example input, a simple text file", + "encodingFormat": "text/plain", + "exampleOfWork": { + "@id": "#simple_input" + } + }, + { + "@id": "#verbose-pv", + "@type": "PropertyValue", + "exampleOfWork": { + "@id": "#verbose-param" + }, + "name": "verbose", + "value": "True" + }, + { + "@id": "outputs/Select_first_on_data_1_2.txt", + "@type": "File", + "name": "Select_first_on_data_1_2 (output)", + "description": "Example output of the last (aka first of reversed) lines", + "encodingFormat": "text/plain", + "exampleOfWork": { + "@id": "#last_lines" + } + }, + { + "@id": "outputs/tac_on_data_360_1.txt", + "@type": "File", + "name": "tac_on_data_360_1 (output)", + "description": "Example output of the reversed lines", + "encodingFormat": "text/plain", + "exampleOfWork": { + "@id": "#reversed" + } + }, + { + "@id": "https://usegalaxy.eu/u/5dbf7f05329e49c98b31243b5f35045c/p/invocation-report-a3a1d27edb703e5c", + "@type": "CreativeWork", + "encodingFormat": "text/html", + "datePublished": "2021-11-18T02:02:00Z", + "name": "Workflow Execution Summary of Hello World" + } + ] +} From edc360dab8973421cd22e81116240efc32da626f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 10:37:29 +0200 Subject: [PATCH 715/902] refactor(core): :fire: remove unnecessary commit --- rocrate_validator/rocrate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rocrate_validator/rocrate.py b/rocrate_validator/rocrate.py index fd45788e..950d5e56 100644 --- a/rocrate_validator/rocrate.py +++ b/rocrate_validator/rocrate.py @@ -205,7 +205,6 @@ def get_conforms_to(self) -> Optional[list[str]]: except Exception as e: if logger.isEnabledFor(logging.DEBUG): logger.exception(e) - logger.exception(e) # TODO: remove return None def as_json(self) -> str: From 82dd3037897c8702dddaa0eabd9d306a7ce8f058 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 10:38:51 +0200 Subject: [PATCH 716/902] feat(core): :sparkles: enhance property getter to support list on entities --- rocrate_validator/rocrate.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/rocrate.py b/rocrate_validator/rocrate.py index 950d5e56..6ac705e7 100644 --- a/rocrate_validator/rocrate.py +++ b/rocrate_validator/rocrate.py @@ -55,8 +55,7 @@ def has_types(self, entity_types: list[str]) -> bool: e_types = self.type if isinstance(self.type, list) else [self.type] return any([t in e_types for t in entity_types]) - def get_property(self, name: str, default=None) -> Union[str, ROCrateEntity]: - data = self._raw_data.get(name, default) + def __process_property__(self, name: str, data: object) -> object: if isinstance(data, dict) and '@id' in data: entity = self.metadata.get_entity(data['@id']) if entity is None: @@ -64,6 +63,14 @@ def get_property(self, name: str, default=None) -> Union[str, ROCrateEntity]: return entity return data + def get_property(self, name: str, default=None) -> Union[str, ROCrateEntity]: + data = self._raw_data.get(name, default) + if data is None: + return None + if isinstance(data, list): + return [self.__process_property__(name, _) for _ in data] + return self.__process_property__(name, data) + @property def raw_data(self) -> object: return self._raw_data From 959953d2b0e4610e7d8f4d9bcd3e9f58ee8b5ffe Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 10:42:00 +0200 Subject: [PATCH 717/902] fix(core): :bug: safely delete candidate profiles --- rocrate_validator/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 29e03230..f8c51cb1 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1264,7 +1264,8 @@ def detect_rocrate_profiles(self) -> list[Profile]: candidate_profiles.append(profile) inherited_profiles = profile.inherited_profiles for inherited_profile in inherited_profiles: - candidate_profiles.remove(inherited_profile) + if inherited_profile in candidate_profiles: + candidate_profiles.remove(inherited_profile) logger.debug("%d Candidate Profiles found: %s", len(candidate_profiles), candidate_profiles) return candidate_profiles From 2faf9dfa7aa645cb3800e503386315f74db8c611 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 10:44:14 +0200 Subject: [PATCH 718/902] refactor(core): :loud_sound: update log --- 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 6a386b90..dfb10abd 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -359,7 +359,7 @@ def progress(self) -> Progress: def update(self, event: Event): # logger.debug("Event: %s", event.event_type) if event.event_type == EventType.PROFILE_VALIDATION_START: - logger.debug("Profile validation start") + logger.debug("Profile validation start: %s", event.profile.identifier) elif event.event_type == EventType.REQUIREMENT_VALIDATION_START: logger.debug("Requirement validation start") elif event.event_type == EventType.REQUIREMENT_CHECK_VALIDATION_START: From 2ab615c3ffd6040a5f52b07c6a7141ab81d6638a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 10:47:39 +0200 Subject: [PATCH 719/902] fix(shacl): :bug: notify that previously skipped checks have been executed --- .../requirements/shacl/checks.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 7c2b50ff..358f7c68 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -172,17 +172,20 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): logger.debug("Added validation issue to the context: %s", c) # As above, but for skipped checks which are not failed - if not shacl_context.fail_fast: - for requirementCheck in list(shacl_context.result.skipped_checks): - if not isinstance(requirementCheck, SHACLCheck): - continue - if requirementCheck.requirement.profile != shacl_context.current_validation_profile and \ - not requirementCheck in failed_requirements_checks and \ - not requirementCheck.identifier in failed_requirement_checks_notified: - failed_requirement_checks_notified.append(requirementCheck.identifier) - shacl_context.result.add_executed_check(requirementCheck, True) - shacl_context.validator.notify(RequirementCheckValidationEvent( - EventType.REQUIREMENT_CHECK_VALIDATION_END, requirementCheck, validation_result=True)) + for requirementCheck in list(shacl_context.result.skipped_checks): + logger.debug("Processing skipped check: %s", requirementCheck.identifier) + if not isinstance(requirementCheck, SHACLCheck): + continue + if requirementCheck.requirement.profile != shacl_context.current_validation_profile and \ + not requirementCheck in failed_requirements_checks and \ + not requirementCheck.identifier in failed_requirement_checks_notified: + failed_requirement_checks_notified.append(requirementCheck.identifier) + shacl_context.result.add_executed_check(requirementCheck, True) + shacl_context.validator.notify(RequirementCheckValidationEvent( + EventType.REQUIREMENT_CHECK_VALIDATION_END, requirementCheck, validation_result=True)) + logger.debug("Added skipped check to the context: %s", requirementCheck.identifier) + + logger.debug("Remaining skipped checks: %s", shacl_context.result.skipped_checks) end_time = timer() logger.debug(f"Execution time for parsing the validation result: {end_time - start_time} seconds") From 81ecc3796782bcaa0b7fe93244c2854e8a8fe1dc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 10:51:24 +0200 Subject: [PATCH 720/902] refactor(shacl): :art: ensure that skipped checks are tracked --- rocrate_validator/requirements/shacl/checks.py | 2 -- rocrate_validator/requirements/shacl/validator.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 358f7c68..997768b1 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -64,8 +64,6 @@ def execute_check(self, context: ValidationContext): logger.debug("SHACL Validation of profile %s requirement %s skipped", self.requirement.profile, self) # The validation is postponed to the more specific profiles # so the check is not considered as failed. - # We assume that the main algorithm catches the issue - # and the check is marked as skipped withing the context.result raise SkipRequirementCheck(self, str(e)) except ROCrateMetadataNotFoundError as e: logger.debug("Unable to perform metadata validation due to missing metadata file: %s", e) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 19b013ca..36343375 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -53,6 +53,7 @@ def __enter__(self) -> SHACLValidationContext: if self._context.settings.get("target_only_validation", False) and \ self._profile.identifier != self._context.settings.get("profile_identifier", None): logger.debug("Skipping validation of profile %s", self._profile.identifier) + self.context.result.add_skipped_check(self._check) 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 39e84cc5a358932057ee9e5db1b5654f182a567c Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 29 Jul 2024 12:18:13 +0200 Subject: [PATCH 721/902] add test for wroc with no mainEntity --- .../no_mainentity/ro-crate-metadata.json | 134 ++++++++++++++++++ .../test_wroc_root_metadata.py | 16 ++- tests/ro_crates.py | 9 ++ 3 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 tests/data/crates/invalid/1_wroc_crate/no_mainentity/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/1_wroc_crate/no_mainentity/ro-crate-metadata.json b/tests/data/crates/invalid/1_wroc_crate/no_mainentity/ro-crate-metadata.json new file mode 100644 index 00000000..6169df3a --- /dev/null +++ b/tests/data/crates/invalid/1_wroc_crate/no_mainentity/ro-crate-metadata.json @@ -0,0 +1,134 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "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://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": "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" + } + ] +} \ No newline at end of file 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 2688f664..dbe94212 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 @@ -1,7 +1,7 @@ import logging from rocrate_validator.models import Severity -from tests.ro_crates import WROCNoLicense +from tests.ro_crates import WROCNoLicense, WROCMainEntity from tests.shared import do_entity_test logger = logging.getLogger(__name__) @@ -19,3 +19,17 @@ def test_wroc_no_license(): ["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" ) + + +def test_wroc_no_mainentity(): + """\ + Test a Workflow RO-Crate where the root data entity has no mainEntity. + """ + do_entity_test( + WROCMainEntity().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" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 5db66576..a70d4ddb 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -265,6 +265,15 @@ def wroc_no_license(self) -> Path: return self.base_path / "no_license" +class WROCMainEntity: + + base_path = INVALID_CRATES_DATA_PATH / "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 877a0e64a8bba4ca9e3a2895a3b801c62d9df2bf Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 12:59:51 +0200 Subject: [PATCH 722/902] feat(utils): :sparkles: add class to configure the CLI pager --- rocrate_validator/cli/utils.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/utils.py b/rocrate_validator/cli/utils.py index 856553da..b9567bcf 100644 --- a/rocrate_validator/cli/utils.py +++ b/rocrate_validator/cli/utils.py @@ -1,9 +1,11 @@ import os +import pydoc import re import textwrap -from typing import Optional +from typing import Any, Optional from rich.padding import Padding +from rich.pager import Pager from rich.rule import Rule from rich.text import Text @@ -35,3 +37,14 @@ def format_text(text: str, def get_app_header_rule() -> Text: return Padding(Rule(f"\n[bold][cyan]ROCrate Validator[/cyan] (ver. [magenta]{get_version()}[/magenta])[/bold]", style="bold cyan"), (1, 2)) + + +class SystemPager(Pager): + """Uses the pager installed on the system.""" + + def _pager(self, content: str) -> Any: + return pydoc.pipepager(content, "less -R") + + def show(self, content: str) -> None: + """Use the same pager used by pydoc.""" + self._pager(content) From 193f76dd58df86801ca6ba1ef34036a0c6fb5da5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 13:02:06 +0200 Subject: [PATCH 723/902] feat(cli): :sparkles: initialise the CLI pager --- rocrate_validator/cli/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index e518725b..dea62015 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -5,6 +5,7 @@ from rich.console import Console import rocrate_validator.log as logging +from rocrate_validator.cli.utils import SystemPager from rocrate_validator.utils import get_version # set up logging @@ -37,9 +38,11 @@ @click.pass_context def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool): ctx.ensure_object(dict) - console = Console(no_color=disable_color) + console = Console(no_color=disable_color, force_terminal=True) # pass the console to subcommands through the click context, after configuration ctx.obj['console'] = console + ctx.obj['pager'] = SystemPager() + try: # If the version flag is set, print the version and exit if version: From f1f9dde0efb61ac92d9686fbc33dc86ea522088c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 13:06:30 +0200 Subject: [PATCH 724/902] feat(cli): :sparkles: configure pager on subcommands --- rocrate_validator/cli/commands/profiles.py | 7 +++++-- rocrate_validator/cli/commands/validate.py | 14 ++++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 84a29ce5..40fc7896 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -55,6 +55,7 @@ def list_profiles(ctx, no_paging: bool = False): # , profiles_path: Path = DEFA """ profiles_path = ctx.obj['profiles_path'] console = ctx.obj['console'] + pager = ctx.obj['pager'] enable_pager = not no_paging try: @@ -102,7 +103,7 @@ def list_profiles(ctx, no_paging: bool = False): # , profiles_path: Path = DEFA table.add_row() # Print the table - with console.pager(styles=True) if enable_pager else console: + 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))) @@ -137,6 +138,8 @@ def describe_profile(ctx, """ # Get the console console = ctx.obj['console'] + pager = ctx.obj['pager'] + # Get the no_paging flag enable_pager = not no_paging @@ -158,7 +161,7 @@ def describe_profile(ctx, else: table = __verbose_describe_profile__(profile) - with console.pager(styles=True) if enable_pager else console: + with console.pager(pager=pager, styles=not console.no_color) if enable_pager else console: console.print(get_app_header_rule()) console.print(Padding(Panel(subheader_content, title=subheader_title, padding=(1, 1), title_align="left", border_style="cyan"), (0, 1, 0, 1))) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index dfb10abd..483c2208 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -13,6 +13,7 @@ from rich.live import Live from rich.markdown import Markdown from rich.padding import Padding +from rich.pager import Pager from rich.panel import Panel from rich.progress import BarColumn, Progress, TextColumn, TimeElapsedColumn from rich.prompt import Prompt @@ -183,6 +184,7 @@ def validate(ctx, [magenta]rocrate-validator:[/magenta] Validate a RO-Crate against a profile """ console: Console = ctx.obj['console'] + pager = ctx.obj['pager'] # Get the no_paging flag enable_pager = not no_paging # Log the input parameters for debugging @@ -266,8 +268,8 @@ def validate(ctx, if not details and enable_pager: details = get_single_char(console, choices=['y', 'n'], message="[bold] > Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]") - if details == "y": - report_layout.show_validation_details(enable_pager=enable_pager) + if details == "y" or details: + report_layout.show_validation_details(pager, enable_pager=enable_pager) if output_file: # Print the validation report to a file @@ -580,7 +582,7 @@ def set_overall_result(self, result: ValidationResult): Padding(Rule(f"[bold][[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", style="bold red"), (1, 1))) - def show_validation_details(self, enable_pager: bool = True): + def show_validation_details(self, pager: Pager, enable_pager: bool = True): """ Print the validation result """ @@ -591,8 +593,12 @@ def show_validation_details(self, enable_pager: bool = True): console = self.console result = self.result + console.print("[purple]Validation Report[/purple]", style="bold purple") + + logger.error("Validation failed: %s", result.failed_requirements) + # Print validation details - with console.pager(styles=True) if enable_pager else console: + with console.pager(pager=pager, styles=not console.no_color) if enable_pager else console: # Print the list of failed requirements console.print( Padding("\n[bold]The following requirements have not meet: [/bold]", (0, 0)), style="white") From b5a7d8516c25dedac9dd9314f94e0ebc614ffb0f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 14:58:57 +0200 Subject: [PATCH 725/902] fix(cli): :fire: clean up --- rocrate_validator/cli/commands/validate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 483c2208..eaeb1dda 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -593,8 +593,6 @@ def show_validation_details(self, pager: Pager, enable_pager: bool = True): console = self.console result = self.result - console.print("[purple]Validation Report[/purple]", style="bold purple") - logger.error("Validation failed: %s", result.failed_requirements) # Print validation details From 1fc2361a064383ca0adf68aaa22ae2123042517b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 15:20:27 +0200 Subject: [PATCH 726/902] fix(cli): :ambulance: missing positional argument --- 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 eaeb1dda..7f4dc73b 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -281,7 +281,7 @@ def validate(ctx, c = Console(file=f, color_system=None) c.print(report_layout.layout) report_layout.console = c - report_layout.show_validation_details(enable_pager=False) + report_layout.show_validation_details(pager, enable_pager=False) # using ctx.exit seems to raise an Exception that gets caught below, # so we use sys.exit instead. From 995588df651d457feb792161d62f04ce8e55a7d6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 19:59:05 +0200 Subject: [PATCH 727/902] fix(utils): :loud_sound: fix and extend debug logs --- rocrate_validator/cli/commands/validate.py | 2 +- rocrate_validator/requirements/shacl/checks.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 7f4dc73b..8d0c464c 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -593,7 +593,7 @@ def show_validation_details(self, pager: Pager, enable_pager: bool = True): console = self.console result = self.result - logger.error("Validation failed: %s", result.failed_requirements) + logger.debug("Validation failed: %s", result.failed_requirements) # Print validation details with console.pager(pager=pager, styles=not console.no_color) if enable_pager else console: diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 997768b1..d21bdb52 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -112,6 +112,7 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): # parse the validation result end_time = timer() logger.debug("Validation '%s' conforms: %s", self.name, shacl_result.conforms) + logger.debug("Number of violations: %s", len(shacl_result.violations)) logger.debug(f"Execution time for validating the data graph: {end_time - start_time} seconds") # store the validation result in the context @@ -170,9 +171,11 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): logger.debug("Added validation issue to the context: %s", c) # As above, but for skipped checks which are not failed + logger.debug("Skipped checks: %s", len(shacl_context.result.skipped_checks)) for requirementCheck in list(shacl_context.result.skipped_checks): logger.debug("Processing skipped check: %s", requirementCheck.identifier) if not isinstance(requirementCheck, SHACLCheck): + logger.debug("Skipped check is not a SHACLCheck: %s", requirementCheck.identifier) continue if requirementCheck.requirement.profile != shacl_context.current_validation_profile and \ not requirementCheck in failed_requirements_checks and \ @@ -183,8 +186,7 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): EventType.REQUIREMENT_CHECK_VALIDATION_END, requirementCheck, validation_result=True)) logger.debug("Added skipped check to the context: %s", requirementCheck.identifier) - logger.debug("Remaining skipped checks: %s", shacl_context.result.skipped_checks) - + logger.debug("Remaining skipped checks: %r", len(shacl_context.result.skipped_checks)) end_time = timer() logger.debug(f"Execution time for parsing the validation result: {end_time - start_time} seconds") From e0b4c723bb0144e497dde54cc27bffe66eba7285 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 20:02:09 +0200 Subject: [PATCH 728/902] fix(core): :ambulance: better identification of requirements and checks --- rocrate_validator/cli/commands/profiles.py | 2 +- rocrate_validator/cli/commands/validate.py | 2 +- rocrate_validator/models.py | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 40fc7896..77db6af4 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -239,7 +239,7 @@ def __verbose_describe_profile__(profile): levels_list.add(level_info) logger.debug("Check %s: %s", check.name, check.description) # checks.append(check) - table_rows.append((str(check.identifier).rjust(14), check.name, + table_rows.append((str(check.relative_identifier).rjust(14), check.name, Markdown(check.description.strip()), level_info)) count_checks += 1 diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 8d0c464c..6675ec3c 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -619,7 +619,7 @@ def show_validation_details(self, pager: Pager, enable_pager: bool = True): key=lambda x: (-x.severity.value, x)): issue_color = get_severity_color(check.level.severity) console.print( - Padding(f"[bold][{issue_color}][{check.identifier.center(16)}][/{issue_color}] [magenta]{check.name}[/magenta][/bold]:", (0, 7)), style="white bold") + Padding(f"[bold][{issue_color}][{check.relative_identifier.center(16)}][/{issue_color}] [magenta]{check.name}[/magenta][/bold]:", (0, 7)), style="white bold") console.print(Padding(Markdown(check.description), (0, 27))) console.print(Padding("[u] Detected issues [/u]", (0, 8)), style="white bold") for issue in sorted(result.get_issues_by_check(check), diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index f8c51cb1..f87e6aa6 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -503,6 +503,10 @@ def order_number(self) -> int: @property def identifier(self) -> str: + return f"{self.profile.identifier}.{self.relative_identifier}" + + @property + def relative_identifier(self) -> str: return f"{self.level.name} {self.order_number}" @property @@ -737,6 +741,10 @@ def order_number(self, value: int) -> None: def identifier(self) -> str: return f"{self.requirement.identifier}.{self.order_number}" + @property + def relative_identifier(self) -> str: + return f"{self.requirement.relative_identifier}.{self.order_number}" + @property def name(self) -> str: if not self._name: From a0191cec94d026fe7c822b450a0ad9df3a70119b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 20:42:59 +0200 Subject: [PATCH 729/902] fix(cli): :bug: details will be reported on file only if validation fails --- rocrate_validator/cli/commands/validate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 6675ec3c..e3e3fb8a 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -281,7 +281,8 @@ def validate(ctx, c = Console(file=f, color_system=None) c.print(report_layout.layout) report_layout.console = c - report_layout.show_validation_details(pager, enable_pager=False) + if not result.passed(): + report_layout.show_validation_details(None, enable_pager=False) # using ctx.exit seems to raise an Exception that gets caught below, # so we use sys.exit instead. From cc30657f0de2c3170056209d0bdb4fe34fa41ec3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 20:46:45 +0200 Subject: [PATCH 730/902] fix(cli): :bug: resolve the duplicate variable issue --- rocrate_validator/cli/commands/validate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index e3e3fb8a..149ef762 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -266,9 +266,9 @@ def validate(ctx, # Print the validation result if not result.passed(): if not details and enable_pager: - details = get_single_char(console, choices=['y', 'n'], - message="[bold] > Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]") - if details == "y" or details: + details_choice = get_single_char(console, choices=['y', 'n'], + message="[bold] > Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]") + if details_choice == "y" or details: report_layout.show_validation_details(pager, enable_pager=enable_pager) if output_file: From faca0c3860cd8561f8f3577ef42568bc77ea529e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 20:49:47 +0200 Subject: [PATCH 731/902] feat(cli): :lipstick: better configuration of the file output layout --- 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 149ef762..809f9a09 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -278,7 +278,7 @@ def validate(ctx, f.write(result.to_json()) elif format == "text": with open(output_file, "w") as f: - c = Console(file=f, color_system=None) + c = Console(file=f, color_system=None, width=120, height=31) c.print(report_layout.layout) report_layout.console = c if not result.passed(): From 78e1816a0bb5c17c4fc3505e65d34d9a3823fb21 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 20:50:27 +0200 Subject: [PATCH 732/902] fix(cli): :ambulance: fix main console configuration --- rocrate_validator/cli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index dea62015..aa6f40a1 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -38,7 +38,7 @@ @click.pass_context def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool): ctx.ensure_object(dict) - console = Console(no_color=disable_color, force_terminal=True) + console = Console(no_color=disable_color) # pass the console to subcommands through the click context, after configuration ctx.obj['console'] = console ctx.obj['pager'] = SystemPager() From 5ef2abaef1ed0018bbae1ec4358ea39a02b2e0d2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 29 Jul 2024 21:12:47 +0200 Subject: [PATCH 733/902] feat(cli): :sparkles: auto detection of no-interactive mode --- rocrate_validator/cli/commands/profiles.py | 10 +++++++++- rocrate_validator/cli/commands/validate.py | 7 ++++++- rocrate_validator/cli/main.py | 7 ++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 77db6af4..67f739b9 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -56,7 +56,12 @@ def list_profiles(ctx, no_paging: bool = False): # , profiles_path: Path = DEFA profiles_path = ctx.obj['profiles_path'] console = ctx.obj['console'] pager = ctx.obj['pager'] + interactive = ctx.obj['interactive'] + # Get the no_paging flag enable_pager = not no_paging + # override the enable_pager flag if the interactive flag is False + if not interactive: + enable_pager = False try: # Get the profiles @@ -139,9 +144,12 @@ def describe_profile(ctx, # Get the console console = ctx.obj['console'] pager = ctx.obj['pager'] - + interactive = ctx.obj['interactive'] # Get the no_paging flag enable_pager = not no_paging + # override the enable_pager flag if the interactive flag is False + if not interactive: + enable_pager = False try: # Get the profile diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 809f9a09..f6d56e10 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -185,8 +185,12 @@ def validate(ctx, """ console: Console = ctx.obj['console'] pager = ctx.obj['pager'] + interactive = ctx.obj['interactive'] # Get the no_paging flag enable_pager = not no_paging + # override the enable_pager flag if the interactive flag is False + if not interactive: + enable_pager = False # Log the input parameters for debugging logger.debug("profiles_path: %s", os.path.abspath(profiles_path)) logger.debug("profile_identifier: %s", profile_identifier) @@ -265,7 +269,8 @@ def validate(ctx, # Print the validation result if not result.passed(): - if not details and enable_pager: + details_choice = "n" + if interactive and not details and enable_pager: details_choice = get_single_char(console, choices=['y', 'n'], message="[bold] > Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]") if details_choice == "y" or details: diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index aa6f40a1..f9d10f73 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -38,10 +38,15 @@ @click.pass_context def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool): ctx.ensure_object(dict) - console = Console(no_color=disable_color) + + # determine if the console is interactive + interactive = sys.stdout.isatty() + + console = Console(no_color=disable_color or not interactive) # pass the console to subcommands through the click context, after configuration ctx.obj['console'] = console ctx.obj['pager'] = SystemPager() + ctx.obj['interactive'] = interactive try: # If the version flag is set, print the version and exit From c737e889338d97bcda162558602f41542030db0f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 30 Jul 2024 08:18:28 +0200 Subject: [PATCH 734/902] refactor(cli): :recycle: rename option `details` to `verbose` --- rocrate_validator/cli/commands/validate.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index f6d56e10..0ab866a5 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -137,7 +137,8 @@ def get_single_char(console: Optional[Console] = None, end: str = "\n", show_default=True ) @click.option( - '--details', + '-v', + '--verbose', is_flag=True, help="Output the validation details without prompting", default=False, @@ -177,7 +178,7 @@ def validate(ctx, no_fail_fast: bool = False, ontologies_path: Optional[Path] = None, no_paging: bool = False, - details: bool = False, + verbose: bool = False, format: str = "text", output_file: Optional[Path] = None): """ @@ -216,7 +217,7 @@ def validate(ctx, "requirement_severity_only": requirement_severity_only, "inherit_profiles": not disable_profile_inheritance, "show_details": False, - "details": details, + "details": verbose, "data_path": rocrate_uri, "ontology_path": Path(ontologies_path).absolute() if ontologies_path else None, "abort_on_first": not no_fail_fast @@ -269,11 +270,11 @@ def validate(ctx, # Print the validation result if not result.passed(): - details_choice = "n" - if interactive and not details and enable_pager: - details_choice = get_single_char(console, choices=['y', 'n'], + verbose_choice = "n" + if interactive and not verbose and enable_pager: + verbose_choice = get_single_char(console, choices=['y', 'n'], message="[bold] > Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]") - if details_choice == "y" or details: + if verbose_choice == "y" or verbose: report_layout.show_validation_details(pager, enable_pager=enable_pager) if output_file: From 1953d11fd7d5932658f585aa7b43ffb336834064 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 30 Jul 2024 08:29:55 +0200 Subject: [PATCH 735/902] test(cli): :white_check_mark: adopt renamed verbose option --- tests/test_cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 9a4630f9..0066b530 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -26,25 +26,25 @@ def test_version(cli_runner: CliRunner): def test_validate_subcmd_invalid_rocrate1(cli_runner: CliRunner): result = cli_runner.invoke(cli, ['validate', str( - InvalidFileDescriptor().invalid_json_format), '--details', '--no-paging', '-p', 'ro-crate']) + InvalidFileDescriptor().invalid_json_format), '--verbose', '--no-paging', '-p', 'ro-crate']) logger.error(result.output) assert result.exit_code == 1 def test_validate_subcmd_valid_local_folder_rocrate(cli_runner: CliRunner): - result = cli_runner.invoke(cli, ['validate', str(ValidROC().wrroc_paper_long_date), '--details', '--no-paging']) + result = cli_runner.invoke(cli, ['validate', str(ValidROC().wrroc_paper_long_date), '--verbose', '--no-paging']) assert result.exit_code == 0 assert re.search(r'RO-Crate.*is valid', result.output) def test_validate_subcmd_valid_remote_rocrate(cli_runner: CliRunner): result = cli_runner.invoke( - cli, ['validate', str(ValidROC().sort_and_change_remote), '--details', '--no-paging']) + cli, ['validate', str(ValidROC().sort_and_change_remote), '--verbose', '--no-paging']) assert result.exit_code == 0 assert re.search(r'RO-Crate.*is valid', result.output) def test_validate_subcmd_invalid_local_archive_rocrate(cli_runner: CliRunner): - result = cli_runner.invoke(cli, ['validate', str(ValidROC().sort_and_change_archive), '--details', '--no-paging']) + result = cli_runner.invoke(cli, ['validate', str(ValidROC().sort_and_change_archive), '--verbose', '--no-paging']) assert result.exit_code == 0 assert re.search(r'RO-Crate.*is valid', result.output) From 21067691062bca850c90036df6fa7d28d5bf46f3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 30 Jul 2024 08:30:25 +0200 Subject: [PATCH 736/902] feat(cli): :sparkles: add -y/--no-interactive option --- rocrate_validator/cli/main.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index f9d10f73..90b5bd09 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -29,6 +29,13 @@ help="Show the version of the rocrate-validator package", default=False ) +@click.option( + '-y', + '--no-interactive', + is_flag=True, + help="Disable interactive mode", + default=False +) @click.option( '--disable-color', is_flag=True, @@ -36,11 +43,11 @@ default=False ) @click.pass_context -def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool): +def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool, no_interactive: bool): ctx.ensure_object(dict) # determine if the console is interactive - interactive = sys.stdout.isatty() + interactive = sys.stdout.isatty() and not no_interactive console = Console(no_color=disable_color or not interactive) # pass the console to subcommands through the click context, after configuration From adbb39bd21709d9cc7558cf4b8c87f1c737fabcb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 30 Jul 2024 08:30:56 +0200 Subject: [PATCH 737/902] refactor(core): :art: reformat --- rocrate_validator/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index a144c229..197351b4 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -45,7 +45,7 @@ def validate(settings: Union[dict, ValidationSettings], def __initialise_validator__(settings: Union[dict, ValidationSettings], - subscribers: Optional[list[Subscriber]] = None) -> Validator: + subscribers: Optional[list[Subscriber]] = None) -> Validator: """ Validate a RO-Crate against a profile """ From 7a3fee48e84d8f4511b67d77500d9581e51b3077 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 30 Jul 2024 08:35:31 +0200 Subject: [PATCH 738/902] refactor(core): :sparkles: refactor the code to allow overriding checks --- rocrate_validator/cli/commands/validate.py | 2 +- rocrate_validator/models.py | 66 ++++++++++++---------- rocrate_validator/services.py | 16 ++++-- tests/unit/requirements/test_profiles.py | 12 ++-- 4 files changed, 54 insertions(+), 42 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 0ab866a5..e4f6bafe 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -684,7 +684,7 @@ def __compute_profile_stats__(validation_settings: dict): check_count_by_severity[severity] = 0 if severity_validation <= severity: num_checks = len( - requirement.get_checks_by_level(LevelCollection.get(severity.name))) + [_ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name)) if not _.overridden]) check_count_by_severity[severity] += num_checks total_checks += num_checks diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index f87e6aa6..a93922cf 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -409,7 +409,8 @@ def load(cls, profiles_base_path: str, @staticmethod def load_profiles(profiles_path: Union[str, Path], publicID: Optional[str] = None, - severity: Severity = Severity.REQUIRED) -> list[Profile]: + severity: Severity = Severity.REQUIRED, + allow_requirement_check_override: bool = True) -> list[Profile]: # if the path is a string, convert it to a Path if isinstance(profiles_path, str): profiles_path = Path(profiles_path) @@ -429,6 +430,34 @@ def load_profiles(profiles_path: Union[str, Path], if profile_path.is_dir() and profile_path not in IGNORED_PROFILE_DIRECTORIES: profile = Profile.load(profiles_path, profile_path, publicID=publicID, severity=severity) profiles.append(profile) + + # navigate the profiles and check for overridden checks + # if the override is enabled in the settings + # overridden checks should be marked as such + # otherwise, raise an error + profiles_checks = {} + # visit the profiles in reverse order + # (the order is important to visit the most specific profiles first) + for profile in sorted(profiles, reverse=True): + profile_checks = [_ for r in profile.get_requirements() for _ in r.get_checks()] + profile_check_names = [] + for check in profile_checks: + # ย find duplicated checks and raise an error + if check.name in profile_check_names and not allow_requirement_check_override: + raise DuplicateRequirementCheck(check.name, profile.identifier) + # ย add check to the list + profile_check_names.append(check.name) + # ย mark overridden checks + check_chain = profiles_checks.get(check.name, None) + if not check_chain: + profiles_checks[check.name] = [check] + elif allow_requirement_check_override: + check.overridden_by = check_chain[-1] + check_chain.append(check) + logger.debug("Overridden check: %s", check) + else: + raise DuplicateRequirementCheck(check.name, 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 # then the profiles that depend on the previous ones, and so on @@ -1120,7 +1149,7 @@ class ValidationSettings: profiles_path: Path = DEFAULT_PROFILES_PATH profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER inherit_profiles: bool = True - allow_shapes_override: bool = True + allow_requirement_check_override: bool = True disable_check_for_duplicates: bool = False # Ontology and inference settings ontology_path: Optional[Path] = None @@ -1447,8 +1476,8 @@ def profile_identifier(self) -> str: return self.settings.get("profile_identifier") @property - def allow_shapes_override(self) -> bool: - return self.settings.get("allow_shapes_override", True) + def allow_requirement_check_override(self) -> bool: + return self.settings.get("allow_requirement_check_override", True) @property def disable_check_for_duplicates(self) -> bool: @@ -1469,7 +1498,8 @@ def __load_profiles__(self) -> list[Profile]: profiles = Profile.load_profiles( self.profiles_path, publicID=self.publicID, - severity=self.requirement_severity) + severity=self.requirement_severity, + 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) @@ -1498,32 +1528,6 @@ def __load_profiles__(self) -> list[Profile]: if self.disable_check_for_duplicates: return profiles - # navigate the profiles and check for overridden checks - # if the override is enabled in the settings - # overridden checks should be marked as such - # otherwise, raise an error - profiles_checks = {} - # visit the profiles in reverse order - # (the order is important to visit the most specific profiles first) - for profile in sorted(profiles, reverse=True): - profile_checks = [_ for r in profile.get_requirements() for _ in r.get_checks()] - profile_check_names = [] - for check in profile_checks: - # ย find duplicated checks and raise an error - if check.name in profile_check_names and not self.allow_shapes_override: - raise DuplicateRequirementCheck(check.name, profile.identifier) - # ย add check to the list - profile_check_names.append(check.name) - # ย mark overridden checks - check_chain = profiles_checks.get(check.name, None) - if not check_chain: - profiles_checks[check.name] = [check] - elif self.settings.get("allow_shapes_override", True): - check.overridden_by = check_chain[-1] - check_chain.append(check) - else: - raise DuplicateRequirementCheck(check.name, profile.identifier) - return profiles @property diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 197351b4..90161f80 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -144,21 +144,29 @@ def __extract_and_validate_rocrate__(rocrate_path: Path): "It MUST be a local directory or a ZIP file (local or remote).") -def get_profiles(profiles_path: Path = DEFAULT_PROFILES_PATH, publicID: str = None, severity=Severity.OPTIONAL) -> list[Profile]: +def get_profiles(profiles_path: Path = DEFAULT_PROFILES_PATH, + publicID: str = None, + severity=Severity.OPTIONAL, + allow_requirement_check_override: bool = ValidationSettings.allow_requirement_check_override) -> list[Profile]: """ Load the profiles from the given path """ - profiles = Profile.load_profiles(profiles_path, publicID=publicID, severity=severity) + profiles = Profile.load_profiles(profiles_path, publicID=publicID, + severity=severity, + allow_requirement_check_override=allow_requirement_check_override) logger.debug("Profiles loaded: %s", profiles) return profiles def get_profile(profiles_path: Path = DEFAULT_PROFILES_PATH, - profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, publicID: str = None) -> Profile: + profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, + publicID: str = None, + allow_requirement_check_override: bool = ValidationSettings.allow_requirement_check_override) -> Profile: """ Load the profiles from the given path """ - profiles = get_profiles(profiles_path, publicID=publicID) + profiles = get_profiles(profiles_path, publicID=publicID, + allow_requirement_check_override=allow_requirement_check_override) 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) if not profile: diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 746f78b8..a77a0872 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -212,12 +212,12 @@ def test_load_invalid_profile_no_override_enabled(fake_profiles_path: str): "profile_identifier": "invalid-duplicated-shapes", "data_path": ValidROC().wrroc_paper, "inherit_profiles": True, - "allow_shapes_override": False, + "allow_requirement_check_override": False, } settings = ValidationSettings(**settings) assert settings.inherit_profiles, "The inheritance mode should be set to True" - assert not settings.allow_shapes_override, "The override mode should be set to False" + assert not settings.allow_requirement_check_override, "The override mode should be set to False" validator = Validator(settings) # initialize the validation context @@ -236,12 +236,12 @@ def test_load_invalid_profile_with_override_on_same_profile(fake_profiles_path: "profile_identifier": "invalid-duplicated-shapes", "data_path": ValidROC().wrroc_paper, "inherit_profiles": True, - "allow_shapes_override": False + "allow_requirement_check_override": False } settings = ValidationSettings(**settings) assert settings.inherit_profiles, "The inheritance mode should be set to True" - assert not settings.allow_shapes_override, "The override mode should be set to `True`" + assert not settings.allow_requirement_check_override, "The override mode should be set to `True`" validator = Validator(settings) # initialize the validation context context = ValidationContext(validator, validator.validation_settings.to_dict()) @@ -259,12 +259,12 @@ def test_load_valid_profile_with_override_on_inherited_profile(fake_profiles_pat "profile_identifier": "c-overridden", "data_path": ValidROC().wrroc_paper, "inherit_profiles": True, - "allow_shapes_override": True + "allow_requirement_check_override": True } settings = ValidationSettings(**settings) assert settings.inherit_profiles, "The inheritance mode should be set to True" - assert settings.allow_shapes_override, "The override mode should be set to `True`" + assert settings.allow_requirement_check_override, "The override mode should be set to `True`" validator = Validator(settings) # initialize the validation context context = ValidationContext(validator, validator.validation_settings.to_dict()) From a706116c137e449572c6914fd18c0cbe4aecd586 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 30 Jul 2024 08:39:02 +0200 Subject: [PATCH 739/902] fix(cli): :bug: properly set the verbose option on validate subcmd --- rocrate_validator/cli/commands/validate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index e4f6bafe..240afcb4 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -216,8 +216,7 @@ def validate(ctx, "requirement_severity": requirement_severity, "requirement_severity_only": requirement_severity_only, "inherit_profiles": not disable_profile_inheritance, - "show_details": False, - "details": verbose, + "verbose": verbose, "data_path": rocrate_uri, "ontology_path": Path(ontologies_path).absolute() if ontologies_path else None, "abort_on_first": not no_fail_fast From 3185211c31d814b5658c4eb7a484536686c6a879 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 30 Jul 2024 08:40:54 +0200 Subject: [PATCH 740/902] fix(shacl): :bug: fix missing notification of executed checks --- .../requirements/shacl/checks.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index d21bdb52..87cda8ec 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -160,15 +160,17 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): # 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 - if not shacl_context.fail_fast: - if requirementCheck.requirement.profile != shacl_context.current_validation_profile and \ - not requirementCheck.identifier in failed_requirement_checks_notified: - # - failed_requirement_checks_notified.append(requirementCheck.identifier) + if not requirementCheck.identifier in failed_requirement_checks_notified: + # + failed_requirement_checks_notified.append(requirementCheck.identifier) + shacl_context.result.add_executed_check(requirementCheck, False) + shacl_context.validator.notify(RequirementCheckValidationEvent( + EventType.REQUIREMENT_CHECK_VALIDATION_END, requirementCheck, validation_result=False)) + logger.debug("Added validation issue to the context: %s", c) - shacl_context.validator.notify(RequirementCheckValidationEvent( - EventType.REQUIREMENT_CHECK_VALIDATION_END, requirementCheck, validation_result=False)) - logger.debug("Added validation issue to the context: %s", c) + # if the fail fast mode is enabled, stop the validation after the first failed check + if shacl_context.fail_fast: + break # As above, but for skipped checks which are not failed logger.debug("Skipped checks: %s", len(shacl_context.result.skipped_checks)) @@ -187,6 +189,8 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): logger.debug("Added skipped check to the context: %s", requirementCheck.identifier) logger.debug("Remaining skipped checks: %r", len(shacl_context.result.skipped_checks)) + for requirementCheck in shacl_context.result.skipped_checks: + logger.debug("Remaining skipped check: %r - %s", requirementCheck.identifier, requirementCheck.name) end_time = timer() logger.debug(f"Execution time for parsing the validation result: {end_time - start_time} seconds") From 2625847aff861e09ec813cdd6e1d2baf9db5be38 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 30 Jul 2024 11:12:46 +0200 Subject: [PATCH 741/902] feat(cli): :sparkles: show overrides on `profiles describe` subcmd --- rocrate_validator/cli/commands/profiles.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 67f739b9..8bb2b686 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -245,10 +245,23 @@ def __verbose_describe_profile__(profile): color = get_severity_color(check.severity) level_info = f"[{color}]{check.severity.name}[/{color}]" levels_list.add(level_info) - logger.debug("Check %s: %s", check.name, check.description) - # checks.append(check) - table_rows.append((str(check.relative_identifier).rjust(14), check.name, - Markdown(check.description.strip()), level_info)) + override = None + if check.overridden_by: + severity_color = get_severity_color(check.overridden_by.severity) + override = f"[overridden by: [bold][magenta]{check.overridden_by.requirement.profile.identifier}[/magenta] "\ + f"[{severity_color}]{check.overridden_by.relative_identifier}[/{severity_color}][/bold]]" + elif check.override: + severity_color = get_severity_color(check.override.severity) + override = f"[override: [bold][magenta]{check.override.requirement.profile.identifier}[/magenta] "\ + f"[{severity_color}]{check.override.relative_identifier}[/{severity_color}][/bold]]" + from rich.align import Align + 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((str(check.relative_identifier), check.name, + description_table, level_info)) count_checks += 1 table = Table(show_header=True, From 89a91595e1d67be17c0575ff5e4b473b433dc00b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 30 Jul 2024 11:18:02 +0200 Subject: [PATCH 742/902] feat(cli): :sparkles: add option to configure output line width --- rocrate_validator/cli/commands/validate.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 240afcb4..2e70eb8d 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -167,6 +167,14 @@ def get_single_char(console: Optional[Console] = None, end: str = "\n", show_default=True, help="Path to the output file for the validation report", ) +@click.option( + '-w', + '--output-line-width', + type=click.INT, + default=120, + show_default=True, + help="Width of the output line", +) @click.pass_context def validate(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH, @@ -180,7 +188,8 @@ def validate(ctx, no_paging: bool = False, verbose: bool = False, format: str = "text", - output_file: Optional[Path] = None): + output_file: Optional[Path] = None, + output_line_width: Optional[int] = None): """ [magenta]rocrate-validator:[/magenta] Validate a RO-Crate against a profile """ @@ -283,7 +292,7 @@ def validate(ctx, f.write(result.to_json()) elif format == "text": with open(output_file, "w") as f: - c = Console(file=f, color_system=None, width=120, height=31) + c = Console(file=f, color_system=None, width=output_line_width, height=31) c.print(report_layout.layout) report_layout.console = c if not result.passed(): From 610e63a0f155b61fc322d505c16c7651ca23088c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 30 Jul 2024 11:19:25 +0200 Subject: [PATCH 743/902] refactor(cli): :recycle: rename output format option --- rocrate_validator/cli/commands/validate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 2e70eb8d..85f539cb 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -153,7 +153,7 @@ def get_single_char(console: Optional[Console] = None, end: str = "\n", ) @click.option( '-f', - '--format', + '--output-format', type=click.Choice(["json", "text"], case_sensitive=False), default="text", show_default=True, @@ -187,7 +187,7 @@ def validate(ctx, ontologies_path: Optional[Path] = None, no_paging: bool = False, verbose: bool = False, - format: str = "text", + output_format: str = "text", output_file: Optional[Path] = None, output_line_width: Optional[int] = None): """ @@ -287,10 +287,10 @@ def validate(ctx, if output_file: # Print the validation report to a file - if format == "json": + if output_format == "json": with open(output_file, "w") as f: f.write(result.to_json()) - elif format == "text": + elif output_format == "text": with open(output_file, "w") as f: c = Console(file=f, color_system=None, width=output_line_width, height=31) c.print(report_layout.layout) From 05cf5f5f69b313a6afcb553f1c2f05bb945860f4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 30 Jul 2024 11:21:23 +0200 Subject: [PATCH 744/902] refactor(logging): :necktie: update log message --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index a93922cf..22e054e5 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -454,7 +454,7 @@ def load_profiles(profiles_path: Union[str, Path], elif allow_requirement_check_override: check.overridden_by = check_chain[-1] check_chain.append(check) - logger.debug("Overridden check: %s", check) + logger.debug("Check %s overridden by %s", check.identifier, check.overridden_by.identifier) else: raise DuplicateRequirementCheck(check.name, profile.identifier) From 058bf9fc26421e2521785d55569c32cadf7b9800 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 30 Jul 2024 11:23:12 +0200 Subject: [PATCH 745/902] feat(core): :sparkles: add an explicit reference to the overridden check --- rocrate_validator/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 22e054e5..1906eec0 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -755,6 +755,7 @@ def __init__(self, self._name = name self._description = description self._overridden_by: RequirementCheck = None + self._override: RequirementCheck = None @property def order_number(self) -> int: @@ -807,6 +808,11 @@ def overridden_by(self, value: RequirementCheck) -> None: assert value is None or isinstance(value, RequirementCheck) and value != self, \ f"Invalid value for overridden_by: {value}" self._overridden_by = value + value._override = self + + @property + def override(self) -> RequirementCheck: + return self._override @property def overridden(self) -> bool: From a87a7fc811e043190024e10ed0dee4b3dd15bb4d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 30 Jul 2024 12:52:10 +0200 Subject: [PATCH 746/902] fix(shacl): :bug: update the flag name to enable shape overriding --- rocrate_validator/requirements/shacl/validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 36343375..0ec49823 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -111,7 +111,7 @@ def __set_current_validation_profile__(self, profile: Profile) -> bool: logger.debug("Loaded shapes: %s", profile_shapes) # enable overriding of checks - if self.settings.get("override_checks", False): + if self.settings.get("allow_requirement_check_override", True): from rocrate_validator.requirements.shacl.requirements import \ SHACLRequirement for requirement in [_ for _ in profile.requirements if isinstance(_, SHACLRequirement)]: From 9a827b224cdafd188216d337742ca2a3a7c60c87 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 30 Jul 2024 14:34:13 +0200 Subject: [PATCH 747/902] fix(core): :bug: do not apply overrides to shapes of the target profile --- rocrate_validator/models.py | 6 ++++++ rocrate_validator/requirements/shacl/validator.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 1906eec0..d203a446 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1542,6 +1542,12 @@ def profiles(self) -> list[Profile]: self._profiles = self.__load_profiles__() return self._profiles.copy() + @property + def target_profile(self) -> Profile: + profiles = self.profiles + assert len(profiles) > 0, "No profiles to validate" + return self.profiles[-1] + def get_profile_by_token(self, token: str) -> list[Profile]: return [p for p in self.profiles if p.token == token] diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 0ec49823..10885d1d 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -118,7 +118,7 @@ def __set_current_validation_profile__(self, profile: Profile) -> bool: logger.debug("Processing requirement: %s", requirement.name) for check in requirement.get_checks(): logger.debug("Processing check: %s", check) - if check.overridden: + if check.overridden and check.requirement.profile != self.target_profile: logger.debug("Overridden check: %s", check) profile_shapes_graph -= check.shape.graph profile_shapes.pop(check.shape.key) From 51e19ce678990399122f9783c50fa50994288f45 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Aug 2024 16:38:36 +0200 Subject: [PATCH 748/902] feat(ci): :construction_worker: integrate a Gitlab CI pipeline --- .gitlab-ci.yml | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..dcffc488 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,67 @@ +stages: + - install + - test + - build + +variables: + IMAGE: python:3.12-slim # Define the desired Docker image for the runner + VENV_PATH: .venv # Define the virtual environment path + POETRY_VERSION: 1.8.3 # Define the desired Poetry version + CACHE_KEY: python:3.12-slim # Define the cache key + RUNNER_TAG: rvdev # Define the desired runner tag + +# Install dependencies +install_dependencies: + stage: install + before_script: + - pip install --upgrade pip + - python -m venv ${VENV_PATH} + - source ${VENV_PATH}/bin/activate + - pip install poetry==${POETRY_VERSION} + script: + - poetry config virtualenvs.in-project true + - poetry install --no-interaction --no-ansi + cache: + key: ${CACHE_KEY} + paths: + - ${VENV_PATH} + tags: + - ${RUNNER_TAG} + +# Run tests +test: + stage: test + before_script: + - source ${VENV_PATH}/bin/activate + script: + - poetry run pytest + dependencies: + - install_dependencies + coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+)%/' + cache: + key: ${CACHE_KEY} + paths: + - ${VENV_PATH} + tags: + - ${RUNNER_TAG} + + +# Build the application +build: + stage: build + before_script: + - source ${VENV_PATH}/bin/activate + script: + - poetry build + dependencies: + - test + artifacts: + paths: + - dist/ + expire_in: 30 minutes + cache: + key: ${CACHE_KEY} + paths: + - ${VENV_PATH} + tags: + - ${RUNNER_TAG} From a5384f8283d36f52204be41696f1f4398d99c62d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Aug 2024 16:50:55 +0200 Subject: [PATCH 749/902] feat(ci): :sparkles: configure badge status --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c63faf01..bc7402d8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # `rocrate-validator` -[![Build Status](https://travis-ci.com/crs4/rocrate-validator.svg?branch=main)](https://travis-ci.com/crs4/rocrate-validator) +[![Build Status](https://repolab.crs4.it/lifemonitor/rocrate-validator/badges/develop/pipeline.svg +)](https://travis-ci.com/crs4/rocrate-validator) [![PyPI version](https://badge.fury.io/py/rocrate-validator.svg)](https://badge.fury.io/py/rocrate-validator) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) From f8f0b8eb86fa11b33cfba1813e172942c692cfc8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 1 Aug 2024 16:59:23 +0200 Subject: [PATCH 750/902] fix(ci): :bug: fix the link to the pipeline page --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bc7402d8..1a43b589 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # `rocrate-validator` [![Build Status](https://repolab.crs4.it/lifemonitor/rocrate-validator/badges/develop/pipeline.svg -)](https://travis-ci.com/crs4/rocrate-validator) +)](https://repolab.crs4.it/lifemonitor/rocrate-validator/-/pipelines?page=1&scope=branches&ref=develop) [![PyPI version](https://badge.fury.io/py/rocrate-validator.svg)](https://badge.fury.io/py/rocrate-validator) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) From 96445b8f05760d7c0455d838b495bfc73726d035 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 5 Aug 2024 14:25:17 +0200 Subject: [PATCH 751/902] fix(core): :sparkles: take into account the `conformsTo` property of the Root Data Entity --- rocrate_validator/models.py | 3 ++- rocrate_validator/rocrate.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index d203a446..6958c4ae 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1291,7 +1291,8 @@ def detect_rocrate_profiles(self) -> list[Profile]: try: # initialize the validation context context = ValidationContext(self, self.validation_settings.to_dict()) - candidate_profiles_uris = context.ro_crate.metadata.get_conforms_to() + candidate_profiles_uris = set(context.ro_crate.metadata.get_conforms_to( + ) + context.ro_crate.metadata.get_root_data_entity_conforms_to()) logger.debug("Candidate profiles: %s", candidate_profiles_uris) if not candidate_profiles_uris: logger.debug("Unable to determine the profile to validate against") diff --git a/rocrate_validator/rocrate.py b/rocrate_validator/rocrate.py index 6ac705e7..df7463ba 100644 --- a/rocrate_validator/rocrate.py +++ b/rocrate_validator/rocrate.py @@ -160,6 +160,20 @@ def get_root_data_entity(self) -> ROCrateEntity: raise ValueError("no main entity in metadata file descriptor") return main_entity + def get_root_data_entity_conforms_to(self) -> Optional[list[str]]: + 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] + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) + return None + def get_main_workflow(self) -> ROCrateEntity: root_data_entity = self.get_root_data_entity() main_workflow = root_data_entity.get_property('mainEntity') From f43ce50e8569939e9370e58750ee91bcb95b26a5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 5 Aug 2024 14:27:28 +0200 Subject: [PATCH 752/902] feat(cli): :sparkles: do not prompt for the validation profile in non-interactive mode --- rocrate_validator/cli/commands/validate.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 85f539cb..d941dddd 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -252,11 +252,16 @@ def validate(ctx, f"[bold]{profile.identifier}[/bold]: [white]{profile.name}[/white]" for profile in 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))) - selected_option = multiple_choice( - console, choices, "[italic]Available Profiles[/italic]", padding=(1, 2)) - selected_profile = available_profiles[int(selected_option) - 1].identifier - logger.debug("Profile selected: %s", selected_profile) - console.print(Padding(Rule(style="bold yellow"), (1, 2))) + if interactive: + selected_option = multiple_choice( + console, choices, "[italic]Available Profiles[/italic]", padding=(1, 2)) + selected_profile = available_profiles[int(selected_option) - 1].identifier + logger.debug("Profile selected: %s", selected_profile) + console.print(Padding(Rule(style="bold yellow"), (1, 2))) + else: + console.print(f"\n{' '*2}[bold yellow]WARNING: [/bold yellow]" + "[bold]Default profile will be used for validation[/bold]") + selected_profile = "ro-crate" # Set the selected profile validation_settings["profile_identifier"] = selected_profile validation_settings["profile_autodetected"] = autodetection From 7f61c0515b950037491fe46ac77884f13e8ec287 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 5 Aug 2024 14:57:09 +0200 Subject: [PATCH 753/902] feat(cli): :sparkles: allow specifying multiple profiles to validate --- rocrate_validator/cli/commands/validate.py | 94 +++++++++++++--------- 1 file changed, 54 insertions(+), 40 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index d941dddd..69af5723 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -107,6 +107,7 @@ def get_single_char(console: Optional[Console] = None, end: str = "\n", @click.option( "-p", "--profile-identifier", + multiple=True, type=click.STRING, default=None, show_default=True, @@ -237,7 +238,7 @@ def validate(ctx, # Detect the profile to use for validation autodetection = False selected_profile = profile_identifier - if selected_profile is None: + if selected_profile is None or len(selected_profile) == 0: candidate_profiles = services.detect_profiles(settings=validation_settings) if candidate_profiles and len(candidate_profiles) == 1: logger.debug("Profile identifier autodetected: %s", candidate_profiles[0].identifier) @@ -262,50 +263,63 @@ def validate(ctx, console.print(f"\n{' '*2}[bold yellow]WARNING: [/bold yellow]" "[bold]Default profile will be used for validation[/bold]") selected_profile = "ro-crate" - # Set the selected profile - validation_settings["profile_identifier"] = selected_profile - validation_settings["profile_autodetected"] = autodetection - logger.debug("Profile selected for validation: %s", validation_settings["profile_identifier"]) - logger.debug("Profile autodetected: %s", autodetection) - - # Compute the profile statistics - profile_stats = __compute_profile_stats__(validation_settings) - - report_layout = ValidationReportLayout(console, validation_settings, profile_stats, None) - - # Validate RO-Crate against the profile and get the validation result - result: ValidationResult = report_layout.live( - lambda: services.validate( - validation_settings, - subscribers=[report_layout.progress_monitor] + # add the selected profile to the list of profile_identifier + profile_identifier = [selected_profile] + + # Validate the RO-Crate against the selected profiles + is_valid = True + for profile in profile_identifier: + # Set the selected profile + validation_settings["profile_identifier"] = profile + validation_settings["profile_autodetected"] = autodetection + logger.debug("Profile selected for validation: %s", validation_settings["profile_identifier"]) + logger.debug("Profile autodetected: %s", autodetection) + + # Compute the profile statistics + profile_stats = __compute_profile_stats__(validation_settings) + + report_layout = ValidationReportLayout(console, validation_settings, profile_stats, None) + + # Validate RO-Crate against the profile and get the validation result + result: ValidationResult = report_layout.live( + lambda: services.validate( + validation_settings, + subscribers=[report_layout.progress_monitor] + ) ) - ) - # Print the validation result - if not result.passed(): - verbose_choice = "n" - if interactive and not verbose and enable_pager: - verbose_choice = get_single_char(console, choices=['y', 'n'], - message="[bold] > Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]") - if verbose_choice == "y" or verbose: - report_layout.show_validation_details(pager, enable_pager=enable_pager) - - if output_file: - # Print the validation report to a file - if output_format == "json": - with open(output_file, "w") as f: - f.write(result.to_json()) - elif output_format == "text": - with open(output_file, "w") as f: - c = Console(file=f, color_system=None, width=output_line_width, height=31) - c.print(report_layout.layout) - report_layout.console = c - if not result.passed(): - report_layout.show_validation_details(None, enable_pager=False) + # store the cumulative validation result + is_valid = is_valid and result.passed(LevelCollection.get(requirement_severity).severity) + + # Print the validation result + if not result.passed(): + verbose_choice = "n" + if interactive and not verbose and enable_pager: + verbose_choice = get_single_char(console, choices=['y', 'n'], + message="[bold] > Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]") + if verbose_choice == "y" or verbose: + report_layout.show_validation_details(pager, enable_pager=enable_pager) + + if output_file: + # Print the validation report to a file + if output_format == "json": + with open(output_file, "w") as f: + f.write(result.to_json()) + elif output_format == "text": + with open(output_file, "w") as f: + c = Console(file=f, color_system=None, width=output_line_width, height=31) + c.print(report_layout.layout) + report_layout.console = c + if not result.passed(): + report_layout.show_validation_details(None, enable_pager=False) + + # Interrupt the validation if the fail fast mode is enabled + if not no_fail_fast and not is_valid: + break # using ctx.exit seems to raise an Exception that gets caught below, # so we use sys.exit instead. - sys.exit(0 if result.passed(LevelCollection.get(requirement_severity).severity) else 1) + sys.exit(0 if is_valid else 1) except Exception as e: handle_error(e, console) From 5a8cd078bed51a2fcd8ba6d818a968b333bed1da Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 5 Aug 2024 14:59:18 +0200 Subject: [PATCH 754/902] refactor(cli): :lipstick: update output message --- 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 69af5723..1ae5ca22 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -609,11 +609,11 @@ def set_overall_result(self, result: ValidationResult): self.result = result if result.passed(): self.overall_result.update( - Padding(Rule(f"[bold][[green]OK[/green]] RO-Crate is [green]valid[/green] !!![/bold]\n\n", + Padding(Rule(f"[bold][[green]OK[/green]] RO-Crate is a [green]valid[/green] [magenta]{result.context.target_profile.identifier}[/magenta] !!![/bold]\n\n", style="bold green"), (1, 1))) else: self.overall_result.update( - Padding(Rule(f"[bold][[red]FAILED[/red]] RO-Crate is [red]not valid[/red] !!![/bold]\n", + Padding(Rule(f"[bold][[red]FAILED[/red]] RO-Crate is [red]not[/red] a [red]valid[/red] [magenta]{result.context.target_profile.identifier}[/magenta] !!![/bold]\n", style="bold red"), (1, 1))) def show_validation_details(self, pager: Pager, enable_pager: bool = True): From 90ab54e4fe670db2ac5ceee3469fd34721385b66 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 5 Aug 2024 15:00:29 +0200 Subject: [PATCH 755/902] test(cli): :white_check_mark: update unit tests --- tests/test_cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 0066b530..1182a308 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -34,17 +34,17 @@ def test_validate_subcmd_invalid_rocrate1(cli_runner: CliRunner): 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 - assert re.search(r'RO-Crate.*is valid', result.output) + assert re.search(r'RO-Crate.*is a valid', result.output) def test_validate_subcmd_valid_remote_rocrate(cli_runner: CliRunner): result = cli_runner.invoke( cli, ['validate', str(ValidROC().sort_and_change_remote), '--verbose', '--no-paging']) assert result.exit_code == 0 - assert re.search(r'RO-Crate.*is valid', result.output) + assert re.search(r'RO-Crate.*is a valid', result.output) def test_validate_subcmd_invalid_local_archive_rocrate(cli_runner: CliRunner): result = cli_runner.invoke(cli, ['validate', str(ValidROC().sort_and_change_archive), '--verbose', '--no-paging']) assert result.exit_code == 0 - assert re.search(r'RO-Crate.*is valid', result.output) + assert re.search(r'RO-Crate.*is a valid', result.output) From 76fa48147066357ca51fd18f415ee7f80b34fc00 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 5 Aug 2024 17:29:08 +0200 Subject: [PATCH 756/902] feat(cli): :sparkles: allow selecting multiple profiles --- poetry.lock | 34 ++++++++++- pyproject.toml | 1 + rocrate_validator/cli/commands/validate.py | 65 ++++++++++------------ 3 files changed, 63 insertions(+), 37 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3bca5185..4d3d2f45 100644 --- a/poetry.lock +++ b/poetry.lock @@ -575,6 +575,24 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "inquirerpy" +version = "0.3.4" +description = "Python port of Inquirer.js (A collection of common interactive command-line user interfaces)" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "InquirerPy-0.3.4-py3-none-any.whl", hash = "sha256:c65fdfbac1fa00e3ee4fb10679f4d3ed7a012abf4833910e63c295827fe2a7d4"}, + {file = "InquirerPy-0.3.4.tar.gz", hash = "sha256:89d2ada0111f337483cb41ae31073108b2ec1e618a49d7110b0d7ade89fc197e"}, +] + +[package.dependencies] +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)"] + [[package]] name = "ipykernel" version = "6.29.4" @@ -862,6 +880,20 @@ files = [ [package.dependencies] ptyprocess = ">=0.5" +[[package]] +name = "pfzy" +version = "0.3.4" +description = "Python port of the fzy fuzzy string matching algorithm" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "pfzy-0.3.4-py3-none-any.whl", hash = "sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96"}, + {file = "pfzy-0.3.4.tar.gz", hash = "sha256:717ea765dd10b63618e7298b2d98efd819e0b30cd5905c9707223dceeb94b3f1"}, +] + +[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)"] + [[package]] name = "pickleshare" version = "0.7.5" @@ -1612,4 +1644,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "9ae41d39fca6e10850ca8ee582f1defb89d3281648e6e47740e0a0f841306235" +content-hash = "f0b0854a2c87e506f581c31abedafe79628055a4e65116ce8a48907e10d29bc3" diff --git a/pyproject.toml b/pyproject.toml index 472b3347..123517dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ colorlog = "^6.8" requests = "^2.32.3" requests-cache = "^1.2.1" +inquirerpy = "^0.3.4" [tool.poetry.group.dev.dependencies] pyproject-flake8 = "^6.1.0" pylint = "^3.1.0" diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 1ae5ca22..63d8a718 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -7,6 +7,8 @@ from pathlib import Path from typing import Optional +from InquirerPy import prompt +from InquirerPy.base.control import Choice from rich.align import Align from rich.console import Console from rich.layout import Layout @@ -16,9 +18,7 @@ from rich.pager import Pager from rich.panel import Panel from rich.progress import BarColumn, Progress, TextColumn, TimeElapsedColumn -from rich.prompt import Prompt from rich.rule import Rule -from rich.table import Table import rocrate_validator.log as logging from rocrate_validator import services @@ -27,9 +27,9 @@ from rocrate_validator.cli.utils import get_app_header_rule from rocrate_validator.colors import get_severity_color from rocrate_validator.events import Event, EventType, Subscriber -from rocrate_validator.models import (LevelCollection, Severity, +from rocrate_validator.models import (LevelCollection, Profile, Severity, ValidationResult) -from rocrate_validator.utils import URI, get_profiles_path, get_version +from rocrate_validator.utils import URI, get_profiles_path # from rich.markdown import Markdown # from rich.table import Table @@ -243,28 +243,24 @@ def validate(ctx, if candidate_profiles and len(candidate_profiles) == 1: logger.debug("Profile identifier autodetected: %s", candidate_profiles[0].identifier) autodetection = True - selected_profile = candidate_profiles[0].identifier + profile_identifier = [candidate_profiles[0].identifier] else: logger.debug("Candidate profiles: %s", candidate_profiles) available_profiles = services.get_profiles(profiles_path) # Define the list of choices - # console.print(get_app_header_rule()) - choices = [ - f"[bold]{profile.identifier}[/bold]: [white]{profile.name}[/white]" for profile in 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))) if interactive: - selected_option = multiple_choice( - console, choices, "[italic]Available Profiles[/italic]", padding=(1, 2)) - selected_profile = available_profiles[int(selected_option) - 1].identifier - logger.debug("Profile selected: %s", selected_profile) + selected_options = multiple_choice(console, available_profiles) + profile_identifier = [available_profiles[int( + selected_option)].identifier for selected_option in selected_options] + logger.debug("Profile selected: %s", selected_options) console.print(Padding(Rule(style="bold yellow"), (1, 2))) else: console.print(f"\n{' '*2}[bold yellow]WARNING: [/bold yellow]" "[bold]Default profile will be used for validation[/bold]") selected_profile = "ro-crate" - # add the selected profile to the list of profile_identifier - profile_identifier = [selected_profile] + profile_identifier = [selected_profile] # Validate the RO-Crate against the selected profiles is_valid = True @@ -325,34 +321,31 @@ def validate(ctx, def multiple_choice(console: Console, - choices: list[str], - title: str = "Main Menu", - padding=(1, 2)) -> str: + choices: list[Profile]): """ Display a multiple choice menu """ - table = Table(title=title, title_justify="left") - table.add_column("#", justify="center", style="bold cyan", no_wrap=True) - table.add_column("Option", justify="left", style="magenta") - - for index, choice in enumerate(choices, start=1): - table.add_row(str(index), choice) - - if padding: - console.print(Padding(Align(table, align="left"), padding)) - else: - console.print(table) - # Build the prompt text - prompt_text = "[bold] > Please select a profile (enter the number)[/bold]" - # console_width = console.size.width - # padding = (console_width - len(prompt_text)) // 2 - # centered_prompt_text = " " * padding + prompt_text + prompt_text = "Please select the profiles to validate the RO-Crate against:" # Get the selected option - selected_option = Prompt.ask(prompt_text, - choices=[str(i) for i in range(1, len(choices) + 1)]) - return selected_option + question = [ + { + "type": "checkbox", + "name": "profiles", + "message": prompt_text, + "choices": [Choice(i, f"{choices[i].identifier}: {choices[i].name}") for i in range(0, len(choices))] + } + ] + console.print("\n") + selected = prompt(question, style={"questionmark": "#ff9d00 bold", + "questionmark": "#e5c07b", + "question": "bold", + "checkbox": "magenta", + "answer": "magenta"}, + style_override=False) + logger.debug("Selected profiles: %s", selected) + return selected["profiles"] class ProgressMonitor(Subscriber): From 611bcb6e0bfb361b9db8e34a92ab0e87438ee5f9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Aug 2024 08:40:14 +0200 Subject: [PATCH 757/902] refactor(cli): :lipstick: update output message --- 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 63d8a718..50cfb411 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -326,7 +326,7 @@ def multiple_choice(console: Console, Display a multiple choice menu """ # Build the prompt text - prompt_text = "Please select the profiles to validate the RO-Crate against:" + prompt_text = "Please select the profiles to validate the RO-Crate against ( to select):" # Get the selected option question = [ From 6aa36e1109286e6a6893eb431856148775e898ea Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Aug 2024 08:57:39 +0200 Subject: [PATCH 758/902] fix(cli): :lipstick: hide `overridden by` label --- rocrate_validator/cli/commands/profiles.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 8bb2b686..f1a4a537 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -246,11 +246,12 @@ def __verbose_describe_profile__(profile): level_info = f"[{color}]{check.severity.name}[/{color}]" levels_list.add(level_info) override = None - if check.overridden_by: - severity_color = get_severity_color(check.overridden_by.severity) - override = f"[overridden by: [bold][magenta]{check.overridden_by.requirement.profile.identifier}[/magenta] "\ - f"[{severity_color}]{check.overridden_by.relative_identifier}[/{severity_color}][/bold]]" - elif check.override: + # Uncomment the following lines to show the overridden checks + # if check.overridden_by: + # severity_color = get_severity_color(check.overridden_by.severity) + # override = f"[overridden by: [bold][magenta]{check.overridden_by.requirement.profile.identifier}[/magenta] "\ + # f"[{severity_color}]{check.overridden_by.relative_identifier}[/{severity_color}][/bold]]" + if check.override: severity_color = get_severity_color(check.override.severity) override = f"[override: [bold][magenta]{check.override.requirement.profile.identifier}[/magenta] "\ f"[{severity_color}]{check.override.relative_identifier}[/{severity_color}][/bold]]" From bc0ec452117eaa8044e9aafebf370948d96344c9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Aug 2024 10:09:49 +0200 Subject: [PATCH 759/902] feat(cli): :sparkles: add an option to disable the auto-detection of validation profiles --- rocrate_validator/cli/commands/validate.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 50cfb411..7ec785af 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -113,6 +113,14 @@ def get_single_char(console: Optional[Console] = None, end: str = "\n", show_default=True, help="Identifier of the profile to use for validation", ) +@click.option( + "-np", + "--no-auto-profile", + is_flag=True, + help="Disable automatic detection of the profile to use for validation", + default=False, + show_default=True +) @click.option( '-nh', '--disable-profile-inheritance', @@ -180,6 +188,7 @@ def get_single_char(console: Optional[Console] = None, end: str = "\n", def validate(ctx, profiles_path: Path = DEFAULT_PROFILES_PATH, profile_identifier: Optional[str] = None, + no_auto_profile: bool = False, disable_profile_inheritance: bool = False, requirement_severity: str = Severity.REQUIRED.name, requirement_severity_only: bool = False, @@ -239,14 +248,13 @@ def validate(ctx, autodetection = False selected_profile = profile_identifier if selected_profile is None or len(selected_profile) == 0: - candidate_profiles = services.detect_profiles(settings=validation_settings) - if candidate_profiles and len(candidate_profiles) == 1: - logger.debug("Profile identifier autodetected: %s", candidate_profiles[0].identifier) - autodetection = True - profile_identifier = [candidate_profiles[0].identifier] + # Auto-detect the profile to use for validation (if not disabled) + candidate_profiles = None + if not no_auto_profile: + candidate_profiles = services.detect_profiles(settings=validation_settings) + logger.error("Candidate profiles: %s", candidate_profiles) else: - logger.debug("Candidate profiles: %s", candidate_profiles) - available_profiles = services.get_profiles(profiles_path) + logger.info("Auto-detection of the profiles to use for validation is disabled") # Define the list of choices 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))) From d9b2bb2ae5d57a91daed256d2537f57e678d5c6d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Aug 2024 10:12:45 +0200 Subject: [PATCH 760/902] feat(cli): :sparkles: do not prompt for profile selection if reasonable candidate profiles are available --- rocrate_validator/cli/commands/validate.py | 25 ++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 7ec785af..49bb0043 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -244,10 +244,14 @@ def validate(ctx, # Print the application header console.print(get_app_header_rule()) + # Get the available profiles + available_profiles = services.get_profiles(profiles_path) + # Detect the profile to use for validation autodetection = False selected_profile = profile_identifier if selected_profile is None or len(selected_profile) == 0: + # Auto-detect the profile to use for validation (if not disabled) candidate_profiles = None if not no_auto_profile: @@ -255,15 +259,24 @@ def validate(ctx, logger.error("Candidate profiles: %s", candidate_profiles) else: logger.info("Auto-detection of the profiles to use for validation is disabled") + + # Prompt the user to select the profile to use for validation if the interactive mode is enabled + # and no profile is autodetected or multiple profiles are detected + if interactive and (not candidate_profiles or len(candidate_profiles) == 0 or len(candidate_profiles) == len(available_profiles)): # Define the list of choices 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))) - if interactive: - selected_options = multiple_choice(console, available_profiles) - profile_identifier = [available_profiles[int( - selected_option)].identifier for selected_option in selected_options] - logger.debug("Profile selected: %s", selected_options) - console.print(Padding(Rule(style="bold yellow"), (1, 2))) + selected_options = multiple_choice(console, available_profiles) + profile_identifier = [available_profiles[int( + selected_option)].identifier for selected_option in selected_options] + logger.debug("Profile selected: %s", selected_options) + 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 + profile_identifier = [_.identifier for _ in candidate_profiles] + else: console.print(f"\n{' '*2}[bold yellow]WARNING: [/bold yellow]" "[bold]Default profile will be used for validation[/bold]") From a41281d4712d187a489b8af94f9dca0a651d8359 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Aug 2024 10:13:36 +0200 Subject: [PATCH 761/902] refactor(cli): :lipstick: better output message for the fallback profile --- rocrate_validator/cli/commands/validate.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 49bb0043..d86fd2ef 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -277,11 +277,15 @@ def validate(ctx, autodetection = True profile_identifier = [_.identifier for _ in candidate_profiles] - else: - console.print(f"\n{' '*2}[bold yellow]WARNING: [/bold yellow]" - "[bold]Default profile will be used for validation[/bold]") - selected_profile = "ro-crate" - profile_identifier = [selected_profile] + # Fall back to the selected profile + if not profile_identifier or len(profile_identifier) == 0: + console.print(f"\n{' '*2}[bold yellow]WARNING: [/bold yellow]", end="") + if no_auto_profile: + 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]") + console.print(f"{' '*11}[bold]The base `ro-crate` profile will be used for validation[/bold]") + profile_identifier = ["ro-crate"] # Validate the RO-Crate against the selected profiles is_valid = True From 29d95998c56a8033195e8dd9451b60824271fe3e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Aug 2024 10:20:25 +0200 Subject: [PATCH 762/902] fix(logging): :loud_sound: fix log level --- 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 d86fd2ef..00b99aef 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -256,7 +256,7 @@ def validate(ctx, candidate_profiles = None if not no_auto_profile: candidate_profiles = services.detect_profiles(settings=validation_settings) - logger.error("Candidate profiles: %s", candidate_profiles) + logger.debug("Candidate profiles: %s", candidate_profiles) else: logger.info("Auto-detection of the profiles to use for validation is disabled") From 2443f2f64bd7607c3175464bf50222bade62ed51 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Aug 2024 16:36:12 +0200 Subject: [PATCH 763/902] refactor(logging): :loud_sound: update log messages --- rocrate_validator/requirements/shacl/checks.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 87cda8ec..af88bd5e 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -48,20 +48,23 @@ def shape(self) -> Shape: def execute_check(self, context: ValidationContext): logger.debug("Starting check %s", self) try: - logger.debug("SHACL Validation of profile %s requirement %s started", self.requirement.profile, self) + logger.debug("SHACL Validation of profile %s requirement %s started", + self.requirement.profile.identifier, self.identifier) with SHACLValidationContextManager(self, context) as ctx: # The check is executed only if the profile is the most specific one - logger.debug("SHACL Validation of profile %s requirement %s started", self.requirement.profile, self) + logger.debug("SHACL Validation of profile %s requirement %s started", + self.requirement.profile.identifier, self.identifier) result = self.__do_execute_check__(ctx) ctx.current_validation_result = not self in result return ctx.current_validation_result except SHACLValidationAlreadyProcessed as e: - logger.debug("SHACL Validation of profile %s already processed", self.requirement.profile) + logger.debug("SHACL Validation of profile %s already processed", self.requirement.profile.identifier) # 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 not self in [i.check for i in context.result.get_issues()] except SHACLValidationSkip as e: - logger.debug("SHACL Validation of profile %s requirement %s skipped", self.requirement.profile, self) + 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 760e553e12cf71e2bf5ed9489c46a5c10dda25f8 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Aug 2024 16:39:39 +0200 Subject: [PATCH 764/902] fix(core): :ambulance: fix `fail fast` condition --- 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 00b99aef..7f3c0dbf 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -335,7 +335,7 @@ def validate(ctx, report_layout.show_validation_details(None, enable_pager=False) # Interrupt the validation if the fail fast mode is enabled - if not no_fail_fast and not is_valid: + if no_fail_fast and not is_valid: break # using ctx.exit seems to raise an Exception that gets caught below, From 8471a243473b9649f031fd1df9380b99ade4dcff Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 6 Aug 2024 16:41:16 +0200 Subject: [PATCH 765/902] fix(shacl): :bug: do not count the number of checks twice --- rocrate_validator/requirements/shacl/checks.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index af88bd5e..682f165b 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -165,11 +165,12 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): # all together and not profile by profile if not requirementCheck.identifier in failed_requirement_checks_notified: # - failed_requirement_checks_notified.append(requirementCheck.identifier) - shacl_context.result.add_executed_check(requirementCheck, False) - shacl_context.validator.notify(RequirementCheckValidationEvent( - EventType.REQUIREMENT_CHECK_VALIDATION_END, requirementCheck, validation_result=False)) - logger.debug("Added validation issue to the context: %s", c) + if requirementCheck.requirement.profile != shacl_context.current_validation_profile: + failed_requirement_checks_notified.append(requirementCheck.identifier) + shacl_context.result.add_executed_check(requirementCheck, False) + shacl_context.validator.notify(RequirementCheckValidationEvent( + EventType.REQUIREMENT_CHECK_VALIDATION_END, requirementCheck, validation_result=False)) + logger.debug("Added validation issue to the context: %s", c) # if the fail fast mode is enabled, stop the validation after the first failed check if shacl_context.fail_fast: From 3ecce831a930904b3398258c7380495360aeec43 Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Thu, 8 Aug 2024 08:50:57 +0200 Subject: [PATCH 766/902] Add acknowledgement of funding sources to README --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a43b589..211945c9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) - A Python package to validate [ROCrate](https://researchobject.github.io/ro-crate/) packages. ## Setup @@ -96,3 +95,10 @@ Contributions are welcome! Please read our [contributing guidelines](CONTRIBUTIN This project is licensed under the terms of the Apache License 2.0. See the [LICENSE](LICENSE) file for details. + +## Acknowledgements + +This work has been partially funded by the following sources: + +* the [BY-COVID](https://by-covid.org/) project (HORIZON Europe grant agreement number 101046203); +* the [LIFEMap](https://www.thelifemap.it/) project, funded by the Italian Ministry of Health (Piano Operativo Salute, Traiettoria 3). From 64f13f52840ce43349aeedfb46ca980df103c7cf Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 09:37:07 +0200 Subject: [PATCH 767/902] feat(docs): :rocket: add EU logos --- .../EN_Co-fundedbytheEU_RGB_BLACK Outline.png | Bin 0 -> 63483 bytes .../eu-logo/EN_Co-fundedbytheEU_RGB_BLACK.png | Bin 0 -> 63434 bytes .../EN_Co-fundedbytheEU_RGB_Monochrome.png | Bin 0 -> 64061 bytes .../img/eu-logo/EN_Co-fundedbytheEU_RGB_NEG.png | Bin 0 -> 65290 bytes .../img/eu-logo/EN_Co-fundedbytheEU_RGB_POS.png | Bin 0 -> 69534 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/img/eu-logo/EN_Co-fundedbytheEU_RGB_BLACK Outline.png create mode 100644 docs/img/eu-logo/EN_Co-fundedbytheEU_RGB_BLACK.png create mode 100644 docs/img/eu-logo/EN_Co-fundedbytheEU_RGB_Monochrome.png create mode 100644 docs/img/eu-logo/EN_Co-fundedbytheEU_RGB_NEG.png create mode 100644 docs/img/eu-logo/EN_Co-fundedbytheEU_RGB_POS.png diff --git a/docs/img/eu-logo/EN_Co-fundedbytheEU_RGB_BLACK Outline.png b/docs/img/eu-logo/EN_Co-fundedbytheEU_RGB_BLACK Outline.png new file mode 100644 index 0000000000000000000000000000000000000000..6764ff476a802a10388073d9b599566afe3190f9 GIT binary patch literal 63483 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Ⓢ`2a(O7dA6FP>B>G#tI5hoe9;$1G&_W%L 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 D9(hW- literal 0 HcmV?d00001 diff --git a/docs/img/eu-logo/EN_Co-fundedbytheEU_RGB_BLACK.png b/docs/img/eu-logo/EN_Co-fundedbytheEU_RGB_BLACK.png new file mode 100644 index 0000000000000000000000000000000000000000..6a2cbbfceb676045d6bad12bfe3440d5e56a0383 GIT binary patch literal 63434 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

+ud#R);P*G#_9 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 literal 0 HcmV?d00001 diff --git a/docs/img/eu-logo/EN_Co-fundedbytheEU_RGB_Monochrome.png b/docs/img/eu-logo/EN_Co-fundedbytheEU_RGB_Monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..2ec6efaa61e41ca8f4fea72af485bfbdcd736210 GIT binary patch literal 64061 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

r@lyn-CWFK~Q~ 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 ComdC} literal 0 HcmV?d00001 diff --git a/docs/img/eu-logo/EN_Co-fundedbytheEU_RGB_POS.png b/docs/img/eu-logo/EN_Co-fundedbytheEU_RGB_POS.png new file mode 100644 index 0000000000000000000000000000000000000000..2a494fb227060f0ba3128552fd4bcda6e4ed7a81 GIT binary patch literal 69534 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 Date: Thu, 8 Aug 2024 09:58:34 +0200 Subject: [PATCH 768/902] feat(docs): :art: add EU logo to the README file --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 211945c9..4000268b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # `rocrate-validator` -[![Build Status](https://repolab.crs4.it/lifemonitor/rocrate-validator/badges/develop/pipeline.svg -)](https://repolab.crs4.it/lifemonitor/rocrate-validator/-/pipelines?page=1&scope=branches&ref=develop) +[![Build Status](https://repolab.crs4.it/lifemonitor/rocrate-validator/badges/develop/pipeline.svg)](https://repolab.crs4.it/lifemonitor/rocrate-validator/-/pipelines?page=1&scope=branches&ref=develop) [![PyPI version](https://badge.fury.io/py/rocrate-validator.svg)](https://badge.fury.io/py/rocrate-validator) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + A Python package to validate [ROCrate](https://researchobject.github.io/ro-crate/) packages. @@ -100,5 +100,10 @@ This project is licensed under the terms of the Apache License 2.0. See the This work has been partially funded by the following sources: -* the [BY-COVID](https://by-covid.org/) project (HORIZON Europe grant agreement number 101046203); -* the [LIFEMap](https://www.thelifemap.it/) project, funded by the Italian Ministry of Health (Piano Operativo Salute, Traiettoria 3). +Co-funded by the EU + +- the [BY-COVID](https://by-covid.org/) project (HORIZON Europe grant agreement number 101046203); +- the [LIFEMap](https://www.thelifemap.it/) project, funded by the Italian Ministry of Health (Piano Operative Salute, Trajectory 3). + From 9cac525d14517e8749f5cea4127e5f9a3a82fe90 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 15:12:54 +0200 Subject: [PATCH 769/902] chore(docs): :memo: add copyright notice --- rocrate_validator/cli/__init__.py | 13 +++++++++++++ rocrate_validator/cli/commands/errors.py | 14 ++++++++++++++ rocrate_validator/cli/commands/profiles.py | 14 ++++++++++++++ rocrate_validator/cli/commands/validate.py | 14 ++++++++++++++ rocrate_validator/cli/main.py | 13 +++++++++++++ rocrate_validator/cli/utils.py | 14 ++++++++++++++ rocrate_validator/colors.py | 14 ++++++++++++++ rocrate_validator/config.py | 14 ++++++++++++++ rocrate_validator/constants.py | 14 ++++++++++++++ rocrate_validator/errors.py | 13 +++++++++++++ rocrate_validator/events.py | 14 ++++++++++++++ rocrate_validator/log.py | 14 ++++++++++++++ rocrate_validator/models.py | 14 ++++++++++++++ .../ro-crate/must/0_file_descriptor_format.py | 14 ++++++++++++++ .../should/2_root_data_entity_relative_uri.py | 14 ++++++++++++++ .../ro-crate/should/5_web_data_entity_metadata.py | 14 ++++++++++++++ .../workflow-ro-crate/may/1_main_workflow.py | 14 ++++++++++++++ .../workflow-ro-crate/must/0_main_workflow.py | 14 ++++++++++++++ rocrate_validator/requirements/python/__init__.py | 14 ++++++++++++++ rocrate_validator/requirements/shacl/__init__.py | 14 ++++++++++++++ rocrate_validator/requirements/shacl/checks.py | 14 ++++++++++++++ rocrate_validator/requirements/shacl/errors.py | 14 ++++++++++++++ rocrate_validator/requirements/shacl/models.py | 14 ++++++++++++++ .../requirements/shacl/requirements.py | 14 ++++++++++++++ rocrate_validator/requirements/shacl/utils.py | 14 ++++++++++++++ rocrate_validator/requirements/shacl/validator.py | 14 ++++++++++++++ rocrate_validator/rocrate.py | 14 ++++++++++++++ rocrate_validator/services.py | 14 ++++++++++++++ rocrate_validator/utils.py | 14 ++++++++++++++ tests/conftest.py | 14 ++++++++++++++ tests/data/crates/valid/workflow-roc/make_crate.py | 14 ++++++++++++++ .../process-run-crate/test_procrc_action.py | 14 ++++++++++++++ .../process-run-crate/test_procrc_application.py | 14 ++++++++++++++ .../process-run-crate/test_procrc_collection.py | 14 ++++++++++++++ .../test_procrc_containerimage.py | 14 ++++++++++++++ .../test_procrc_root_data_entity.py | 14 ++++++++++++++ .../profiles/process-run-crate/test_valid_prc.py | 14 ++++++++++++++ .../profiles/ro-crate/test_data_entity_metadata.py | 14 ++++++++++++++ .../ro-crate/test_file_descriptor_entity.py | 14 ++++++++++++++ .../ro-crate/test_file_descriptor_format.py | 14 ++++++++++++++ .../profiles/ro-crate/test_root_data_entity.py | 14 ++++++++++++++ .../profiles/ro-crate/test_valid_ro-crate.py | 14 ++++++++++++++ .../workflow-ro-crate/test_main_workflow.py | 14 ++++++++++++++ .../profiles/workflow-ro-crate/test_valid_wroc.py | 14 ++++++++++++++ .../profiles/workflow-ro-crate/test_wroc_crate.py | 14 ++++++++++++++ .../workflow-ro-crate/test_wroc_descriptor.py | 14 ++++++++++++++ .../profiles/workflow-ro-crate/test_wroc_readme.py | 14 ++++++++++++++ .../workflow-ro-crate/test_wroc_root_metadata.py | 14 ++++++++++++++ tests/ro_crates.py | 14 ++++++++++++++ tests/shared.py | 14 ++++++++++++++ tests/test_cli.py | 13 +++++++++++++ tests/test_models.py | 14 ++++++++++++++ tests/unit/requirements/test_profiles.py | 14 ++++++++++++++ tests/unit/test_rocrate.py | 13 +++++++++++++ tests/unit/test_uri.py | 14 ++++++++++++++ tests/unit/test_validation_settings.py | 14 ++++++++++++++ 56 files changed, 779 insertions(+) diff --git a/rocrate_validator/cli/__init__.py b/rocrate_validator/cli/__init__.py index e1435c8e..bc4038b7 100644 --- a/rocrate_validator/cli/__init__.py +++ b/rocrate_validator/cli/__init__.py @@ -1,3 +1,16 @@ +# 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. from .commands import profiles, validate from .main import cli diff --git a/rocrate_validator/cli/commands/errors.py b/rocrate_validator/cli/commands/errors.py index 1c4db0e8..a8dd3c61 100644 --- a/rocrate_validator/cli/commands/errors.py +++ b/rocrate_validator/cli/commands/errors.py @@ -1,3 +1,17 @@ +# 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 sys import textwrap diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index f1a4a537..49c2a9e6 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -1,3 +1,17 @@ +# 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. + from pathlib import Path from rich.markdown import Markdown diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 7f3c0dbf..ca28f893 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -1,3 +1,17 @@ +# 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. + from __future__ import annotations import os diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index 90b5bd09..778373be 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -1,3 +1,16 @@ +# 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 sys diff --git a/rocrate_validator/cli/utils.py b/rocrate_validator/cli/utils.py index b9567bcf..42e3a386 100644 --- a/rocrate_validator/cli/utils.py +++ b/rocrate_validator/cli/utils.py @@ -1,3 +1,17 @@ +# 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 os import pydoc import re diff --git a/rocrate_validator/colors.py b/rocrate_validator/colors.py index 826d0674..3676b2d8 100644 --- a/rocrate_validator/colors.py +++ b/rocrate_validator/colors.py @@ -1,3 +1,17 @@ +# 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. + from typing import Union from .models import LevelCollection, Severity diff --git a/rocrate_validator/config.py b/rocrate_validator/config.py index a39c0c3b..7283a922 100644 --- a/rocrate_validator/config.py +++ b/rocrate_validator/config.py @@ -1,3 +1,17 @@ +# 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 import colorlog diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index abeec8c9..f6567073 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -1,3 +1,17 @@ +# 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. + # Define allowed RDF extensions and serialization formats as map import typing diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index c1bafaba..9c5c8722 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -1,3 +1,16 @@ +# 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. from typing import Optional diff --git a/rocrate_validator/events.py b/rocrate_validator/events.py index 773e9b71..e6b03023 100644 --- a/rocrate_validator/events.py +++ b/rocrate_validator/events.py @@ -1,3 +1,17 @@ +# 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 enum from abc import ABC, abstractmethod from functools import total_ordering diff --git a/rocrate_validator/log.py b/rocrate_validator/log.py index a5782914..aeeb4fbb 100644 --- a/rocrate_validator/log.py +++ b/rocrate_validator/log.py @@ -1,3 +1,17 @@ +# 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 atexit import sys import threading diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 6958c4ae..0d99be2c 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1,3 +1,17 @@ +# 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. + from __future__ import annotations import bisect diff --git a/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py index b3e25942..fc544da9 100644 --- a/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py @@ -1,3 +1,17 @@ +# 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 ValidationContext from rocrate_validator.requirements.python import (PyFunctionCheck, check, diff --git a/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py b/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py index 26421f1e..1bd5bd54 100644 --- a/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py +++ b/rocrate_validator/profiles/ro-crate/should/2_root_data_entity_relative_uri.py @@ -1,3 +1,17 @@ +# 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 ValidationContext from rocrate_validator.requirements.python import (PyFunctionCheck, check, diff --git a/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py index 7189ddac..c5122d38 100644 --- a/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py @@ -1,3 +1,17 @@ +# 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 ValidationContext from rocrate_validator.requirements.python import (PyFunctionCheck, check, 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 d595e7e8..dd5be1f5 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 @@ -1,3 +1,17 @@ +# 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 ValidationContext from rocrate_validator.requirements.python import (PyFunctionCheck, check, 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 3ea472f2..18fbc0d6 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 @@ -1,3 +1,17 @@ +# 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 ValidationContext from rocrate_validator.requirements.python import (PyFunctionCheck, check, diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index 6a645686..0920819a 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -1,3 +1,17 @@ +# 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 inspect import re diff --git a/rocrate_validator/requirements/shacl/__init__.py b/rocrate_validator/requirements/shacl/__init__.py index aaf4d888..68cca36f 100644 --- a/rocrate_validator/requirements/shacl/__init__.py +++ b/rocrate_validator/requirements/shacl/__init__.py @@ -1,3 +1,17 @@ +# 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. + from .checks import SHACLCheck from .errors import SHACLValidationError from .requirements import SHACLRequirement, SHACLRequirementLoader diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 682f165b..1fe9cebd 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -1,3 +1,17 @@ +# 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 json from timeit import default_timer as timer from typing import Optional diff --git a/rocrate_validator/requirements/shacl/errors.py b/rocrate_validator/requirements/shacl/errors.py index ebcf708f..4b70f6c4 100644 --- a/rocrate_validator/requirements/shacl/errors.py +++ b/rocrate_validator/requirements/shacl/errors.py @@ -1,3 +1,17 @@ +# 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. + from ...errors import ValidationError from .validator import SHACLValidationResult diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index d7b784a8..11c5414c 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -1,3 +1,17 @@ +# 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. + from __future__ import annotations from pathlib import Path diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index e58af9bb..d97020ea 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -1,3 +1,17 @@ +# 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. + from pathlib import Path from typing import Optional diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index fee6e69f..b1a9331f 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -1,3 +1,17 @@ +# 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. + from __future__ import annotations import hashlib diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 10885d1d..97c11fc6 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -1,3 +1,17 @@ +# 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. + from __future__ import annotations import os diff --git a/rocrate_validator/rocrate.py b/rocrate_validator/rocrate.py index df7463ba..38d8aa0f 100644 --- a/rocrate_validator/rocrate.py +++ b/rocrate_validator/rocrate.py @@ -1,3 +1,17 @@ +# 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. + from __future__ import annotations import io diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 90161f80..00ed6525 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -1,3 +1,17 @@ +# 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 shutil import tempfile import zipfile diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index 4e8254e3..a895d7db 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -1,3 +1,17 @@ +# 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. + from __future__ import annotations import inspect diff --git a/tests/conftest.py b/tests/conftest.py index c9f28965..162d343d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,17 @@ +# 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. + # calculate the absolute path of the rocrate-validator package # and add it to the system path import os diff --git a/tests/data/crates/valid/workflow-roc/make_crate.py b/tests/data/crates/valid/workflow-roc/make_crate.py index 1143cb4c..5b654197 100644 --- a/tests/data/crates/valid/workflow-roc/make_crate.py +++ b/tests/data/crates/valid/workflow-roc/make_crate.py @@ -1,3 +1,17 @@ +# 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. + """\ (Re)generate the RO-Crate metadata. Requires https://github.com/ResearchObject/ro-crate-py. diff --git a/tests/integration/profiles/process-run-crate/test_procrc_action.py b/tests/integration/profiles/process-run-crate/test_procrc_action.py index 3d06a169..0c6cbf2e 100644 --- a/tests/integration/profiles/process-run-crate/test_procrc_action.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_action.py @@ -1,3 +1,17 @@ +# 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.models import Severity diff --git a/tests/integration/profiles/process-run-crate/test_procrc_application.py b/tests/integration/profiles/process-run-crate/test_procrc_application.py index 631ba290..417f228f 100644 --- a/tests/integration/profiles/process-run-crate/test_procrc_application.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_application.py @@ -1,3 +1,17 @@ +# 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.models import Severity diff --git a/tests/integration/profiles/process-run-crate/test_procrc_collection.py b/tests/integration/profiles/process-run-crate/test_procrc_collection.py index d76ee275..15e0e97b 100644 --- a/tests/integration/profiles/process-run-crate/test_procrc_collection.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_collection.py @@ -1,3 +1,17 @@ +# 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.models import Severity diff --git a/tests/integration/profiles/process-run-crate/test_procrc_containerimage.py b/tests/integration/profiles/process-run-crate/test_procrc_containerimage.py index 800233ab..7af7ba9b 100644 --- a/tests/integration/profiles/process-run-crate/test_procrc_containerimage.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_containerimage.py @@ -1,3 +1,17 @@ +# 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.models import Severity diff --git a/tests/integration/profiles/process-run-crate/test_procrc_root_data_entity.py b/tests/integration/profiles/process-run-crate/test_procrc_root_data_entity.py index 6c4f4e57..7ac3fc0b 100644 --- a/tests/integration/profiles/process-run-crate/test_procrc_root_data_entity.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_root_data_entity.py @@ -1,3 +1,17 @@ +# 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.models import Severity diff --git a/tests/integration/profiles/process-run-crate/test_valid_prc.py b/tests/integration/profiles/process-run-crate/test_valid_prc.py index fe467fa0..f32b59f8 100644 --- a/tests/integration/profiles/process-run-crate/test_valid_prc.py +++ b/tests/integration/profiles/process-run-crate/test_valid_prc.py @@ -1,3 +1,17 @@ +# 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.models import Severity 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 1b5f1445..e34500a6 100644 --- a/tests/integration/profiles/ro-crate/test_data_entity_metadata.py +++ b/tests/integration/profiles/ro-crate/test_data_entity_metadata.py @@ -1,3 +1,17 @@ +# 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 diff --git a/tests/integration/profiles/ro-crate/test_file_descriptor_entity.py b/tests/integration/profiles/ro-crate/test_file_descriptor_entity.py index ff010aea..96ceba97 100644 --- a/tests/integration/profiles/ro-crate/test_file_descriptor_entity.py +++ b/tests/integration/profiles/ro-crate/test_file_descriptor_entity.py @@ -1,3 +1,17 @@ +# 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 import pytest 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 ecaf520c..3c4a779f 100644 --- a/tests/integration/profiles/ro-crate/test_file_descriptor_format.py +++ b/tests/integration/profiles/ro-crate/test_file_descriptor_format.py @@ -1,3 +1,17 @@ +# 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 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 dc151d0f..792aa5f3 100644 --- a/tests/integration/profiles/ro-crate/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/test_root_data_entity.py @@ -1,3 +1,17 @@ +# 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 diff --git a/tests/integration/profiles/ro-crate/test_valid_ro-crate.py b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py index a2f8515a..edc7dfb0 100644 --- a/tests/integration/profiles/ro-crate/test_valid_ro-crate.py +++ b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py @@ -1,3 +1,17 @@ +# 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 import pytest 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 0815dc74..e942e67b 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py +++ b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py @@ -1,3 +1,17 @@ +# 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.models import Severity 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 2b092b94..890af63e 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py +++ b/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py @@ -1,3 +1,17 @@ +# 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.models import Severity 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 b7ec71cf..3bee8058 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py @@ -1,3 +1,17 @@ +# 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.models import Severity 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 b1a387ad..f7b1bd6b 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py @@ -1,3 +1,17 @@ +# 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.models import Severity 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 0e7b8ea2..c4ec2aec 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py @@ -1,3 +1,17 @@ +# 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.models import Severity 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 dbe94212..69965027 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 @@ -1,3 +1,17 @@ +# 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.models import Severity diff --git a/tests/ro_crates.py b/tests/ro_crates.py index a70d4ddb..d5885de8 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -1,3 +1,17 @@ +# 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. + from pathlib import Path from tempfile import TemporaryDirectory diff --git a/tests/shared.py b/tests/shared.py index 34e11a84..9d6cd3a0 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -1,3 +1,17 @@ +# 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. + """ Library of shared functions for testing RO-Crate profiles """ diff --git a/tests/test_cli.py b/tests/test_cli.py index 1182a308..c6e66fb5 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,16 @@ +# 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 re diff --git a/tests/test_models.py b/tests/test_models.py index e32c945b..8a33744f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,3 +1,17 @@ +# 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 pytest from rocrate_validator import models, services diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index a77a0872..d0f1c21e 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -1,3 +1,17 @@ +# 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 import os diff --git a/tests/unit/test_rocrate.py b/tests/unit/test_rocrate.py index 1b418acb..ef6672ec 100644 --- a/tests/unit/test_rocrate.py +++ b/tests/unit/test_rocrate.py @@ -1,3 +1,16 @@ +# 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. from pathlib import Path import pytest diff --git a/tests/unit/test_uri.py b/tests/unit/test_uri.py index 522da00d..adbe6daa 100644 --- a/tests/unit/test_uri.py +++ b/tests/unit/test_uri.py @@ -1,3 +1,17 @@ +# 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 unittest import pytest diff --git a/tests/unit/test_validation_settings.py b/tests/unit/test_validation_settings.py index 5e20565d..1160a4e0 100644 --- a/tests/unit/test_validation_settings.py +++ b/tests/unit/test_validation_settings.py @@ -1,3 +1,17 @@ +# 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 pytest from rocrate_validator.models import Severity, ValidationSettings From 409762124fd04f90a765da56196787e152f13d54 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 15:17:01 +0200 Subject: [PATCH 770/902] chore(docs): :memo: add copyright notice --- .gitlab-ci.yml | 14 ++++++++++++++ pytest.ini | 14 ++++++++++++++ .../valid/wrroc-paper/mapping/environment.lock.yml | 14 ++++++++++++++ .../valid/wrroc-paper/mapping/environment.yml | 14 ++++++++++++++ .../valid/wrroc-paper/mapping/prov-mapping.yml | 14 ++++++++++++++ 5 files changed, 70 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dcffc488..ae994017 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,3 +1,17 @@ +# 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. + stages: - install - test diff --git a/pytest.ini b/pytest.ini index f023c908..bc3faa76 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,17 @@ +# 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. + [pytest] # markers = sources # log_cli=true diff --git a/tests/data/crates/valid/wrroc-paper/mapping/environment.lock.yml b/tests/data/crates/valid/wrroc-paper/mapping/environment.lock.yml index 81c27174..e233103b 100644 --- a/tests/data/crates/valid/wrroc-paper/mapping/environment.lock.yml +++ b/tests/data/crates/valid/wrroc-paper/mapping/environment.lock.yml @@ -1,3 +1,17 @@ +# 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. + name: sssom channels: - conda-forge diff --git a/tests/data/crates/valid/wrroc-paper/mapping/environment.yml b/tests/data/crates/valid/wrroc-paper/mapping/environment.yml index 9e6d1209..a23b09bf 100644 --- a/tests/data/crates/valid/wrroc-paper/mapping/environment.yml +++ b/tests/data/crates/valid/wrroc-paper/mapping/environment.yml @@ -1,3 +1,17 @@ +# 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. + name: sssom channels: - conda-forge diff --git a/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.yml b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.yml index 9529edb2..6a971338 100644 --- a/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.yml +++ b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.yml @@ -1,3 +1,17 @@ +# 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. + curie_map: prov: http://www.w3.org/ns/prov# schema: http://schema.org/ From 33d3a0197f7834ef9e5d5cf47a05d62461ecab67 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 15:24:37 +0200 Subject: [PATCH 771/902] fix(test-data): :bug: remove copyright notice --- .../valid/wrroc-paper/mapping/prov-mapping.yml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.yml b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.yml index 6a971338..15e882d2 100644 --- a/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.yml +++ b/tests/data/crates/valid/wrroc-paper/mapping/prov-mapping.yml @@ -1,17 +1,3 @@ -# 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. - curie_map: prov: http://www.w3.org/ns/prov# schema: http://schema.org/ @@ -26,4 +12,4 @@ mapping_set_title: 'Mapping PROV to Workflow Run RO-Crate' object_source: https://w3id.org/ro/wfrun/provenance/0.3 object_source_version: 0.3 subject_source: http://www.w3.org/ns/prov-o# -subject_source_version: 20130430 \ No newline at end of file +subject_source_version: 20130430 From b35a5dfd23cf0fa88c9e8f943684d3143558b1ce Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 15:30:05 +0200 Subject: [PATCH 772/902] ci(test-conf): :construction_worker: update cache key --- .gitlab-ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ae994017..e5a1e7c6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,11 +18,11 @@ stages: - build variables: - IMAGE: python:3.12-slim # Define the desired Docker image for the runner - VENV_PATH: .venv # Define the virtual environment path - POETRY_VERSION: 1.8.3 # Define the desired Poetry version - CACHE_KEY: python:3.12-slim # Define the cache key - RUNNER_TAG: rvdev # Define the desired runner tag + IMAGE: python:3.12-slim # Define the desired Docker image for the runner + VENV_PATH: .venv # Define the virtual environment path + POETRY_VERSION: 1.8.3 # Define the desired Poetry version + CACHE_KEY: $CI_COMMIT_REF_NAME # Define the cache key + RUNNER_TAG: rvdev # Define the desired runner tag # Install dependencies install_dependencies: From 620a1191031d0e608ff6b62fcff7e6274cf6ad8d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 15:39:17 +0200 Subject: [PATCH 773/902] fix(test-data): :bug: remove copyright notice --- tests/data/crates/valid/workflow-roc/make_crate.py | 14 -------------- .../valid/wrroc-paper/mapping/environment.lock.yml | 14 -------------- .../valid/wrroc-paper/mapping/environment.yml | 14 -------------- 3 files changed, 42 deletions(-) diff --git a/tests/data/crates/valid/workflow-roc/make_crate.py b/tests/data/crates/valid/workflow-roc/make_crate.py index 5b654197..1143cb4c 100644 --- a/tests/data/crates/valid/workflow-roc/make_crate.py +++ b/tests/data/crates/valid/workflow-roc/make_crate.py @@ -1,17 +1,3 @@ -# 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. - """\ (Re)generate the RO-Crate metadata. Requires https://github.com/ResearchObject/ro-crate-py. diff --git a/tests/data/crates/valid/wrroc-paper/mapping/environment.lock.yml b/tests/data/crates/valid/wrroc-paper/mapping/environment.lock.yml index e233103b..81c27174 100644 --- a/tests/data/crates/valid/wrroc-paper/mapping/environment.lock.yml +++ b/tests/data/crates/valid/wrroc-paper/mapping/environment.lock.yml @@ -1,17 +1,3 @@ -# 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. - name: sssom channels: - conda-forge diff --git a/tests/data/crates/valid/wrroc-paper/mapping/environment.yml b/tests/data/crates/valid/wrroc-paper/mapping/environment.yml index a23b09bf..9e6d1209 100644 --- a/tests/data/crates/valid/wrroc-paper/mapping/environment.yml +++ b/tests/data/crates/valid/wrroc-paper/mapping/environment.yml @@ -1,17 +1,3 @@ -# 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. - name: sssom channels: - conda-forge From 9fb409155b0e4d0e30fb26e38ec4db8d9569c05a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 15:50:37 +0200 Subject: [PATCH 774/902] fix(test-conf): :white_check_mark: update test --- tests/unit/test_rocrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_rocrate.py b/tests/unit/test_rocrate.py index ef6672ec..d48924f6 100644 --- a/tests/unit/test_rocrate.py +++ b/tests/unit/test_rocrate.py @@ -56,7 +56,7 @@ def test_valid_local_rocrate(): assert size == 26788, "Size should be 26788" # test crate size - assert roc.size == 309520, "Size should be 309520" + assert roc.size == 309521, "Size should be 309521" # test get_file_content binary mode content = roc.get_file_content(metadata_file_descriptor) From 0413093898d9c38b19c2d69b2fb6504fd457c124 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 15:55:33 +0200 Subject: [PATCH 775/902] fix(docs): :bug: update EU logo link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4000268b..b678b415 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ This project is licensed under the terms of the Apache License 2.0. See the This work has been partially funded by the following sources: Co-funded by the EU - the [BY-COVID](https://by-covid.org/) project (HORIZON Europe grant agreement number 101046203); From 35a16f11f8ca7fc2703c6430701ca169ec2d092f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 16:05:14 +0200 Subject: [PATCH 776/902] refactor(docs): update title format --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b678b415..154f0eb9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# `rocrate-validator` +# rocrate-validator [![Build Status](https://repolab.crs4.it/lifemonitor/rocrate-validator/badges/develop/pipeline.svg)](https://repolab.crs4.it/lifemonitor/rocrate-validator/-/pipelines?page=1&scope=branches&ref=develop) [![PyPI version](https://badge.fury.io/py/rocrate-validator.svg)](https://badge.fury.io/py/rocrate-validator) From cab2984ba5c5e80f8f031a0a1ec68745f5876929 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 16:07:24 +0200 Subject: [PATCH 777/902] fix(docs): :coffin: disable unused pypi badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 154f0eb9..275f9b4c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # rocrate-validator [![Build Status](https://repolab.crs4.it/lifemonitor/rocrate-validator/badges/develop/pipeline.svg)](https://repolab.crs4.it/lifemonitor/rocrate-validator/-/pipelines?page=1&scope=branches&ref=develop) -[![PyPI version](https://badge.fury.io/py/rocrate-validator.svg)](https://badge.fury.io/py/rocrate-validator) + [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) From 3f896d0664a2387c5c06fd57faed8ba9fd46358d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 16:08:17 +0200 Subject: [PATCH 778/902] fix(docs): :art: fix badge alignment --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 275f9b4c..4f0c1a70 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # rocrate-validator -[![Build Status](https://repolab.crs4.it/lifemonitor/rocrate-validator/badges/develop/pipeline.svg)](https://repolab.crs4.it/lifemonitor/rocrate-validator/-/pipelines?page=1&scope=branches&ref=develop) - -[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![Build Status](https://repolab.crs4.it/lifemonitor/rocrate-validator/badges/develop/pipeline.svg)](https://repolab.crs4.it/lifemonitor/rocrate-validator/-/pipelines?page=1&scope=branches&ref=develop)[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) From ef53d30e46915b46863b6ea397dc988053816210 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 16:10:15 +0200 Subject: [PATCH 779/902] fix(docs): :memo: fix usage section: missing `validate` subcmd --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4f0c1a70..cd5e5e7c 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ After installation, you can use the main command `rocrate-validator` to validate Run the validator using the following command: ```bash -poetry run rocrate-validator +poetry run rocrate-validator validate ``` Replace `` with the path to the ROCrate you want to validate. @@ -70,7 +70,7 @@ source .venv/bin/activate Then, run the validator using the following command: ```bash -rocrate-validator +rocrate-validator validate ``` Replace `` with the path to the ROCrate you want to validate. From bc376c72970a119ad8a991d594191e2a6b0fa256 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 16:15:55 +0200 Subject: [PATCH 780/902] refactor(profiles): :fire: remove profile stub --- .../profiles/workflow-run-crate/profile.ttl | 71 ------------------- 1 file changed, 71 deletions(-) delete mode 100644 rocrate_validator/profiles/workflow-run-crate/profile.ttl diff --git a/rocrate_validator/profiles/workflow-run-crate/profile.ttl b/rocrate_validator/profiles/workflow-run-crate/profile.ttl deleted file mode 100644 index addcc275..00000000 --- a/rocrate_validator/profiles/workflow-run-crate/profile.ttl +++ /dev/null @@ -1,71 +0,0 @@ -@prefix dct: . -@prefix prof: . -@prefix role: . -@prefix rdfs: . - - - - - a prof:Profile ; - - # the Profile's label - rdfs:label "Workflow Run Crate 0.5" ; - - # regular metadata, a basic description of the Profile - rdfs:comment """RO-Crate Metadata Specification."""@en ; - - # URI of the publisher of the Metadata Specification - dct:publisher ; - - # This profile is an extension of Process Run Crate and Workflow RO-Crate - prof:isProfileOf , - ; - - # Explicitly state that this profile is a transitive profile of the RO-Crate Metadata Specification 1.1 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 refered 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 refered 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 Process Run Crate and Workflow RO-Crate - prof:isInheritedFrom , - ; - ] ; - - # a short code to refer to the Profile with when a URI can't be used - prof:hasToken "workflow-run-crate" ; -. From 376bf1a57a67ecd98d16c22ba327a1f9acd9481e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 17:07:04 +0200 Subject: [PATCH 781/902] feat(core): :sparkles: define a local prefix for the validator entities --- rocrate_validator/constants.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index f6567073..0051afe8 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -20,6 +20,9 @@ # Define SHACL namespace SHACL_NS = "http://www.w3.org/ns/shacl#" +# Define the Validator namespace +VALIDATOR_NS = Namespace("https://github.com/crs4/rocrate-validator/") + # Define the Profiles Vocabulary namespace PROF_NS = Namespace("http://www.w3.org/ns/dx/prof/") From 7d11a4cda310531819e4d39cd88c1a0c87c5462b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 17:08:26 +0200 Subject: [PATCH 782/902] feat(core): :sparkles: add profile getter by name --- rocrate_validator/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 0d99be2c..6667ba41 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -293,6 +293,12 @@ def get_requirements( if (not exact_match and requirement.severity >= severity) or (exact_match and requirement.severity == severity)] + def get_requirement(self, name: str) -> Optional[Requirement]: + for requirement in self.requirements: + if requirement.name == name: + return requirement + return None + @classmethod def __get_nested_profiles__(cls, source: str) -> list[str]: result = [] From 1f67920fb49a1dcb543a18a5b4eae2dc70d0b95c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 17:09:19 +0200 Subject: [PATCH 783/902] refactor(core): :recycle: check hidden shape by using the local prefix --- rocrate_validator/requirements/shacl/requirements.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index d97020ea..de64b618 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -15,7 +15,10 @@ from pathlib import Path from typing import Optional +from rdflib import RDF + import rocrate_validator.log as logging +from rocrate_validator.constants import VALIDATOR_NS from ...models import (Profile, Requirement, RequirementCheck, RequirementLevel, RequirementLoader) @@ -73,10 +76,8 @@ def level(self) -> RequirementLevel: @property def hidden(self) -> bool: - from rdflib import RDF, Namespace - SHACL = Namespace("http://www.w3.org/ns/shacl#") if self.shape.node is not None: - if (self.shape.node, RDF.type, SHACL.hidden) in self.shape.graph: + if (self.shape.node, RDF.type, VALIDATOR_NS.HiddenShape) in self.shape.graph: return True return False From bde4d9bfa7cc9e0b0000a5f72a6785b2576b51b7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 17:11:29 +0200 Subject: [PATCH 784/902] refactor(profiles): :recycle: use the `validator:HiddenShape` entity --- .../profiles/ro-crate/must/1_file-descriptor_metadata.ttl | 3 ++- .../profiles/ro-crate/must/2_root_data_entity_metadata.ttl | 3 ++- .../profiles/ro-crate/must/5_web_data_entity_metadata.ttl | 3 ++- .../profiles/ro-crate/must/6_contextual_entity.ttl | 5 +++-- .../profiles/workflow-ro-crate/must/0_main-workflow.ttl | 3 ++- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index c44e4c39..654b514c 100644 --- a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -6,9 +6,10 @@ @prefix xml1: . @prefix xsd: . @prefix rocrate: . +@prefix validator: . -rocrate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape, sh:hidden; +rocrate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape, validator:HiddenShape; 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 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 5c5a31bd..a8937cac 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 @@ -6,6 +6,7 @@ @prefix xml1: . @prefix xsd: . @prefix rocrate: . +@prefix validator: . ro:RootDataEntityType @@ -46,7 +47,7 @@ ro:RootDataEntityType ] . -rocrate:FindRootDataEntity a sh:NodeShape, sh:hidden; +rocrate: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 resource ro-crate-metadata.json in the RO-Crate diff --git a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl index 76a42f93..62f412a7 100644 --- a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl @@ -8,9 +8,10 @@ @prefix owl: . @prefix rdfs: . @prefix rocrate: . +@prefix validator: . -ro:WebBasedDataEntity a sh:NodeShape, sh:hidden ; +ro: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""" ; diff --git a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl index b6bfd78e..b416e2ef 100644 --- a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl @@ -8,9 +8,10 @@ @prefix owl: . @prefix rdfs: . @prefix rocrate: . +@prefix validator: . -rocrate:FindLicenseEntity a sh:NodeShape, sh:hidden ; +rocrate:FindLicenseEntity a sh:NodeShape, validator:HiddenShape ; sh:name "Identify License Entity" ; sh:description """Mark a license entity any Data Entity referenced by the `schema:licence` property.""" ; sh:target [ @@ -54,7 +55,7 @@ rocrate:WebSiteRecommendedProperties a sh:NodeShape ; ] . -rocrate:CreativeWorkAuthorDefinition a sh:NodeShape, sh:hidden ; +rocrate:CreativeWorkAuthorDefinition a sh:NodeShape, validator:HiddenShape ; sh:name "CreativeWork Author Definition" ; sh:description """Define the `CretiveWorkAuthor` as the `Person` object of the `schema:author` predicate.""" ; sh:targetObjectsOf schema:author ; 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 af458063..333aed27 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 @@ -7,6 +7,7 @@ @prefix xsd: . @prefix rocrate: . @prefix bioschemas: . +@prefix validator: . ro:MainEntityProperyMustExist a sh:NodeShape ; @@ -22,7 +23,7 @@ ro:MainEntityProperyMustExist a sh:NodeShape ; ] . -ro:FindMainWorkflow a sh:NodeShape, sh:hidden ; +ro:FindMainWorkflow a sh:NodeShape, validator:HiddenShape ; sh:name "Identify Main Workflow" ; sh:description "Identify the Main Workflow" ; sh:target [ From f2379744d88dfb14d9af7ecae1b623364ae8ed08 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 17:11:57 +0200 Subject: [PATCH 785/902] test(core): :white_check_mark: add unit to validate hidden shapes --- tests/test_models.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_models.py b/tests/test_models.py index 8a33744f..2154955d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -108,3 +108,13 @@ def test_sortability_issues(validation_settings: ValidationSettings): one, two = next(i_issues), next(i_issues) assert one >= two assert one.check >= two.check + + +def test_hidden_shape(): + rocrate_profile = services.get_profile(profile_identifier="ro-crate-1.1") + assert rocrate_profile is not None, "Profile should not be None" + # get the hidden requirement + hidden_requirement = rocrate_profile.get_requirement("Identify the Root Data Entity of the RO-Crate") + assert hidden_requirement is not None, "Requirement should not be None" + # check if the requirement is hidden + assert hidden_requirement.hidden is True, "Hidden should be True" From 8647819d7cfd97083ce1650d6377554db0e2c9b6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 8 Aug 2024 20:07:10 +0200 Subject: [PATCH 786/902] refactor(profiles): :truck: use local prefix to denote entities defined by rules --- .../may/0_software-application.ttl | 2 +- .../process-run-crate/may/1_create_action.ttl | 2 +- .../may/3_container_image.ttl | 2 +- .../must/1_create_action.ttl | 6 +++- .../must/2_root_data_entity_metadata.ttl | 9 +++-- .../should/0_software-application.ttl | 7 +++- .../should/1_create_action.ttl | 9 +++-- .../process-run-crate/should/2_collection.ttl | 9 +++-- .../should/3_container_image.ttl | 7 +++- .../ro-crate/may/4_data_entity_metadata.ttl | 13 ++++--- .../must/1_file-descriptor_metadata.ttl | 7 ++-- .../must/2_root_data_entity_metadata.ttl | 15 ++++---- .../ro-crate/must/4_data_entity_metadata.ttl | 36 +++++++++---------- .../must/5_web_data_entity_metadata.ttl | 5 +-- .../ro-crate/must/6_contextual_entity.ttl | 11 +++--- .../profiles/ro-crate/ontology.ttl | 9 ++--- .../should/2_root_data_entity_metadata.ttl | 7 ++-- .../should/4_data_entity_metadata.ttl | 8 +++-- .../should/6_contextual_entity_metadata.ttl | 12 ++++--- .../workflow-ro-crate/may/0_main-workflow.ttl | 9 +++-- .../must/0_main-workflow.ttl | 11 +++--- .../must/1_wroc_root_data_entity.ttl | 8 +++-- .../workflow-ro-crate/should/1_wroc_crate.ttl | 8 +++-- .../should/2_main-workflow.ttl | 9 +++-- 24 files changed, 144 insertions(+), 77 deletions(-) diff --git a/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl b/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl index 682b4247..2246a927 100644 --- a/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl @@ -5,7 +5,7 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . -@prefix rocrate: . +# @prefix rocrate: . @prefix bioschemas: . diff --git a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl index 2f7a124f..79d08a4e 100644 --- a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl @@ -5,7 +5,7 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . -@prefix rocrate: . +# @prefix rocrate: . @prefix bioschemas: . @prefix wfrun: . diff --git a/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl b/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl index c43df33f..a3b19b40 100644 --- a/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl @@ -5,7 +5,7 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . -@prefix rocrate: . +# @prefix rocrate: . @prefix bioschemas: . @prefix wfrun: . diff --git a/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl index a90e19a9..0eb73403 100644 --- a/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl @@ -5,9 +5,13 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . -@prefix rocrate: . @prefix bioschemas: . +@prefix validator: . +@prefix ro-crate: . +@prefix process-run-crate: . + + ro:ProcRCActionRequired a sh:NodeShape ; sh:name "Process Run Crate Action" ; sh:description "Properties of the Process Run Crate Action" ; diff --git a/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl index de38931f..d22d57d2 100644 --- a/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl @@ -5,12 +5,17 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . -@prefix rocrate: . +# @prefix rocrate: . + +@prefix validator: . +@prefix ro-crate: . +@prefix process-run-crate: . + ro:ProcRCRootDataEntityMetadata a sh:NodeShape ; sh:name "Root Data Entity Metadata" ; sh:description "Properties of the Root Data Entity" ; - sh:targetClass rocrate:RootDataEntity ; + sh:targetClass ro-crate:RootDataEntity ; sh:property [ a sh:PropertyShape ; sh:name "Root Data Entity conformsTo" ; diff --git a/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl b/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl index bb2c7c54..1c3ecc4d 100644 --- a/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl @@ -5,9 +5,14 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . -@prefix rocrate: . +# @prefix rocrate: . @prefix bioschemas: . +@prefix validator: . +@prefix ro-crate: . +@prefix process-run-crate: . + + ro:ProcRCApplication a sh:NodeShape ; sh:name "ProcRC Application" ; diff --git a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl index c440077a..07aa4f08 100644 --- a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl @@ -5,10 +5,15 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . -@prefix rocrate: . +# @prefix rocrate: . @prefix bioschemas: . @prefix wfrun: . +@prefix validator: . +@prefix ro-crate: . +@prefix process-run-crate: . + + ro:ProcRCActionRecommended a sh:NodeShape ; sh:name "Process Run Crate Action SHOULD" ; sh:description "Recommended properties of the Process Run Crate Action" ; @@ -20,7 +25,7 @@ ro:ProcRCActionRecommended a sh:NodeShape ; sh:name "Action SHOULD be referenced via mentions from root" ; sh:description "The Action SHOULD be referenced from the Root Data Entity via mentions" ; sh:path [ sh:inversePath schema:mentions ] ; - sh:node rocrate:RootDataEntity ; + sh:node ro-crate:RootDataEntity ; sh:minCount 1 ; sh:message "The Action SHOULD be referenced from the Root Data Entity via mentions" ; ] ; diff --git a/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl b/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl index 2c5eefad..47b025eb 100644 --- a/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl @@ -5,9 +5,14 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . -@prefix rocrate: . +# @prefix rocrate: . @prefix bioschemas: . +@prefix validator: . +@prefix ro-crate: . +@prefix process-run-crate: . + + ro:ProcRCCollectionRecommended a sh:NodeShape ; sh:name "Process Run Crate Collection SHOULD" ; sh:description "Recommended properties of the Process Run Crate Collection" ; @@ -31,7 +36,7 @@ ro:ProcRCCollectionRecommended a sh:NodeShape ; sh:name "Collection SHOULD be referenced via mentions from root" ; sh:description "The Collection SHOULD be referenced from the Root Data Entity via mentions" ; sh:path [ sh:inversePath schema:mentions ] ; - sh:node rocrate:RootDataEntity ; + sh:node ro-crate:RootDataEntity ; sh:minCount 1 ; sh:message "The Collection SHOULD be referenced from the Root Data Entity via mentions" ; ] ; diff --git a/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl b/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl index d0813c8e..a7d2d833 100644 --- a/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl @@ -5,10 +5,15 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . -@prefix rocrate: . +# @prefix rocrate: . @prefix bioschemas: . @prefix wfrun: . +@prefix validator: . +@prefix ro-crate: . +@prefix process-run-crate: . + + ro:ProcRCContainerImageRecommended a sh:NodeShape ; sh:name "Process Run Crate ContainerImage SHOULD" ; sh:description "Recommended properties of the Process Run Crate ContainerImage" ; diff --git a/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl index 3ecd5fa7..8c7c23a4 100644 --- a/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl @@ -7,18 +7,21 @@ @prefix xsd: . @prefix owl: . @prefix rdfs: . -@prefix rocrate: . + @prefix schema: . +@prefix validator: . +@prefix ro-crate: . + -rocrate:FileDataEntityWebOptionalProperties a sh:NodeShape ; +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: `รฌdentifier`, `url`, `subjectOf`and `mainEntityOfPage`""" ; - sh:targetClass rocrate:File ; + sh:targetClass ro-crate:File ; # Check if the Web-based Data Entity has a contentSize property sh:property [ a sh:PropertyShape ; @@ -62,10 +65,10 @@ rocrate:FileDataEntityWebOptionalProperties a sh:NodeShape ; ] . -rocrate:DirectoryDataEntityWebOptionalDistribution a sh:NodeShape ; +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 rocrate:File ; + sh:targetClass ro-crate:File ; # Check if the Web-based Data Entity has a contentSize property sh:property [ a sh:PropertyShape ; diff --git a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index 654b514c..2e93a675 100644 --- a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -7,9 +7,10 @@ @prefix xsd: . @prefix rocrate: . @prefix validator: . +@prefix ro-crate: . -rocrate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape, validator:HiddenShape; +ro-crate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape, validator:HiddenShape; 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 @@ -31,7 +32,7 @@ rocrate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape, validator:Hidden a sh:TripleRule ; sh:subject sh:this ; sh:predicate rdf:type ; - sh:object rocrate:ROCrateMetadataFileDescriptor ; + sh:object ro-crate:ROCrateMetadataFileDescriptor ; ] . ro:ROCrateMetadataFileDescriptorExistence @@ -45,7 +46,7 @@ ro:ROCrateMetadataFileDescriptorExistence sh:description """Check if the RO-Crate Metadata File Descriptor entity exists, i.e., if there exists an entity with @id `ro-crate-metadata.json` and type `schema:CreativeWork`""" ; sh:path rdf:type ; - sh:hasValue rocrate:ROCrateMetadataFileDescriptor ; + sh:hasValue ro-crate:ROCrateMetadataFileDescriptor ; sh:minCount 1 ; sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; ] . 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 a8937cac..56df5c89 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 @@ -7,6 +7,7 @@ @prefix xsd: . @prefix rocrate: . @prefix validator: . +@prefix ro-crate: . ro:RootDataEntityType @@ -47,7 +48,7 @@ ro:RootDataEntityType ] . -rocrate:FindRootDataEntity a sh:NodeShape, validator:HiddenShape; +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 resource ro-crate-metadata.json in the RO-Crate @@ -71,7 +72,7 @@ rocrate:FindRootDataEntity a sh:NodeShape, validator:HiddenShape; a sh:TripleRule ; sh:subject sh:this ; sh:predicate rdf:type ; - sh:object rocrate:RootDataEntity ; + sh:object ro-crate:RootDataEntity ; ] . @@ -79,7 +80,7 @@ ro:RootDataEntityValueRestriction a sh:NodeShape ; sh:name "RO-Crate Root Data Entity value restriction" ; sh:description "The Root Data Entity MUST end with `/`" ; - sh:targetNode rocrate:RootDataEntity ; + sh:targetNode ro-crate:RootDataEntity ; sh:property [ a sh:PropertyShape ; sh:name "Root Data Entity URI value" ; @@ -94,16 +95,16 @@ ro:RootDataEntityHasPartValueRestriction a sh:NodeShape ; sh:name "RO-Crate Root Data Entity: `hasPart` value restriction" ; sh:description "The Root Data Entity MUST be linked to the declared `File`, `Directory` and other types of instances through the `hasPart` property" ; - sh:targetClass rocrate:RootDataEntity ; + sh:targetClass ro-crate:RootDataEntity ; sh:property [ a sh:PropertyShape ; sh:name "RO-Crate Root Data Entity: `hasPart` value restriction" ; sh:description "Check if the Root Data Entity is linked to the declared `File`, `Directory` and other types of instances through the `hasPart` property" ; sh:path schema_org:hasPart ; sh:or ( - [ sh:class rocrate:File ] - [ sh:class rocrate:Directory ] - [ sh:class rocrate:GenericDataEntity ] + [ sh:class ro-crate:File ] + [ sh:class ro-crate:Directory ] + [ sh:class ro-crate:GenericDataEntity ] ) ; # sh:message """The Root Data Entity MUST be linked to either File or Directory instances, nothing else""" ; ] . diff --git a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl index abe7b0cd..72cdb3cd 100644 --- a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl @@ -7,14 +7,14 @@ @prefix xsd: . @prefix owl: . @prefix rdfs: . -@prefix rocrate: . - +@prefix validator: . +@prefix ro-crate: . ro: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 sbsolute URI""" ; - sh:targetClass rocrate:DataEntity ; + sh:targetClass ro-crate:DataEntity ; sh:property [ sh:name "Data Entity: @id value restriction" ; @@ -49,7 +49,7 @@ ro:FileDataEntity a sh:NodeShape ; `File` is an RO-Crate alias for the schema.org `MediaObject`. """ ; sh:path rdf:type ; - sh:hasValue rocrate:File ; + sh:hasValue ro-crate:File ; sh:severity sh:Violation ; sh:message """File Data Entities MUST have "File" as a value for @type.""" ; ] ; @@ -59,7 +59,7 @@ ro:FileDataEntity a sh:NodeShape ; a sh:TripleRule ; sh:subject sh:this ; sh:predicate rdf:type ; - sh:object rocrate:DataEntity ; + sh:object ro-crate:DataEntity ; ] . @@ -87,7 +87,7 @@ ro:DirectoryDataEntity a sh:NodeShape ; # sh:name "Test Directory" ; # sh:description """Data Entities representing directories MUST have "Directory" as a value for @type.""" ; # sh:path rdf:type ; - # sh:hasValue rocrate:File ; + # sh:hasValue ro-crate:File ; # sh:severity sh:Violation ; # ] ; @@ -96,7 +96,7 @@ ro:DirectoryDataEntity a sh:NodeShape ; a sh:TripleRule ; sh:subject sh:this ; sh:predicate rdf:type ; - sh:object rocrate:Directory ; + sh:object ro-crate:Directory ; ] ; # Expand data graph with triples from the directory data entity @@ -104,7 +104,7 @@ ro:DirectoryDataEntity a sh:NodeShape ; a sh:TripleRule ; sh:subject sh:this ; sh:predicate rdf:type ; - sh:object rocrate:DataEntity ; + sh:object ro-crate:DataEntity ; ] ; # Ensure that the directory data entity is a dataset @@ -119,7 +119,7 @@ ro:DirectoryDataEntity a sh:NodeShape ; ro:DataEntityRquiredPropertiesShape a sh:NodeShape ; sh:name "Data Entity: REQUIRED properties" ; sh:description """A `DataEntity` MUST be linked, either directly or inderectly, from the Root Data Entity""" ; - sh:targetClass rocrate:DataEntity ; + sh:targetClass ro-crate:DataEntity ; sh:property [ a sh:PropertyShape ; @@ -134,7 +134,7 @@ ro:DataEntityRquiredPropertiesShape a sh:NodeShape ; ro:DirectoryDataEntityRequiredValueRestriction a sh:NodeShape ; sh:name "Directory Data Entity: REQUIRED value restriction" ; sh:description """A Directory Data Entity MUST end with `/`""" ; - sh:targetNode rocrate:Directory ; + sh:targetNode ro-crate:Directory ; sh:property [ a sh:PropertyShape ; sh:name "Directory Data Entity: REQUIRED value restriction" ; @@ -171,7 +171,7 @@ ro:GenericDataEntity a sh:NodeShape ; a sh:TripleRule ; sh:subject sh:this ; sh:predicate rdf:type ; - sh:object rocrate:GenericDataEntity ; + sh:object ro-crate:GenericDataEntity ; ] ; # Expand data graph with triples to mark the matching entities as DataEntity instances @@ -179,20 +179,20 @@ ro:GenericDataEntity a sh:NodeShape ; a sh:TripleRule ; sh:subject sh:this ; sh:predicate rdf:type ; - sh:object rocrate:DataEntity ; + sh:object ro-crate:DataEntity ; ] . # Uncomment for debugging -# rocrate:TestGenericDataEntity a sh:NodeShape ; +# ro-crate:TestGenericDataEntity a sh:NodeShape ; # sh:disabled true ; -# sh:targetClass rocrate:GenericDataEntity ; +# 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 rocrate:invalidProperty ; +# sh:path ro-crate:invalidProperty ; # sh:severity sh:Violation ; # sh:message "Testing the generic data entity"; # sh:datatype xsd:string ; @@ -204,12 +204,12 @@ ro:GenericDataEntity a sh:NodeShape ; # 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 rocrate:Directory ; +# sh:targetClass ro-crate:Directory ; # sh:property [ # sh:name "Test Directory instance" ; -# sh:description """Check if the Directory DataEntity instance has the fake property rocrate:foo""" ; +# sh:description """Check if the Directory DataEntity instance has the fake property ro-crate:foo""" ; # sh:path rdf:type ; -# sh:hasValue rocrate:foo ; +# sh:hasValue ro-crate:foo ; # sh:severity sh:Violation ; # ] . diff --git a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl index 62f412a7..810b30d2 100644 --- a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl @@ -9,6 +9,7 @@ @prefix rdfs: . @prefix rocrate: . @prefix validator: . +@prefix ro-crate: . ro:WebBasedDataEntity a sh:NodeShape, validator:HiddenShape ; @@ -33,7 +34,7 @@ ro:WebBasedDataEntity a sh:NodeShape, validator:HiddenShape ; a sh:TripleRule ; sh:subject sh:this ; sh:predicate rdf:type ; - sh:object rocrate:WebDataEntity ; + sh:object ro-crate:WebDataEntity ; ] . @@ -41,7 +42,7 @@ ro:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; sh:name "Web-based Data Entity: RECOMMENDED properties" ; sh:description """A Web-based Data Entity MUST be identified by an absolute URL and SHOULD have a `contentSize` and `sdDatePublished` property""" ; - sh:targetClass rocrate:WebDataEntity ; + sh:targetClass ro-crate:WebDataEntity ; # Check if the Web-based Data Entity has a contentSize property sh:property [ a sh:PropertyShape ; diff --git a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl index b416e2ef..35392f11 100644 --- a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl @@ -9,9 +9,10 @@ @prefix rdfs: . @prefix rocrate: . @prefix validator: . +@prefix ro-crate: . -rocrate:FindLicenseEntity a sh:NodeShape, validator:HiddenShape ; +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:licence` property.""" ; sh:target [ @@ -30,11 +31,11 @@ rocrate:FindLicenseEntity a sh:NodeShape, validator:HiddenShape ; a sh:TripleRule ; sh:subject sh:this ; sh:predicate rdf:type ; - sh:object rocrate:ContextualEntity ; + sh:object ro-crate:ContextualEntity ; ] . -rocrate:WebSiteRecommendedProperties a sh:NodeShape ; +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 ; @@ -55,7 +56,7 @@ rocrate:WebSiteRecommendedProperties a sh:NodeShape ; ] . -rocrate:CreativeWorkAuthorDefinition a sh:NodeShape, validator:HiddenShape ; +ro-crate:CreativeWorkAuthorDefinition a sh:NodeShape, validator:HiddenShape ; sh:name "CreativeWork Author Definition" ; sh:description """Define the `CretiveWorkAuthor` as the `Person` object of the `schema:author` predicate.""" ; sh:targetObjectsOf schema:author ; @@ -63,7 +64,7 @@ rocrate:CreativeWorkAuthorDefinition a sh:NodeShape, validator:HiddenShape ; a sh:TripleRule ; sh:subject sh:this ; sh:predicate rdf:type ; - sh:object rocrate:CreativeWorkAuthor ; + sh:object ro-crate:CreativeWorkAuthor ; sh:condition [ sh:property [ sh:path rdf:type ; sh:hasValue schema:Person ; sh:minCount 1 ] ; ] ; diff --git a/rocrate_validator/profiles/ro-crate/ontology.ttl b/rocrate_validator/profiles/ro-crate/ontology.ttl index 8dd30ab6..8e08e0cd 100644 --- a/rocrate_validator/profiles/ro-crate/ontology.ttl +++ b/rocrate_validator/profiles/ro-crate/ontology.ttl @@ -7,6 +7,7 @@ @prefix schema: . @prefix rocrate: . @prefix bioschemas: . +@prefix ro-crate: . # @base <./.> . rdf:type owl:Ontology ; @@ -17,7 +18,7 @@ # # ################################################################# # Declare the RootDataEntity class -rocrate:RootDataEntity rdf:type owl:Class ; +ro-crate:RootDataEntity rdf:type owl:Class ; rdfs:subClassOf schema:Dataset ; rdfs:label "RootDataEntity"@en . @@ -27,7 +28,7 @@ schema:CreativeWork rdf:type owl:Class ; ### http://schema.org/MediaObject schema:MediaObject rdf:type owl:Class ; - owl:equivalentClass rocrate:File ; + owl:equivalentClass ro-crate:File ; rdfs:label "MediaObject"@en . @@ -41,12 +42,12 @@ bioschemas:ComputationalWorkflow rdf:type owl:Class . ### https://w3id.org/ro/crate/1.1/DataEntity -rocrate:DataEntity rdf:type owl:Class ; +ro-crate:DataEntity rdf:type owl:Class ; rdfs:subClassOf schema:CreativeWork ; rdfs:label "DataEntity"@en . # # ### https://w3id.org/ro/crate/1.1/Directory -rocrate:Directory rdf:type owl:Class ; +ro-crate:Directory rdf:type owl:Class ; rdfs:subClassOf schema:Dataset ; rdfs:label "Directory"@en . 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 28cd96aa..82b2529a 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 @@ -5,7 +5,10 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . -@prefix rocrate: . +# @prefix rocrate: . + +@prefix validator: . +@prefix ro-crate: . ro:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; @@ -13,7 +16,7 @@ ro:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; sh:description """The Root Data Entity SHOULD have the properties `name`, `description` and `license` defined as described in the RO-Crate specification """; - sh:targetClass rocrate:RootDataEntity ; + sh:targetClass ro-crate:RootDataEntity ; sh:property [ a sh:PropertyShape ; sh:name "Root Data Entity: `name` property" ; diff --git a/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl index c7c1430a..cab1bc17 100644 --- a/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl @@ -1,5 +1,4 @@ @prefix ro: <./> . -@prefix rocrate: . @prefix rdf: . @prefix rdfs: . @prefix dct: . @@ -9,8 +8,11 @@ @prefix xsd: . @prefix owl: . -rocrate:FileRecommendedProperties a sh:NodeShape ; - sh:targetClass rocrate:File ; +@prefix validator: . +@prefix ro-crate: . + +ro-crate:FileRecommendedProperties a sh:NodeShape ; + sh:targetClass ro-crate:File ; sh:name "File Data Entity: RECOMMENDED properties"; sh:description """A `File` Data Entity SHOULD have detailed descriptions encodings through the `encodingFormat` property""" ; sh:property [ diff --git a/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl index 120f4820..abbf9f2f 100644 --- a/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl @@ -7,12 +7,16 @@ @prefix xsd: . @prefix owl: . @prefix rdfs: . -@prefix rocrate: . +# @prefix rocrate: . -rocrate:CreativeWorkAuthorMinimuRecommendedProperties a sh:NodeShape ; + +@prefix validator: . +@prefix ro-crate: . + +ro-crate:CreativeWorkAuthorMinimuRecommendedProperties 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 rocrate:CreativeWorkAuthor ; + sh:targetClass ro-crate:CreativeWorkAuthor ; sh:property [ sh:path schema:name ; sh:minCount 1 ; @@ -44,7 +48,7 @@ rocrate:CreativeWorkAuthorMinimuRecommendedProperties a sh:NodeShape ; ] . -rocrate:OrganizationRecommendedProperties a sh:NodeShape ; +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 ; diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl index 78df89ab..89814db9 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl @@ -5,15 +5,20 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . -@prefix rocrate: . +# @prefix rocrate: . @prefix bioschemas: . @prefix wroc: . +@prefix validator: . +@prefix ro-crate: . +@prefix workflow-ro-crate: . + + ro:MainWorkflowOptionalProperties a sh:NodeShape ; sh:name "Main Workflow optional properties" ; sh:description """Main Workflow properties defined as MAY"""; - sh:targetClass rocrate:MainWorkflow ; + sh:targetClass workflow-ro-crate:MainWorkflow ; sh:property [ a sh:PropertyShape ; sh:name "Main Workflow image" ; 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 333aed27..796a9674 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 @@ -5,15 +5,18 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . -@prefix rocrate: . +# @prefix rocrate: . @prefix bioschemas: . + @prefix validator: . +@prefix ro-crate: . +@prefix workflow-ro-crate: . ro:MainEntityProperyMustExist 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 rocrate:RootDataEntity ; + sh:targetClass ro-crate:RootDataEntity ; sh:property [ a sh:PropertyShape ; sh:path schema:mainEntity ; @@ -43,13 +46,13 @@ ro:FindMainWorkflow a sh:NodeShape, validator:HiddenShape ; a sh:TripleRule ; sh:subject sh:this ; sh:predicate rdf:type ; - sh:object rocrate:MainWorkflow ; + sh:object workflow-ro-crate:MainWorkflow ; ] . ro:MainWorkflowRequiredProperties a sh:NodeShape ; sh:name "Main Workflow definition" ; sh:description """Main Workflow properties defined as MUST"""; - sh:targetClass rocrate:MainWorkflow ; + sh:targetClass workflow-ro-crate:MainWorkflow ; sh:property [ a sh:PropertyShape ; sh:name "Main Workflow type" ; diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl b/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl index 6a420419..a256c75d 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl @@ -5,14 +5,18 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . -@prefix rocrate: . +# @prefix rocrate: . @prefix bioschemas: . +@prefix validator: . +@prefix ro-crate: . +@prefix workflow-ro-crate: . + ro:WROCRootDataEntityRequiredProperties a sh:NodeShape ; sh:name "WROC Root Data Entity Required Properties" ; sh:description """Root Data Entity properties defined as MUST""" ; - sh:targetClass rocrate:RootDataEntity ; + sh:targetClass ro-crate:RootDataEntity ; sh:property [ a sh:PropertyShape ; sh:name "Crate license" ; diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl index 26a05a23..8452d91a 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl @@ -5,9 +5,13 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . -@prefix rocrate: . +# @prefix rocrate: . @prefix bioschemas: . +@prefix validator: . +@prefix ro-crate: . +@prefix workflow-ro-crate: . + ro:DescriptorProperties a sh:NodeShape ; sh:name "WROC Metadata File Descriptor properties" ; @@ -33,7 +37,7 @@ ro:FindReadme a sh:NodeShape ; sh:name "README.md about" ; sh:description "The README.md SHOULD be about the crate" ; sh:path schema:about ; - sh:class rocrate:RootDataEntity ; + sh:class ro-crate:RootDataEntity ; sh:minCount 1 ; sh:message "The README.md SHOULD be about the crate" ; ] ; diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl index 04712d32..3f3bd120 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl @@ -5,14 +5,19 @@ @prefix sh: . @prefix xml1: . @prefix xsd: . -@prefix rocrate: . +# @prefix rocrate: . @prefix bioschemas: . +@prefix validator: . +@prefix ro-crate: . +@prefix workflow-ro-crate: . + + ro:MainWorkflowRecommendedProperties a sh:NodeShape ; sh:name "Main Workflow recommended properties" ; sh:description """Main Workflow properties defined as SHOULD"""; - sh:targetClass rocrate:MainWorkflow ; + sh:targetClass workflow-ro-crate:MainWorkflow ; sh:property [ a sh:PropertyShape ; sh:name "Main Workflow Bioschemas compliance" ; From 00238c190a6dbb0136cf961ef23c3c646669066d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Aug 2024 10:28:35 +0200 Subject: [PATCH 787/902] refactor(profiles/ro-crate): :recycle: use the local prefix to denote shapes of the ro-crate profile --- .../profiles/ro-crate/may/61_license_entity.ttl | 4 +++- .../ro-crate/must/1_file-descriptor_metadata.ttl | 4 ++-- .../ro-crate/must/2_root_data_entity_metadata.ttl | 6 +++--- .../ro-crate/must/4_data_entity_metadata.ttl | 12 ++++++------ .../ro-crate/must/5_web_data_entity_metadata.ttl | 4 ++-- .../ro-crate/should/2_root_data_entity_metadata.ttl | 2 +- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/may/61_license_entity.ttl b/rocrate_validator/profiles/ro-crate/may/61_license_entity.ttl index bf6687f7..6c4d54f9 100644 --- a/rocrate_validator/profiles/ro-crate/may/61_license_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/may/61_license_entity.ttl @@ -6,7 +6,9 @@ @prefix xml1: . @prefix xsd: . -ro:LicenseDefinition a sh:NodeShape ; +@prefix ro-crate: . + +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 ; diff --git a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index 2e93a675..dd5de55c 100644 --- a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -35,7 +35,7 @@ ro-crate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape, validator:Hidde sh:object ro-crate:ROCrateMetadataFileDescriptor ; ] . -ro:ROCrateMetadataFileDescriptorExistence +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 named `ro-crate-metadata.json` and typed as `schema:CreativeWork`" ; @@ -51,7 +51,7 @@ ro:ROCrateMetadataFileDescriptorExistence sh:message "The root of the document MUST have an entity with @id `ro-crate-metadata.json`" ; ] . -ro:ROCrateMetadataFileDescriptorRecommendedProperties a sh:NodeShape ; +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 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 56df5c89..2d463a29 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 @@ -10,7 +10,7 @@ @prefix ro-crate: . -ro:RootDataEntityType +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`)" ; @@ -76,7 +76,7 @@ ro-crate:FindRootDataEntity a sh:NodeShape, validator:HiddenShape; ] . -ro:RootDataEntityValueRestriction +ro-crate:RootDataEntityValueRestriction a sh:NodeShape ; sh:name "RO-Crate Root Data Entity value restriction" ; sh:description "The Root Data Entity MUST end with `/`" ; @@ -91,7 +91,7 @@ ro:RootDataEntityValueRestriction sh:pattern "/$" ; ] . -ro:RootDataEntityHasPartValueRestriction +ro-crate:RootDataEntityHasPartValueRestriction a sh:NodeShape ; sh:name "RO-Crate Root Data Entity: `hasPart` value restriction" ; sh:description "The Root Data Entity MUST be linked to the declared `File`, `Directory` and other types of instances through the `hasPart` property" ; diff --git a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl index 72cdb3cd..e1b627be 100644 --- a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl @@ -10,7 +10,7 @@ @prefix validator: . @prefix ro-crate: . -ro:DataEntityRequiredProperties a sh:NodeShape ; +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 sbsolute URI""" ; @@ -25,7 +25,7 @@ ro:DataEntityRequiredProperties a sh:NodeShape ; sh:message """Data Entities MUST have an absolute or relative URI as @id.""" ; ] . -ro:FileDataEntity a sh:NodeShape ; +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`. @@ -63,7 +63,7 @@ ro:FileDataEntity a sh:NodeShape ; ] . -ro:DirectoryDataEntity a sh:NodeShape ; +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. @@ -116,7 +116,7 @@ ro:DirectoryDataEntity a sh:NodeShape ; sh:severity sh:Violation ; ] . -ro:DataEntityRquiredPropertiesShape a sh:NodeShape ; +ro-crate:DataEntityRquiredPropertiesShape a sh:NodeShape ; sh:name "Data Entity: REQUIRED properties" ; sh:description """A `DataEntity` MUST be linked, either directly or inderectly, from the Root Data Entity""" ; sh:targetClass ro-crate:DataEntity ; @@ -131,7 +131,7 @@ ro:DataEntityRquiredPropertiesShape a sh:NodeShape ; # sh:message "A Data Entity MUST be directly or indirectly linked to the `Root Data Entity` through the `hasPart` property" ; ] . -ro:DirectoryDataEntityRequiredValueRestriction a sh:NodeShape ; +ro-crate:DirectoryDataEntityRequiredValueRestriction a sh:NodeShape ; sh:name "Directory Data Entity: REQUIRED value restriction" ; sh:description """A Directory Data Entity MUST end with `/`""" ; sh:targetNode ro-crate:Directory ; @@ -144,7 +144,7 @@ ro:DirectoryDataEntityRequiredValueRestriction a sh:NodeShape ; sh:pattern "/$" ; ] . -ro:GenericDataEntity a sh:NodeShape ; +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 [ diff --git a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl index 810b30d2..83c82acc 100644 --- a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl @@ -12,7 +12,7 @@ @prefix ro-crate: . -ro:WebBasedDataEntity a sh:NodeShape, validator:HiddenShape ; +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""" ; @@ -38,7 +38,7 @@ ro:WebBasedDataEntity a sh:NodeShape, validator:HiddenShape ; ] . -ro:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; +ro-crate:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; sh:name "Web-based Data Entity: RECOMMENDED properties" ; sh:description """A Web-based Data Entity MUST be identified by an absolute URL and SHOULD have a `contentSize` and `sdDatePublished` property""" ; 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 82b2529a..fb5e0078 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 @@ -11,7 +11,7 @@ @prefix ro-crate: . -ro:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; +ro-crate:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; sh:name "RO-Crate Root Data Entity RECOMMENDED properties" ; sh:description """The Root Data Entity SHOULD have the properties `name`, `description` and `license` defined as described From 48935da56c995d6edb6b96df2c37af3fbaa583e4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Aug 2024 10:34:39 +0200 Subject: [PATCH 788/902] refactor(profiles/workflow-ro-crate): :recycle: use the local prefix to denote shapes of the workflow-ro-crate profile --- .../workflow-ro-crate/may/0_main-workflow.ttl | 6 +++--- .../profiles/workflow-ro-crate/may/2_wrroc_crate.ttl | 11 +++++++++-- .../workflow-ro-crate/must/0_main-workflow.ttl | 6 +++--- .../must/1_wroc_root_data_entity.ttl | 2 +- .../workflow-ro-crate/should/1_wroc_crate.ttl | 4 ++-- .../workflow-ro-crate/should/2_main-workflow.ttl | 2 +- 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl index 89814db9..8bffddf0 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl @@ -15,7 +15,7 @@ @prefix workflow-ro-crate: . -ro:MainWorkflowOptionalProperties a sh:NodeShape ; +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 ; @@ -34,14 +34,14 @@ ro:MainWorkflowOptionalProperties a sh:NodeShape ; 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 ro:CWLDescriptionProperties ; + 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 ; ] . -ro:CWLDescriptionProperties a sh:NodeShape ; +workflow-ro-crate:CWLDescriptionProperties a sh:NodeShape ; sh:name "CWL Description properties" ; sh:description "Main Workflow CWL Description properties" ; sh:property [ diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl index 10834cab..1aa7b6e8 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl @@ -8,7 +8,14 @@ @prefix rocrate: . @prefix bioschemas: . -ro:FindTestDir a sh:NodeShape ; + +@prefix validator: . +@prefix ro-crate: . +@prefix workflow-ro-crate: . + + + +workflow-ro-crate:FindTestDir a sh:NodeShape ; sh:name "test directory" ; sh:description "Dataset data entity to hold tests" ; sh:targetNode ro:test\/ ; @@ -22,7 +29,7 @@ ro:FindTestDir a sh:NodeShape ; sh:message "The test/ dir should be a Dataset" ; ] . -ro:FindExamplesDir a sh:NodeShape ; +workflow-ro-crate:FindExamplesDir a sh:NodeShape ; sh:name "examples directory" ; sh:description "Dataset data entity to hold examples" ; sh:targetNode ro:examples\/ ; 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 796a9674..ed335f74 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 @@ -13,7 +13,7 @@ @prefix workflow-ro-crate: . -ro:MainEntityProperyMustExist a sh:NodeShape ; +workflow-ro-crate:MainEntityProperyMustExist 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 ; @@ -26,7 +26,7 @@ ro:MainEntityProperyMustExist a sh:NodeShape ; ] . -ro:FindMainWorkflow a sh:NodeShape, validator:HiddenShape ; +workflow-ro-crate:FindMainWorkflow a sh:NodeShape, validator:HiddenShape ; sh:name "Identify Main Workflow" ; sh:description "Identify the Main Workflow" ; sh:target [ @@ -49,7 +49,7 @@ ro:FindMainWorkflow a sh:NodeShape, validator:HiddenShape ; sh:object workflow-ro-crate:MainWorkflow ; ] . -ro:MainWorkflowRequiredProperties a sh:NodeShape ; +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 ; diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl b/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl index a256c75d..ed33ef50 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl @@ -13,7 +13,7 @@ @prefix workflow-ro-crate: . -ro:WROCRootDataEntityRequiredProperties a sh:NodeShape ; +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 ; diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl index 8452d91a..ddfce921 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl @@ -13,7 +13,7 @@ @prefix workflow-ro-crate: . -ro:DescriptorProperties a sh:NodeShape ; +workflow-ro-crate:DescriptorProperties a sh:NodeShape ; sh:name "WROC Metadata File Descriptor properties" ; sh:description "Properties of the WROC Metadata File Descriptor" ; sh:targetNode ro:ro-crate-metadata.json ; @@ -28,7 +28,7 @@ ro:DescriptorProperties a sh:NodeShape ; sh:message "The Metadata File Descriptor conformsTo SHOULD contain https://w3id.org/ro/crate/1.1 and https://w3id.org/workflowhub/workflow-ro-crate/1.0" ; ] . -ro:FindReadme a sh:NodeShape ; +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 ; diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl index 3f3bd120..74fe826a 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl @@ -14,7 +14,7 @@ -ro:MainWorkflowRecommendedProperties a sh:NodeShape ; +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 ; From 8d8fc229fa7714d2031e59766579aa47fa8b5884 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Aug 2024 10:48:28 +0200 Subject: [PATCH 789/902] refactor(profiles/process-run-crate): :recycle: use the local prefix to denote shapes of the process-run-crate profile --- .../process-run-crate/may/0_software-application.ttl | 8 +++++++- .../profiles/process-run-crate/may/1_create_action.ttl | 10 ++++++++-- .../process-run-crate/may/3_container_image.ttl | 8 +++++++- .../process-run-crate/must/1_create_action.ttl | 2 +- .../must/2_root_data_entity_metadata.ttl | 2 +- .../should/0_software-application.ttl | 10 +++++----- .../process-run-crate/should/1_create_action.ttl | 8 ++++---- .../profiles/process-run-crate/should/2_collection.ttl | 2 +- .../process-run-crate/should/3_container_image.ttl | 2 +- 9 files changed, 35 insertions(+), 17 deletions(-) diff --git a/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl b/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl index 2246a927..674a8527 100644 --- a/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl @@ -9,7 +9,13 @@ @prefix bioschemas: . -ro:ProcRCSoftwareApplicationOptional a sh:NodeShape ; +@prefix validator: . +@prefix ro-crate: . +@prefix process-run-crate: . + + + +process-run-crate:ProcRCSoftwareApplicationOptional a sh:NodeShape ; sh:name "ProcRC SoftwareApplication MAY" ; sh:description "Optional properties of a Process Run Crate SoftwareApplication" ; # Avoid performing checks on dependencies diff --git a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl index 79d08a4e..5b9f871a 100644 --- a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl @@ -9,7 +9,13 @@ @prefix bioschemas: . @prefix wfrun: . -ro:ProcRCActionOptional a sh:NodeShape ; + +@prefix validator: . +@prefix ro-crate: . +@prefix process-run-crate: . + + +process-run-crate:ProcRCActionOptional a sh:NodeShape ; sh:name "Process Run Crate Action MAY" ; sh:description "Recommended properties of the Process Run Crate Action" ; sh:targetClass schema:CreateAction , @@ -57,7 +63,7 @@ ro:ProcRCActionOptional a sh:NodeShape ; ] . -ro:ProcRCActionErrorMay a sh:NodeShape ; +process-run-crate:ProcRCActionErrorMay a sh:NodeShape ; sh:name "Process Run Crate Action MAY have error" ; sh:description "error MAY be specified if actionStatus is set to FailedActionStatus" ; sh:target [ diff --git a/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl b/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl index a3b19b40..3916506d 100644 --- a/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl @@ -9,7 +9,13 @@ @prefix bioschemas: . @prefix wfrun: . -ro:ProcRCContainerImageOptional a sh:NodeShape ; + +@prefix validator: . +@prefix ro-crate: . +@prefix process-run-crate: . + + +process-run-crate:ProcRCContainerImageOptional a sh:NodeShape ; sh:name "Process Run Crate ContainerImage MAY" ; sh:description "Optional properties of the Process Run Crate ContainerImage" ; sh:targetClass wfrun:ContainerImage ; diff --git a/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl index 0eb73403..b8832132 100644 --- a/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl @@ -12,7 +12,7 @@ @prefix process-run-crate: . -ro:ProcRCActionRequired a sh:NodeShape ; +process-run-crate:ProcRCActionRequired a sh:NodeShape ; sh:name "Process Run Crate Action" ; sh:description "Properties of the Process Run Crate Action" ; sh:targetClass schema:CreateAction , diff --git a/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl index d22d57d2..21430604 100644 --- a/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl @@ -12,7 +12,7 @@ @prefix process-run-crate: . -ro:ProcRCRootDataEntityMetadata a sh:NodeShape ; +process-run-crate:ProcRCRootDataEntityMetadata a sh:NodeShape ; sh:name "Root Data Entity Metadata" ; sh:description "Properties of the Root Data Entity" ; sh:targetClass ro-crate:RootDataEntity ; diff --git a/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl b/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl index 1c3ecc4d..bf0d6707 100644 --- a/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl @@ -14,7 +14,7 @@ -ro:ProcRCApplication a sh:NodeShape ; +process-run-crate:ProcRCApplication a sh:NodeShape ; sh:name "ProcRC Application" ; sh:description "Properties of a Process Run Crate Application" ; sh:targetClass schema:SoftwareApplication, @@ -38,7 +38,7 @@ ro:ProcRCApplication a sh:NodeShape ; ] . -ro:ProcRCSoftwareSourceCodeComputationalWorkflow a sh:NodeShape ; +process-run-crate:ProcRCSoftwareSourceCodeComputationalWorkflow a sh:NodeShape ; sh:name "ProcRC SoftwareSourceCode or ComputationalWorkflow" ; sh:description "Properties of a Process Run Crate SoftwareSourceCode or ComputationalWorkflow" ; sh:targetClass schema:SoftwareSourceCode, @@ -53,7 +53,7 @@ ro:ProcRCSoftwareSourceCodeComputationalWorkflow a sh:NodeShape ; ] . -ro:ProcRCSoftwareApplication a sh:NodeShape ; +process-run-crate:ProcRCSoftwareApplication a sh:NodeShape ; sh:name "ProcRC SoftwareApplication" ; sh:description "Properties of a Process Run Crate SoftwareApplication" ; sh:targetClass schema:SoftwareApplication ; @@ -70,7 +70,7 @@ ro:ProcRCSoftwareApplication a sh:NodeShape ; ] . -ro:ProcRCSoftwareApplicationSingleVersion a sh:NodeShape ; +process-run-crate:ProcRCSoftwareApplicationSingleVersion a sh:NodeShape ; sh:name "ProcRC SoftwareApplication SingleVersion" ; sh:description "Process Run Crate SoftwareApplication should not have both version and softwareVersion" ; sh:message "Process Run Crate SoftwareApplication should not have both version and softwareVersion" ; @@ -91,7 +91,7 @@ ro:ProcRCSoftwareApplicationSingleVersion a sh:NodeShape ; ] . -ro:ProcRCSoftwareApplicationID a sh:NodeShape ; +process-run-crate:ProcRCSoftwareApplicationID a sh:NodeShape ; sh:name "ProcRC SoftwareApplication ID" ; sh:description "Process Run Crate SoftwareApplication ID" ; sh:targetNode schema:SoftwareApplication , diff --git a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl index 07aa4f08..5633da7a 100644 --- a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl @@ -14,7 +14,7 @@ @prefix process-run-crate: . -ro:ProcRCActionRecommended a sh:NodeShape ; +process-run-crate:ProcRCActionRecommended a sh:NodeShape ; sh:name "Process Run Crate Action SHOULD" ; sh:description "Recommended properties of the Process Run Crate Action" ; sh:targetClass schema:CreateAction , @@ -106,7 +106,7 @@ ro:ProcRCActionRecommended a sh:NodeShape ; ] . -ro:ProcRCCreateUpdateActionRecommended a sh:NodeShape ; +process-run-crate:ProcRCCreateUpdateActionRecommended a sh:NodeShape ; sh:name "Process Run Crate CreateAction UpdateAction SHOULD" ; sh:description "Recommended properties of the Process Run Crate CreateAction or UpdateAction" ; sh:targetClass schema:CreateAction , @@ -121,7 +121,7 @@ ro:ProcRCCreateUpdateActionRecommended a sh:NodeShape ; ] . -ro:ProcRCActionError a sh:NodeShape ; +process-run-crate:ProcRCActionError a sh:NodeShape ; sh:name "Process Run Crate Action error" ; sh:description "error SHOULD NOT be specified unless actionStatus is set to FailedActionStatus" ; sh:message "error SHOULD NOT be specified unless actionStatus is set to FailedActionStatus" ; @@ -147,7 +147,7 @@ ro:ProcRCActionError a sh:NodeShape ; ] . -ro:ProcRCActionObjectResultType a sh:NodeShape ; +process-run-crate:ProcRCActionObjectResultType a sh:NodeShape ; sh:name "Process Run Crate Action object and result types" ; sh:description "object and result SHOULD point to entities of type MediaObject, Dataset, Collection, CreativeWork or PropertyValue" ; sh:targetClass schema:CreateAction , diff --git a/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl b/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl index 47b025eb..9e317e1f 100644 --- a/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl @@ -13,7 +13,7 @@ @prefix process-run-crate: . -ro:ProcRCCollectionRecommended a sh:NodeShape ; +process-run-crate:ProcRCCollectionRecommended a sh:NodeShape ; sh:name "Process Run Crate Collection SHOULD" ; sh:description "Recommended properties of the Process Run Crate Collection" ; sh:target [ diff --git a/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl b/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl index a7d2d833..893778fe 100644 --- a/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl @@ -14,7 +14,7 @@ @prefix process-run-crate: . -ro:ProcRCContainerImageRecommended a sh:NodeShape ; +process-run-crate:ProcRCContainerImageRecommended a sh:NodeShape ; sh:name "Process Run Crate ContainerImage SHOULD" ; sh:description "Recommended properties of the Process Run Crate ContainerImage" ; sh:targetClass wfrun:ContainerImage ; From 533cd713af3d197a30cd384a378e1ec7a13d887e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Aug 2024 11:30:41 +0200 Subject: [PATCH 790/902] refactor(profiles): :wastebasket: remove unused prefixes --- .../may/0_software-application.ttl | 14 ++------------ .../process-run-crate/may/1_create_action.ttl | 15 +++------------ .../process-run-crate/may/3_container_image.ttl | 14 ++------------ .../process-run-crate/must/1_create_action.ttl | 11 ++--------- .../must/2_root_data_entity_metadata.ttl | 11 ++--------- .../should/0_software-application.ttl | 16 +++------------- .../process-run-crate/should/1_create_action.ttl | 14 +++----------- .../process-run-crate/should/2_collection.ttl | 13 +++---------- .../should/3_container_image.ttl | 14 +++----------- .../ro-crate/may/4_data_entity_metadata.ttl | 9 +-------- .../profiles/ro-crate/may/61_license_entity.ttl | 8 ++------ .../ro-crate/must/1_file-descriptor_metadata.ttl | 5 +---- .../must/2_root_data_entity_metadata.ttl | 6 +----- .../ro-crate/must/4_data_entity_metadata.ttl | 6 +----- .../ro-crate/must/5_web_data_entity_metadata.ttl | 9 +++------ .../ro-crate/must/6_contextual_entity.ttl | 6 +----- rocrate_validator/profiles/ro-crate/prefixes.ttl | 4 ---- .../should/2_root_data_entity_metadata.ttl | 8 +------- .../ro-crate/should/4_data_entity_metadata.ttl | 8 +------- .../should/6_contextual_entity_metadata.ttl | 11 +---------- .../workflow-ro-crate/may/0_main-workflow.ttl | 15 +++------------ .../workflow-ro-crate/may/2_wrroc_crate.ttl | 15 +++------------ .../workflow-ro-crate/must/0_main-workflow.ttl | 13 +++---------- .../must/1_wroc_root_data_entity.ttl | 12 +++--------- .../workflow-ro-crate/should/1_wroc_crate.ttl | 13 +++---------- .../workflow-ro-crate/should/2_main-workflow.ttl | 13 ++----------- 26 files changed, 53 insertions(+), 230 deletions(-) diff --git a/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl b/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl index 674a8527..7419adb1 100644 --- a/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl @@ -1,20 +1,10 @@ @prefix ro: <./> . -@prefix dct: . -@prefix rdf: . +@prefix ro-crate: . +@prefix process-run-crate: . @prefix schema: . @prefix sh: . -@prefix xml1: . -@prefix xsd: . -# @prefix rocrate: . @prefix bioschemas: . - -@prefix validator: . -@prefix ro-crate: . -@prefix process-run-crate: . - - - process-run-crate:ProcRCSoftwareApplicationOptional a sh:NodeShape ; sh:name "ProcRC SoftwareApplication MAY" ; sh:description "Optional properties of a Process Run Crate SoftwareApplication" ; diff --git a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl index 5b9f871a..3095ab8c 100644 --- a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl @@ -1,20 +1,11 @@ @prefix ro: <./> . -@prefix dct: . -@prefix rdf: . +@prefix ro-crate: . +@prefix process-run-crate: . @prefix schema: . -@prefix sh: . -@prefix xml1: . -@prefix xsd: . -# @prefix rocrate: . @prefix bioschemas: . +@prefix sh: . @prefix wfrun: . - -@prefix validator: . -@prefix ro-crate: . -@prefix process-run-crate: . - - process-run-crate:ProcRCActionOptional a sh:NodeShape ; sh:name "Process Run Crate Action MAY" ; sh:description "Recommended properties of the Process Run Crate Action" ; diff --git a/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl b/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl index 3916506d..b6d8ba50 100644 --- a/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl @@ -1,20 +1,10 @@ @prefix ro: <./> . -@prefix dct: . -@prefix rdf: . -@prefix schema: . +@prefix ro-crate: . +@prefix process-run-crate: . @prefix sh: . -@prefix xml1: . -@prefix xsd: . -# @prefix rocrate: . @prefix bioschemas: . @prefix wfrun: . - -@prefix validator: . -@prefix ro-crate: . -@prefix process-run-crate: . - - process-run-crate:ProcRCContainerImageOptional a sh:NodeShape ; sh:name "Process Run Crate ContainerImage MAY" ; sh:description "Optional properties of the Process Run Crate ContainerImage" ; diff --git a/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl index b8832132..553212b2 100644 --- a/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl @@ -1,17 +1,10 @@ @prefix ro: <./> . -@prefix dct: . -@prefix rdf: . +@prefix ro-crate: . +@prefix process-run-crate: . @prefix schema: . @prefix sh: . -@prefix xml1: . -@prefix xsd: . @prefix bioschemas: . -@prefix validator: . -@prefix ro-crate: . -@prefix process-run-crate: . - - process-run-crate:ProcRCActionRequired a sh:NodeShape ; sh:name "Process Run Crate Action" ; sh:description "Properties of the Process Run Crate Action" ; diff --git a/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl index 21430604..04de32a1 100644 --- a/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl @@ -1,16 +1,9 @@ @prefix ro: <./> . +@prefix ro-crate: . +@prefix process-run-crate: . @prefix dct: . -@prefix rdf: . @prefix schema: . @prefix sh: . -@prefix xml1: . -@prefix xsd: . -# @prefix rocrate: . - -@prefix validator: . -@prefix ro-crate: . -@prefix process-run-crate: . - process-run-crate:ProcRCRootDataEntityMetadata a sh:NodeShape ; sh:name "Root Data Entity Metadata" ; diff --git a/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl b/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl index bf0d6707..cbe731d0 100644 --- a/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl @@ -1,18 +1,10 @@ @prefix ro: <./> . -@prefix dct: . +@prefix ro-crate: . +@prefix process-run-crate: . @prefix rdf: . @prefix schema: . -@prefix sh: . -@prefix xml1: . -@prefix xsd: . -# @prefix rocrate: . @prefix bioschemas: . - -@prefix validator: . -@prefix ro-crate: . -@prefix process-run-crate: . - - +@prefix sh: . process-run-crate:ProcRCApplication a sh:NodeShape ; sh:name "ProcRC Application" ; @@ -37,7 +29,6 @@ process-run-crate:ProcRCApplication a sh:NodeShape ; sh:message "The Application SHOULD have a url" ; ] . - process-run-crate:ProcRCSoftwareSourceCodeComputationalWorkflow a sh:NodeShape ; sh:name "ProcRC SoftwareSourceCode or ComputationalWorkflow" ; sh:description "Properties of a Process Run Crate SoftwareSourceCode or ComputationalWorkflow" ; @@ -52,7 +43,6 @@ process-run-crate:ProcRCSoftwareSourceCodeComputationalWorkflow a sh:NodeShape ; sh:message "The SoftwareSourceCode or ComputationalWorkflow SHOULD have a version" ; ] . - process-run-crate:ProcRCSoftwareApplication a sh:NodeShape ; sh:name "ProcRC SoftwareApplication" ; sh:description "Properties of a Process Run Crate SoftwareApplication" ; diff --git a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl index 5633da7a..827cb0df 100644 --- a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl @@ -1,19 +1,11 @@ @prefix ro: <./> . -@prefix dct: . -@prefix rdf: . +@prefix ro-crate: . +@prefix process-run-crate: . @prefix schema: . -@prefix sh: . -@prefix xml1: . -@prefix xsd: . -# @prefix rocrate: . @prefix bioschemas: . +@prefix sh: . @prefix wfrun: . -@prefix validator: . -@prefix ro-crate: . -@prefix process-run-crate: . - - process-run-crate:ProcRCActionRecommended a sh:NodeShape ; sh:name "Process Run Crate Action SHOULD" ; sh:description "Recommended properties of the Process Run Crate Action" ; diff --git a/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl b/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl index 9e317e1f..ce2adacb 100644 --- a/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl @@ -1,17 +1,10 @@ @prefix ro: <./> . -@prefix dct: . -@prefix rdf: . +@prefix ro-crate: . +@prefix process-run-crate: . @prefix schema: . -@prefix sh: . -@prefix xml1: . -@prefix xsd: . -# @prefix rocrate: . @prefix bioschemas: . - +@prefix sh: . @prefix validator: . -@prefix ro-crate: . -@prefix process-run-crate: . - process-run-crate:ProcRCCollectionRecommended a sh:NodeShape ; sh:name "Process Run Crate Collection SHOULD" ; diff --git a/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl b/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl index 893778fe..3100c769 100644 --- a/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl @@ -1,19 +1,11 @@ @prefix ro: <./> . -@prefix dct: . -@prefix rdf: . +@prefix ro-crate: . +@prefix process-run-crate: . @prefix schema: . -@prefix sh: . -@prefix xml1: . -@prefix xsd: . -# @prefix rocrate: . @prefix bioschemas: . +@prefix sh: . @prefix wfrun: . -@prefix validator: . -@prefix ro-crate: . -@prefix process-run-crate: . - - process-run-crate:ProcRCContainerImageRecommended a sh:NodeShape ; sh:name "Process Run Crate ContainerImage SHOULD" ; sh:description "Recommended properties of the Process Run Crate ContainerImage" ; diff --git a/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl index 8c7c23a4..39e561ea 100644 --- a/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl @@ -1,18 +1,11 @@ @prefix ro: <./> . -@prefix dct: . +@prefix ro-crate: . @prefix rdf: . -@prefix schema_org: . @prefix sh: . -@prefix xml1: . @prefix xsd: . @prefix owl: . -@prefix rdfs: . - @prefix schema: . - @prefix validator: . -@prefix ro-crate: . - ro-crate:FileDataEntityWebOptionalProperties a sh:NodeShape ; sh:name "File Data Entity with web presence: OPTIONAL properties" ; diff --git a/rocrate_validator/profiles/ro-crate/may/61_license_entity.ttl b/rocrate_validator/profiles/ro-crate/may/61_license_entity.ttl index 6c4d54f9..4a4f5962 100644 --- a/rocrate_validator/profiles/ro-crate/may/61_license_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/may/61_license_entity.ttl @@ -1,12 +1,8 @@ @prefix ro: <./> . -@prefix dct: . +@prefix ro-crate: . @prefix rdf: . @prefix schema_org: . @prefix sh: . -@prefix xml1: . -@prefix xsd: . - -@prefix ro-crate: . ro-crate:LicenseDefinition a sh:NodeShape ; sh:name "License definition" ; @@ -32,4 +28,4 @@ ro-crate:LicenseDefinition a sh:NodeShape ; sh:path schema_org:description ; sh:message "Missing license description" ; ] . - + diff --git a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index dd5de55c..1a3d2896 100644 --- a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -1,13 +1,10 @@ @prefix ro: <./> . +@prefix ro-crate: . @prefix dct: . @prefix rdf: . @prefix schema_org: . @prefix sh: . -@prefix xml1: . -@prefix xsd: . -@prefix rocrate: . @prefix validator: . -@prefix ro-crate: . ro-crate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape, validator:HiddenShape; 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 2d463a29..1f1afdff 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 @@ -1,13 +1,9 @@ @prefix ro: <./> . -@prefix dct: . +@prefix ro-crate: . @prefix rdf: . @prefix schema_org: . @prefix sh: . -@prefix xml1: . -@prefix xsd: . -@prefix rocrate: . @prefix validator: . -@prefix ro-crate: . ro-crate:RootDataEntityType diff --git a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl index e1b627be..c1af3d27 100644 --- a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl @@ -1,14 +1,10 @@ @prefix ro: <./> . -@prefix dct: . +@prefix ro-crate: . @prefix rdf: . @prefix schema_org: . @prefix sh: . -@prefix xml1: . -@prefix xsd: . @prefix owl: . -@prefix rdfs: . @prefix validator: . -@prefix ro-crate: . ro-crate:DataEntityRequiredProperties a sh:NodeShape ; sh:name "Data Entity: REQUIRED properties" ; diff --git a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl index 83c82acc..b8f9cfd2 100644 --- a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl @@ -1,15 +1,12 @@ @prefix ro: <./> . -@prefix dct: . +@prefix ro-crate: . @prefix rdf: . +@prefix dct: . @prefix schema_org: . @prefix sh: . -@prefix xml1: . -@prefix xsd: . @prefix owl: . -@prefix rdfs: . -@prefix rocrate: . +@prefix xsd: . @prefix validator: . -@prefix ro-crate: . ro-crate:WebBasedDataEntity a sh:NodeShape, validator:HiddenShape ; diff --git a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl index 35392f11..d183c684 100644 --- a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl @@ -1,15 +1,11 @@ @prefix ro: <./> . -@prefix dct: . +@prefix ro-crate: . @prefix rdf: . @prefix schema: . @prefix sh: . -@prefix xml1: . @prefix xsd: . @prefix owl: . -@prefix rdfs: . -@prefix rocrate: . @prefix validator: . -@prefix ro-crate: . ro-crate:FindLicenseEntity a sh:NodeShape, validator:HiddenShape ; diff --git a/rocrate_validator/profiles/ro-crate/prefixes.ttl b/rocrate_validator/profiles/ro-crate/prefixes.ttl index 111eadaf..77d97015 100644 --- a/rocrate_validator/profiles/ro-crate/prefixes.ttl +++ b/rocrate_validator/profiles/ro-crate/prefixes.ttl @@ -1,9 +1,5 @@ @prefix ro: <./> . -@prefix dct: . -@prefix rdf: . -@prefix schema_org: . @prefix sh: . -@prefix xml1: . @prefix xsd: . @prefix owl: . 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 fb5e0078..73d5386b 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 @@ -1,15 +1,9 @@ @prefix ro: <./> . -@prefix dct: . +@prefix ro-crate: . @prefix rdf: . @prefix schema_org: . @prefix sh: . -@prefix xml1: . -@prefix xsd: . -# @prefix rocrate: . - @prefix validator: . -@prefix ro-crate: . - ro-crate:RootDataEntityDirectRecommendedProperties a sh:NodeShape ; sh:name "RO-Crate Root Data Entity RECOMMENDED properties" ; diff --git a/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl index cab1bc17..f8f78dea 100644 --- a/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl @@ -1,15 +1,9 @@ @prefix ro: <./> . +@prefix ro-crate: . @prefix rdf: . -@prefix rdfs: . -@prefix dct: . @prefix schema_org: . @prefix sh: . -@prefix xml: . @prefix xsd: . -@prefix owl: . - -@prefix validator: . -@prefix ro-crate: . ro-crate:FileRecommendedProperties a sh:NodeShape ; sh:targetClass ro-crate:File ; diff --git a/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl index abbf9f2f..cf9e9b13 100644 --- a/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl @@ -1,17 +1,8 @@ @prefix ro: <./> . -@prefix dct: . -@prefix rdf: . +@prefix ro-crate: . @prefix schema: . @prefix sh: . -@prefix xml1: . @prefix xsd: . -@prefix owl: . -@prefix rdfs: . -# @prefix rocrate: . - - -@prefix validator: . -@prefix ro-crate: . ro-crate:CreativeWorkAuthorMinimuRecommendedProperties a sh:NodeShape ; sh:name "CreativeWork Author: minimum RECOMMENDED properties" ; diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl index 8bffddf0..fb02e9a4 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl @@ -1,20 +1,12 @@ @prefix ro: <./> . -@prefix dct: . +@prefix ro-crate: . +@prefix workflow-ro-crate: . @prefix rdf: . @prefix schema: . -@prefix sh: . -@prefix xml1: . -@prefix xsd: . -# @prefix rocrate: . @prefix bioschemas: . +@prefix sh: . @prefix wroc: . - -@prefix validator: . -@prefix ro-crate: . -@prefix workflow-ro-crate: . - - workflow-ro-crate:MainWorkflowOptionalProperties a sh:NodeShape ; sh:name "Main Workflow optional properties" ; sh:description """Main Workflow properties defined as MAY"""; @@ -40,7 +32,6 @@ workflow-ro-crate:MainWorkflowOptionalProperties a sh:NodeShape ; # sh:severity sh:Info ; ] . - workflow-ro-crate:CWLDescriptionProperties a sh:NodeShape ; sh:name "CWL Description properties" ; sh:description "Main Workflow CWL Description properties" ; diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl index 1aa7b6e8..57740ca7 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl @@ -1,19 +1,10 @@ @prefix ro: <./> . -@prefix dct: . +@prefix ro-crate: . +@prefix workflow-ro-crate: . @prefix rdf: . @prefix schema: . -@prefix sh: . -@prefix xml1: . -@prefix xsd: . -@prefix rocrate: . @prefix bioschemas: . - - -@prefix validator: . -@prefix ro-crate: . -@prefix workflow-ro-crate: . - - +@prefix sh: . workflow-ro-crate:FindTestDir a sh:NodeShape ; sh:name "test directory" ; 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 ed335f74..a6751b02 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 @@ -1,17 +1,11 @@ @prefix ro: <./> . -@prefix dct: . +@prefix ro-crate: . +@prefix workflow-ro-crate: . +@prefix bioschemas: . @prefix rdf: . @prefix schema: . @prefix sh: . -@prefix xml1: . -@prefix xsd: . -# @prefix rocrate: . -@prefix bioschemas: . - @prefix validator: . -@prefix ro-crate: . -@prefix workflow-ro-crate: . - workflow-ro-crate:MainEntityProperyMustExist a sh:NodeShape ; sh:name "Main Workflow entity existence" ; @@ -25,7 +19,6 @@ workflow-ro-crate:MainEntityProperyMustExist a sh:NodeShape ; 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" ; diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl b/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl index ed33ef50..341fd5d6 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl @@ -1,17 +1,11 @@ @prefix ro: <./> . -@prefix dct: . -@prefix rdf: . +@prefix ro-crate: . +@prefix workflow-ro-crate: . @prefix schema: . +@prefix bioschemas: . @prefix sh: . -@prefix xml1: . @prefix xsd: . -# @prefix rocrate: . -@prefix bioschemas: . - @prefix validator: . -@prefix ro-crate: . -@prefix workflow-ro-crate: . - workflow-ro-crate:WROCRootDataEntityRequiredProperties a sh:NodeShape ; sh:name "WROC Root Data Entity Required Properties" ; diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl index ddfce921..c964a05f 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl @@ -1,17 +1,10 @@ @prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-ro-crate: . @prefix dct: . -@prefix rdf: . +@prefix bioschemas: . @prefix schema: . @prefix sh: . -@prefix xml1: . -@prefix xsd: . -# @prefix rocrate: . -@prefix bioschemas: . - -@prefix validator: . -@prefix ro-crate: . -@prefix workflow-ro-crate: . - workflow-ro-crate:DescriptorProperties a sh:NodeShape ; sh:name "WROC Metadata File Descriptor properties" ; diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl index 74fe826a..89172089 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl @@ -1,19 +1,10 @@ @prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-ro-crate: . @prefix dct: . -@prefix rdf: . -@prefix schema: . @prefix sh: . -@prefix xml1: . -@prefix xsd: . -# @prefix rocrate: . @prefix bioschemas: . -@prefix validator: . -@prefix ro-crate: . -@prefix workflow-ro-crate: . - - - workflow-ro-crate:MainWorkflowRecommendedProperties a sh:NodeShape ; sh:name "Main Workflow recommended properties" ; sh:description """Main Workflow properties defined as SHOULD"""; From 069f7a2304f738dac98c1c6b6b4ce21b1db90ed9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Aug 2024 11:39:29 +0200 Subject: [PATCH 791/902] refactor(profiles): :recycle: use local prefix to declare SPARQL prefixes --- .../process-run-crate/may/0_software-application.ttl | 2 +- .../profiles/process-run-crate/may/1_create_action.ttl | 2 +- .../profiles/process-run-crate/should/1_create_action.ttl | 2 +- .../profiles/process-run-crate/should/2_collection.ttl | 2 +- .../profiles/ro-crate/must/1_file-descriptor_metadata.ttl | 2 +- .../profiles/ro-crate/must/2_root_data_entity_metadata.ttl | 4 ++-- .../profiles/ro-crate/must/4_data_entity_metadata.ttl | 6 +++--- .../profiles/ro-crate/must/5_web_data_entity_metadata.ttl | 2 +- .../profiles/ro-crate/must/6_contextual_entity.ttl | 2 +- rocrate_validator/profiles/ro-crate/prefixes.ttl | 4 ++-- .../profiles/workflow-ro-crate/must/0_main-workflow.ttl | 2 +- 11 files changed, 15 insertions(+), 15 deletions(-) diff --git a/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl b/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl index 7419adb1..9005edc0 100644 --- a/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl @@ -11,7 +11,7 @@ process-run-crate:ProcRCSoftwareApplicationOptional a sh:NodeShape ; # Avoid performing checks on dependencies sh:target [ a sh:SPARQLTarget ; - sh:prefixes ro:sparqlPrefixes ; + sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ SELECT ?this WHERE { diff --git a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl index 3095ab8c..57bd21e0 100644 --- a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl @@ -59,7 +59,7 @@ process-run-crate:ProcRCActionErrorMay a sh:NodeShape ; sh:description "error MAY be specified if actionStatus is set to FailedActionStatus" ; sh:target [ a sh:SPARQLTarget ; - sh:prefixes ro:sparqlPrefixes ; + sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ SELECT ?this WHERE { diff --git a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl index 827cb0df..e7ce5aa4 100644 --- a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl @@ -119,7 +119,7 @@ process-run-crate:ProcRCActionError a sh:NodeShape ; sh:message "error SHOULD NOT be specified unless actionStatus is set to FailedActionStatus" ; sh:target [ a sh:SPARQLTarget ; - sh:prefixes ro:sparqlPrefixes ; + sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ SELECT ?this WHERE { diff --git a/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl b/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl index ce2adacb..38f63916 100644 --- a/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl @@ -11,7 +11,7 @@ process-run-crate:ProcRCCollectionRecommended a sh:NodeShape ; sh:description "Recommended properties of the Process Run Crate Collection" ; sh:target [ a sh:SPARQLTarget ; - sh:prefixes ro:sparqlPrefixes ; + sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ SELECT ?this WHERE { diff --git a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index 1a3d2896..4813f086 100644 --- a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -14,7 +14,7 @@ ro-crate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape, validator:Hidde available at [Finding RO-Crate Root in RDF triple stores](https://www.researchobject.org/ro-crate/1.1/appendix/relative-uris.html#finding-ro-crate-root-in-rdf-triple-stores).""" ; sh:target [ a sh:SPARQLTarget ; - sh:prefixes ro:sparqlPrefixes ; + sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ SELECT ?this WHERE { 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 1f1afdff..c5869266 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 @@ -12,7 +12,7 @@ ro-crate:RootDataEntityType sh:description "The Root Data Entity MUST be a `Dataset` (as per `schema.org`)" ; sh:target [ a sh:SPARQLTarget ; - sh:prefixes ro:sparqlPrefixes ; + sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ SELECT ?this WHERE { @@ -52,7 +52,7 @@ ro-crate:FindRootDataEntity a sh:NodeShape, validator:HiddenShape; """ ; sh:target [ a sh:SPARQLTarget ; - sh:prefixes ro:sparqlPrefixes ; + sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ SELECT ?this WHERE { diff --git a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl index c1af3d27..08d061da 100644 --- a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl @@ -29,7 +29,7 @@ ro-crate:FileDataEntity a sh:NodeShape ; """ ; sh:target [ a sh:SPARQLTarget ; - sh:prefixes ro:sparqlPrefixes ; + sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ SELECT ?this WHERE { @@ -66,7 +66,7 @@ ro-crate:DirectoryDataEntity a sh:NodeShape ; """ ; sh:target [ a sh:SPARQLTarget ; - sh:prefixes ro:sparqlPrefixes ; + sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ SELECT ?this WHERE { @@ -145,7 +145,7 @@ ro-crate:GenericDataEntityRequiredProperties a sh:NodeShape ; sh:description """A Data Entity other than a File or a Directory MUST be a `DataEntity`""" ; sh:target [ a sh:SPARQLTarget ; - sh:prefixes ro:sparqlPrefixes ; + sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ SELECT ?this WHERE { diff --git a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl index b8f9cfd2..3c761816 100644 --- a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl @@ -15,7 +15,7 @@ ro-crate:WebBasedDataEntity a sh:NodeShape, validator:HiddenShape ; sh:target [ a sh:SPARQLTarget ; - sh:prefixes ro:sparqlPrefixes ; + sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ SELECT ?this WHERE { diff --git a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl index d183c684..13755c55 100644 --- a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl @@ -13,7 +13,7 @@ ro-crate:FindLicenseEntity a sh:NodeShape, validator:HiddenShape ; sh:description """Mark a license entity any Data Entity referenced by the `schema:licence` property.""" ; sh:target [ a sh:SPARQLTarget ; - sh:prefixes ro:sparqlPrefixes ; + sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ SELECT ?this WHERE { diff --git a/rocrate_validator/profiles/ro-crate/prefixes.ttl b/rocrate_validator/profiles/ro-crate/prefixes.ttl index 77d97015..02e26ef1 100644 --- a/rocrate_validator/profiles/ro-crate/prefixes.ttl +++ b/rocrate_validator/profiles/ro-crate/prefixes.ttl @@ -1,10 +1,10 @@ @prefix ro: <./> . @prefix sh: . @prefix xsd: . -@prefix owl: . +@prefix ro-crate: . # Define the prefixes used in the SPARQL queries -ro:sparqlPrefixes +ro-crate:sparqlPrefixes sh:declare [ sh:prefix "schema" ; sh:namespace "http://schema.org/"^^xsd:anyURI ; 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 a6751b02..4decb8d5 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 @@ -24,7 +24,7 @@ workflow-ro-crate:FindMainWorkflow a sh:NodeShape, validator:HiddenShape ; sh:description "Identify the Main Workflow" ; sh:target [ a sh:SPARQLTarget ; - sh:prefixes ro:sparqlPrefixes ; + sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ SELECT ?this WHERE { From 9662b137bebecd92dbaa6ca50834fe29e4b37f82 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Aug 2024 11:47:41 +0200 Subject: [PATCH 792/902] chore(profiles): :memo: add copyright notice --- .../may/0_software-application.ttl | 14 ++++++++++++++ .../process-run-crate/may/1_create_action.ttl | 14 ++++++++++++++ .../process-run-crate/may/3_container_image.ttl | 14 ++++++++++++++ .../process-run-crate/must/1_create_action.ttl | 14 ++++++++++++++ .../must/2_root_data_entity_metadata.ttl | 14 ++++++++++++++ .../profiles/process-run-crate/profile.ttl | 14 ++++++++++++++ .../should/0_software-application.ttl | 14 ++++++++++++++ .../process-run-crate/should/1_create_action.ttl | 14 ++++++++++++++ .../process-run-crate/should/2_collection.ttl | 14 ++++++++++++++ .../process-run-crate/should/3_container_image.ttl | 14 ++++++++++++++ .../ro-crate/may/4_data_entity_metadata.ttl | 14 ++++++++++++++ .../profiles/ro-crate/may/61_license_entity.ttl | 14 ++++++++++++++ .../ro-crate/must/1_file-descriptor_metadata.ttl | 14 ++++++++++++++ .../ro-crate/must/2_root_data_entity_metadata.ttl | 14 ++++++++++++++ .../ro-crate/must/4_data_entity_metadata.ttl | 14 ++++++++++++++ .../ro-crate/must/5_web_data_entity_metadata.ttl | 14 ++++++++++++++ .../profiles/ro-crate/must/6_contextual_entity.ttl | 14 ++++++++++++++ rocrate_validator/profiles/ro-crate/ontology.ttl | 14 ++++++++++++++ rocrate_validator/profiles/ro-crate/prefixes.ttl | 14 ++++++++++++++ rocrate_validator/profiles/ro-crate/profile.ttl | 14 ++++++++++++++ .../should/2_root_data_entity_metadata.ttl | 14 ++++++++++++++ .../ro-crate/should/4_data_entity_metadata.ttl | 14 ++++++++++++++ .../should/6_contextual_entity_metadata.ttl | 14 ++++++++++++++ .../workflow-ro-crate/may/0_main-workflow.ttl | 14 ++++++++++++++ .../workflow-ro-crate/may/2_wrroc_crate.ttl | 14 ++++++++++++++ .../workflow-ro-crate/must/0_main-workflow.ttl | 14 ++++++++++++++ .../must/1_wroc_root_data_entity.ttl | 14 ++++++++++++++ .../profiles/workflow-ro-crate/profile.ttl | 14 ++++++++++++++ .../workflow-ro-crate/should/1_wroc_crate.ttl | 14 ++++++++++++++ .../workflow-ro-crate/should/2_main-workflow.ttl | 14 ++++++++++++++ 30 files changed, 420 insertions(+) diff --git a/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl b/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl index 9005edc0..460c35fa 100644 --- a/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/0_software-application.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix process-run-crate: . diff --git a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl index 57bd21e0..b3ed6db9 100644 --- a/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/1_create_action.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix process-run-crate: . diff --git a/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl b/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl index b6d8ba50..108afa72 100644 --- a/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl +++ b/rocrate_validator/profiles/process-run-crate/may/3_container_image.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix process-run-crate: . diff --git a/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl index 553212b2..bb59410e 100644 --- a/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/must/1_create_action.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix process-run-crate: . diff --git a/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl index 04de32a1..c42413c9 100644 --- a/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/process-run-crate/must/2_root_data_entity_metadata.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix process-run-crate: . diff --git a/rocrate_validator/profiles/process-run-crate/profile.ttl b/rocrate_validator/profiles/process-run-crate/profile.ttl index 45d13ad2..29f984fa 100644 --- a/rocrate_validator/profiles/process-run-crate/profile.ttl +++ b/rocrate_validator/profiles/process-run-crate/profile.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix dct: . @prefix prof: . @prefix role: . diff --git a/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl b/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl index cbe731d0..fa73c227 100644 --- a/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/0_software-application.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix process-run-crate: . diff --git a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl index e7ce5aa4..06ebfcab 100644 --- a/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/1_create_action.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix process-run-crate: . diff --git a/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl b/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl index 38f63916..c6e452b6 100644 --- a/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/2_collection.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix process-run-crate: . diff --git a/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl b/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl index 3100c769..62c4121c 100644 --- a/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl +++ b/rocrate_validator/profiles/process-run-crate/should/3_container_image.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix process-run-crate: . diff --git a/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl index 39e561ea..95dc5cf7 100644 --- a/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/may/4_data_entity_metadata.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix rdf: . diff --git a/rocrate_validator/profiles/ro-crate/may/61_license_entity.ttl b/rocrate_validator/profiles/ro-crate/may/61_license_entity.ttl index 4a4f5962..9e0d4198 100644 --- a/rocrate_validator/profiles/ro-crate/may/61_license_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/may/61_license_entity.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix rdf: . diff --git a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl index 4813f086..8e23f9ad 100644 --- a/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/1_file-descriptor_metadata.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix dct: . 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 c5869266..c269a3f9 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 @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix rdf: . diff --git a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl index 08d061da..04426096 100644 --- a/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/4_data_entity_metadata.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix rdf: . diff --git a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl index 3c761816..fda7dc0c 100644 --- a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix rdf: . diff --git a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl index 13755c55..d0cf1dd4 100644 --- a/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/must/6_contextual_entity.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix rdf: . diff --git a/rocrate_validator/profiles/ro-crate/ontology.ttl b/rocrate_validator/profiles/ro-crate/ontology.ttl index 8e08e0cd..4a4535f2 100644 --- a/rocrate_validator/profiles/ro-crate/ontology.ttl +++ b/rocrate_validator/profiles/ro-crate/ontology.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix owl: . @prefix rdf: . diff --git a/rocrate_validator/profiles/ro-crate/prefixes.ttl b/rocrate_validator/profiles/ro-crate/prefixes.ttl index 02e26ef1..e0329f2f 100644 --- a/rocrate_validator/profiles/ro-crate/prefixes.ttl +++ b/rocrate_validator/profiles/ro-crate/prefixes.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix sh: . @prefix xsd: . diff --git a/rocrate_validator/profiles/ro-crate/profile.ttl b/rocrate_validator/profiles/ro-crate/profile.ttl index eda8a696..64c34837 100644 --- a/rocrate_validator/profiles/ro-crate/profile.ttl +++ b/rocrate_validator/profiles/ro-crate/profile.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix dct: . @prefix prof: . @prefix role: . 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 73d5386b..b495a024 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 @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix rdf: . diff --git a/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl index f8f78dea..762d80ab 100644 --- a/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/should/4_data_entity_metadata.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix rdf: . diff --git a/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl index cf9e9b13..57d29b4f 100644 --- a/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix schema: . diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl index fb02e9a4..850da254 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix workflow-ro-crate: . diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl index 57740ca7..9cac260e 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix workflow-ro-crate: . 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 4decb8d5..dc8f6fc1 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 @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix workflow-ro-crate: . diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl b/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl index 341fd5d6..0f6a913a 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix workflow-ro-crate: . diff --git a/rocrate_validator/profiles/workflow-ro-crate/profile.ttl b/rocrate_validator/profiles/workflow-ro-crate/profile.ttl index 7f293477..30d8b32f 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/profile.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/profile.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix dct: . @prefix prof: . @prefix role: . diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl index c964a05f..5c1daf32 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix workflow-ro-crate: . diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl index 89172089..834e8473 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl @@ -1,3 +1,17 @@ +# 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. + @prefix ro: <./> . @prefix ro-crate: . @prefix workflow-ro-crate: . From 34f804b4b2beda7555de5d9b81e1791050e28b3e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Aug 2024 11:57:19 +0200 Subject: [PATCH 793/902] docs(readme): :adhesive_bandage: fix link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cd5e5e7c..97dccef5 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ This project is licensed under the terms of the Apache License 2.0. See the This work has been partially funded by the following sources: Co-funded by the EU - the [BY-COVID](https://by-covid.org/) project (HORIZON Europe grant agreement number 101046203); From 1397216fe294a086362b7aded4b06a42f2949211 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Aug 2024 12:11:41 +0200 Subject: [PATCH 794/902] ci(github): :construction_worker: initialise testing workflow --- .github/workflows/testing.yaml | 61 ++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/testing.yaml diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml new file mode 100644 index 00000000..499a47a8 --- /dev/null +++ b/.github/workflows/testing.yaml @@ -0,0 +1,61 @@ +name: CI Pipeline + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + push: + branches: + - "**" + tags: + - "*.*.*" + paths: + - "**" + - "!docs/**" + - "!examples/**" + pull_request: + paths: + - "**" + - "!docs/**" + - "!examples/**" + +env: + TERM: xterm + # enable Docker push only if the required secrets are defined + # ENABLE_DOCKER_PUSH: ${{ secrets.DOCKERHUB_USER != null && secrets.DOCKERHUB_TOKEN != null }} + # Base Image + IMAGE: python:3.12-slim + # Define the virtual environment path + VENV_PATH: .venv + +jobs: + # Verifies pep8, pyflakes and circular complexity + flake8: + name: Lint Python Code (Flake8) (python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11"] + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + - name: Set up Python v${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install flake8 + run: pip install flake8 + - name: Run checks + run: flake8 -v . + + test: + name: "Test" + runs-on: ${IMAGE} + needs: [flake8] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Upgrade pip + run: pip install --upgrade pip + - name: Initialise a virtual env + run: python -m venv ${VENV_PATH} + From 0d33ece2b9c0ec6d22fce2e51335673ba3f0c265 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Aug 2024 12:18:42 +0200 Subject: [PATCH 795/902] ci(github): :fire: disable flake8 --- .github/workflows/testing.yaml | 37 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 499a47a8..841eff8b 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -28,29 +28,30 @@ env: VENV_PATH: .venv jobs: + # TODO: refactor this using poetry # Verifies pep8, pyflakes and circular complexity - flake8: - name: Lint Python Code (Flake8) (python ${{ matrix.python-version }}) - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.11"] - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v4 - - name: Set up Python v${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install flake8 - run: pip install flake8 - - name: Run checks - run: flake8 -v . + # flake8: + # name: Lint Python Code (Flake8) (python ${{ matrix.python-version }}) + # runs-on: ubuntu-latest + # strategy: + # matrix: + # python-version: ["3.11"] + # steps: + # # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + # - uses: actions/checkout@v4 + # - name: Set up Python v${{ matrix.python-version }} + # uses: actions/setup-python@v5 + # with: + # python-version: ${{ matrix.python-version }} + # - name: Install flake8 + # run: pip install flake8 + # - name: Run checks + # run: flake8 -v . test: name: "Test" runs-on: ${IMAGE} - needs: [flake8] + # needs: [flake8] steps: - name: Checkout uses: actions/checkout@v4 From 10cb0b4a7ca14c35a0c2e81519f4550c119f1b15 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Aug 2024 12:21:31 +0200 Subject: [PATCH 796/902] ci(github): :white_check_mark: set up test job --- .github/workflows/testing.yaml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 841eff8b..30a2bd0f 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -23,7 +23,7 @@ env: # enable Docker push only if the required secrets are defined # ENABLE_DOCKER_PUSH: ${{ secrets.DOCKERHUB_USER != null && secrets.DOCKERHUB_TOKEN != null }} # Base Image - IMAGE: python:3.12-slim + # IMAGE: python:3.12-slim # Define the virtual environment path VENV_PATH: .venv @@ -49,8 +49,8 @@ jobs: # run: flake8 -v . test: - name: "Test" - runs-on: ${IMAGE} + name: "Tests" + runs-on: ubuntu-latest # needs: [flake8] steps: - name: Checkout @@ -59,4 +59,12 @@ jobs: run: pip install --upgrade pip - name: Initialise a virtual env run: python -m venv ${VENV_PATH} + - name: Enable virtual env + run: source ${VENV_PATH}/bin/activate + - name: Install Poetry + run: pip install poetry + - name: Install dependencies + run: poetry install --no-interaction --no-ansi + - name: Run tests + run: poetry run pytest From 00ba47a08bd83ebc55a94627888874c909eeb74c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Aug 2024 13:21:50 +0200 Subject: [PATCH 797/902] docs(readme): :green_heart: add the badge for the GitHub testing pipeline --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 97dccef5..07728efb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # rocrate-validator -[![Build Status](https://repolab.crs4.it/lifemonitor/rocrate-validator/badges/develop/pipeline.svg)](https://repolab.crs4.it/lifemonitor/rocrate-validator/-/pipelines?page=1&scope=branches&ref=develop)[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![main workflow](https://github.com/crs4/rocrate-validator/actions/workflows/testing.yaml/badge.svg)](https://github.com/crs4/rocrate-validator/actions/workflows/testing.yaml) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + + + + From cac07378bb3c182544eb9e88482fe3ebb96b6419 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 9 Aug 2024 13:35:23 +0200 Subject: [PATCH 798/902] docs: :memo: update list of authors --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 123517dc..6bd39cb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "rocrate-validator" version = "0.1.0" description = "" -authors = ["Marco Enrico Piras ", "Luca Pireddu "] +authors = ["Marco Enrico Piras ", "Luca Pireddu ", "Simone Leo "] readme = "README.md" packages = [{ include = "rocrate_validator", from = "." }] include = [ From e797566b83762820eb62def8c864db4c13e14abd Mon Sep 17 00:00:00 2001 From: Luca Pireddu Date: Mon, 12 Aug 2024 15:48:09 +0200 Subject: [PATCH 799/902] Update README --- README.md | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 07728efb..c7276512 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,28 @@ -A Python package to validate [ROCrate](https://researchobject.github.io/ro-crate/) packages. +A Python package to validate [RO-Crate](https://researchobject.github.io/ro-crate/) packages. + +* Supports CLI-based validation as well as programmatic validation (so it can + easily be used by Python code). +* Implements an extensible validation framework to which new RO-Crate profiles + can be added. Validation is based on SHACL shapes and Python code. +* Currently, validations for the following profiles are implemented: RO-Crate + (base profile), [Workflow + RO-Crate](https://www.researchobject.org/ro-crate/specification/1.1/workflows.html), + [Process Run + Crate](https://www.researchobject.org/workflow-run-crate/profiles/0.1/process_run_crate.html). + More are being implemented. + +**Note**: this software is still work in progress. Feel free to try it out, +report positive and negative feedback. Do send a note (e.g., by opening an +Issue) before starting to develop patches you would like to contribute. The +implementation of validation code for additional RO-Crate profiles would be +particularly welcome. ## Setup -Follow these steps to setup the project: +Follow these steps to set up the project: 1. **Clone the repository** @@ -21,9 +38,9 @@ git clone https://github.com/crs4/rocrate-validator.git cd rocrate-validator ``` -2. **Setup a Python virtual environment (optional)** +2. **Set up a Python virtual environment (optional)** -Setup a Python virtual environment using `venv`: +Set up a Python virtual environment using `venv`: ```bash python3 -m venv .venv @@ -51,7 +68,7 @@ poetry install After installation, you can use the main command `rocrate-validator` to validate ROCrates. -### using Poetry +### Using Poetry Run the validator using the following command: @@ -59,11 +76,11 @@ Run the validator using the following command: poetry run rocrate-validator validate ``` -Replace `` with the path to the ROCrate you want to validate. +Replace `` with the path to the RO-Crate you want to validate. Type `poetry run rocrate-validator --help` for more information. -### using the installed package on your virtual environment +### Using the installed package on your virtual environment Activate the virtual environment: @@ -77,7 +94,7 @@ Then, run the validator using the following command: rocrate-validator validate ``` -Replace `` with the path to the ROCrate you want to validate. +Replace `` with the path to the RO-Crate you want to validate. Type `rocrate-validator --help` for more information. @@ -102,10 +119,9 @@ This project is licensed under the terms of the Apache License 2.0. See the This work has been partially funded by the following sources: +* the [BY-COVID](https://by-covid.org/) project (HORIZON Europe grant agreement number 101046203); +* the [LIFEMap](https://www.thelifemap.it/) project, funded by the Italian Ministry of Health (Piano Operative Salute, Trajectory 3). + Co-funded by the EU - -- the [BY-COVID](https://by-covid.org/) project (HORIZON Europe grant agreement number 101046203); -- the [LIFEMap](https://www.thelifemap.it/) project, funded by the Italian Ministry of Health (Piano Operative Salute, Trajectory 3). - From 0e6f59f278b9c3608c6bdf5166b1838e77c5afe4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 5 Sep 2024 19:52:05 +0200 Subject: [PATCH 800/902] refactor(shacl): :sparkles: avoid infinite loops when parsing cyclic shapes graphs --- rocrate_validator/requirements/shacl/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index b1a9331f..d796fa9f 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -21,9 +21,9 @@ from rdflib import RDF, BNode, Graph, Namespace from rdflib.term import Node +import rocrate_validator.log as logging from rocrate_validator.constants import RDF_SYNTAX_NS, SHACL_NS from rocrate_validator.errors import BadSyntaxError -import rocrate_validator.log as logging from rocrate_validator.models import Severity # set up logging @@ -233,14 +233,17 @@ def __extract_related_triples__(graph, subject_node): """ Recursively extract all triples related to a given shape. """ + related_triples = [] + # Directly related triples related_triples.extend((_, p, o) for (_, p, o) in graph.triples((subject_node, None, None))) # Recursively find triples related to nested shapes for _, _, object_node in related_triples: if isinstance(object_node, Node): - related_triples.extend(__extract_related_triples__(graph, object_node)) + if str(object_node) != str(subject_node): + related_triples.extend(__extract_related_triples__(graph, object_node)) return related_triples From 1e411fa9c69efb1a1e56235617a827ef67eee3c0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Sep 2024 08:37:02 +0200 Subject: [PATCH 801/902] feat(utils): :sparkles: programmatically disallow property injection --- rocrate_validator/requirements/shacl/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index d796fa9f..8ade2c9c 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -16,7 +16,7 @@ import hashlib from pathlib import Path -from typing import Union +from typing import Optional, Union from rdflib import RDF, BNode, Graph, Namespace from rdflib.term import Node @@ -70,10 +70,10 @@ def make_uris_relative(text: str, ro_crate_path: Union[Path, str]) -> str: return text.replace(str(ro_crate_path), './') -def inject_attributes(obj: object, node_graph: Graph, node: Node) -> object: +def inject_attributes(obj: object, node_graph: Graph, node: Node, exclude: Optional[list] = None) -> object: # inject attributes of the shape property # logger.debug("Injecting attributes of node %s", node) - skip_properties = ["node"] + skip_properties = ["node"] if exclude is None else exclude + ["node"] triples = node_graph.triples((node, None, None)) for node, p, o in triples: predicate_as_string = p.toPython() From ec00c6b61b4d8f7452a91ba8371bf57606507a03 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Sep 2024 09:39:12 +0200 Subject: [PATCH 802/902] feat(shacl): :sparkles: automatically generate name and description of nodes and properties --- .../requirements/shacl/models.py | 65 +++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 11c5414c..ea92f3e7 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -34,8 +34,8 @@ class SHACLNode: # define default values - name: str = None - description: str = None + _name: str = None + _description: str = None severity: str = None def __init__(self, node: Node, graph: Graph, parent: Optional[SHACLNode] = None): @@ -54,6 +54,28 @@ def __init__(self, node: Node, graph: Graph, parent: Optional[SHACLNode] = None) # inject attributes of the shape to the object inject_attributes(self, graph, node) + @property + def name(self) -> str: + """Return the name of the shape""" + if not self._name: + self._name = self._node.split("#")[-1] if "#" in self.node else self._node.split("/")[-1] + return self._name or self._node.split("/")[-1] + + @name.setter + def name(self, value: str): + self._name = value + + @property + def description(self) -> str: + """Return the description of the shape""" + if not self._description: + self._description = f"Check properties of the \"**{self.name}**\" entity" + return self._description + + @description.setter + def description(self, value: str): + self._description = value + @property def key(self) -> str: """Return the key of the shape""" @@ -168,8 +190,9 @@ class PropertyGroup(SHACLNodeCollection): class PropertyShape(Shape): # define default values - name: str = None - description: str = None + _name: str = None + _short_name: str = None + _description: str = None group: str = None defaultValue: str = None order: int = 0 @@ -185,6 +208,40 @@ def __init__(self, # store the parent shape self._parent = parent + @property + def name(self) -> str: + """Return the name of the shape property""" + if not self._name: + # get the object of the predicate sh:path + shacl_ns = Namespace(SHACL_NS) + path = self.graph.value(subject=self.node, predicate=shacl_ns.path) + if path: + self._short_name = path.split("#")[-1] if "#" in path else path.split("/")[-1] + if self.parent: + self._name = f"{self._short_name} of {self.parent.name}" + return self._name + + @name.setter + def name(self, value: str): + self._name = value + + @property + def description(self) -> str: + """Return the description of the shape property""" + if not self._description: + # get the object of the predicate sh:description + property_name = self.name + if self._short_name: + property_name = self._short_name + self._description = f"Check the property \"**{property_name}**\"" + if self.parent and self.parent.name not in property_name: + self._description += f" of the entity \"**{self.parent.name}**\"" + return self._description + + @description.setter + def description(self, value: str): + self._description = value + @property def node(self) -> Node: """Return the node of the shape property""" From 501e8aaf860a630d6567b5608eb32c55a6807cc3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Sep 2024 09:42:06 +0200 Subject: [PATCH 803/902] fix(shacl): :adhesive_bandage: fix test for membership --- rocrate_validator/requirements/shacl/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index ea92f3e7..0e0c1d89 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -364,7 +364,7 @@ def load_shapes(self, shapes_path: Union[str, Path], publicID: Optional[str] = N property_shape, property_graph, shape) shape.add_property(p_shape) group = __process_property_group__(property_groups, p_shape) - if group and not group in shapes: + if group and group not in shapes: grouped = True shapes.append(group) if not group: From ec1e9ff14587bcb7740862bc93c4001856cd1488 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Sep 2024 10:25:43 +0200 Subject: [PATCH 804/902] feat(shacl): :building_construction: generalize the check of processed nodes to avoid infinte loops --- rocrate_validator/requirements/shacl/utils.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index 8ade2c9c..bc009dca 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -229,21 +229,29 @@ def load_from_graph(cls, graph: Graph) -> ShapesList: return load_shapes_from_graph(graph) -def __extract_related_triples__(graph, subject_node): +def __extract_related_triples__(graph, subject_node, processed_nodes=None): """ Recursively extract all triples related to a given shape. """ related_triples = [] + processed_nodes = processed_nodes if processed_nodes is not None else set() + + # Skip the current node if it has already been processed + if subject_node in processed_nodes: + return related_triples + + # Add the current node to the processed nodes + processed_nodes.add(subject_node) + # Directly related triples related_triples.extend((_, p, o) for (_, p, o) in graph.triples((subject_node, None, None))) # Recursively find triples related to nested shapes for _, _, object_node in related_triples: if isinstance(object_node, Node): - if str(object_node) != str(subject_node): - related_triples.extend(__extract_related_triples__(graph, object_node)) + related_triples.extend(__extract_related_triples__(graph, object_node, processed_nodes)) return related_triples From 024234995af9a6119321015ee19c930c6c15cbc0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Sep 2024 11:00:16 +0200 Subject: [PATCH 805/902] refactor: :rotating_light: fix flake8 warning E713 --- rocrate_validator/models.py | 6 +++--- rocrate_validator/requirements/shacl/checks.py | 10 +++++----- rocrate_validator/requirements/shacl/validator.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 6667ba41..db90d22b 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -306,15 +306,15 @@ def __get_nested_profiles__(cls, source: str) -> list[str]: queue = [source] while len(queue) > 0: p = queue.pop() - if not p in visited: + if p not in visited: visited.append(p) profile = cls.__profiles_map.get_by_key(p) inherited_profiles = profile.is_profile_of if inherited_profiles: for p in sorted(inherited_profiles, reverse=True): - if not p in visited: + if p not in visited: queue.append(p) - if not p in result: + if p not in result: result.insert(0, p) return result diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 1fe9cebd..f833594f 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -69,13 +69,13 @@ def execute_check(self, context: ValidationContext): logger.debug("SHACL Validation of profile %s requirement %s started", self.requirement.profile.identifier, self.identifier) result = self.__do_execute_check__(ctx) - ctx.current_validation_result = not self in result + ctx.current_validation_result = self not in result return ctx.current_validation_result except SHACLValidationAlreadyProcessed as e: logger.debug("SHACL Validation of profile %s already processed", self.requirement.profile.identifier) # 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 not self in [i.check for i in context.result.get_issues()] + return self not in [i.check 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) @@ -177,7 +177,7 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): # 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 - if not requirementCheck.identifier in failed_requirement_checks_notified: + if requirementCheck.identifier not in failed_requirement_checks_notified: # if requirementCheck.requirement.profile != shacl_context.current_validation_profile: failed_requirement_checks_notified.append(requirementCheck.identifier) @@ -198,8 +198,8 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): logger.debug("Skipped check is not a SHACLCheck: %s", requirementCheck.identifier) continue if requirementCheck.requirement.profile != shacl_context.current_validation_profile and \ - not requirementCheck in failed_requirements_checks and \ - not requirementCheck.identifier in failed_requirement_checks_notified: + requirementCheck not in failed_requirements_checks and \ + requirementCheck.identifier not in failed_requirement_checks_notified: failed_requirement_checks_notified.append(requirementCheck.identifier) shacl_context.result.add_executed_check(requirementCheck, True) shacl_context.validator.notify(RequirementCheckValidationEvent( diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 97c11fc6..738a40d6 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -113,7 +113,7 @@ def __init__(self, context: ValidationContext): self._ontology_graph: Graph = Graph() def __set_current_validation_profile__(self, profile: Profile) -> bool: - if not profile.identifier in self._processed_profiles: + if profile.identifier not in self._processed_profiles: # augment the ontology graph with the profile ontology ontology_graph = self.__load_ontology_graph__(profile.path) if ontology_graph: @@ -211,7 +211,7 @@ def __load_ontology_graph__(self, profile_path: Path, ontology_filename: Optiona def ontology_graph(self) -> Graph: return self._ontology_graph - @ classmethod + @classmethod def get_instance(cls, context: ValidationContext) -> SHACLValidationContext: instance = getattr(context, "_shacl_validation_context", None) if not instance: From 4927ae4eaee215832885aa86e818080870238a5e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Sep 2024 13:39:23 +0200 Subject: [PATCH 806/902] refactor: :rotating_light: fix flake8 warning E501 --- rocrate_validator/cli/commands/errors.py | 3 +- rocrate_validator/cli/commands/profiles.py | 3 +- rocrate_validator/cli/commands/validate.py | 50 ++++++++++++++----- rocrate_validator/models.py | 28 +++++++---- .../should/5_web_data_entity_metadata.py | 8 +-- .../requirements/shacl/checks.py | 13 ++--- .../requirements/shacl/validator.py | 9 ++-- rocrate_validator/services.py | 6 ++- .../process-run-crate/test_procrc_action.py | 6 ++- .../test_procrc_containerimage.py | 8 ++- .../test_procrc_root_data_entity.py | 9 ++-- .../ro-crate/test_root_data_entity.py | 3 +- .../workflow-ro-crate/test_wroc_descriptor.py | 3 +- tests/unit/requirements/test_profiles.py | 3 +- tests/unit/test_rocrate.py | 12 ++++- 15 files changed, 115 insertions(+), 49 deletions(-) diff --git a/rocrate_validator/cli/commands/errors.py b/rocrate_validator/cli/commands/errors.py index a8dd3c61..412b98c2 100644 --- a/rocrate_validator/cli/commands/errors.py +++ b/rocrate_validator/cli/commands/errors.py @@ -50,7 +50,8 @@ def handle_error(e: Exception, console: Console) -> None: 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)) + "Please report it to the issue tracker " + "along with the following stack trace:\n", ' ' * 9)) console.print_exception() console.print(f"\n\n[bold][[red]ERROR[/red]] {error_message}[/bold]\n", style="white") diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 49c2a9e6..4335f0cf 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -263,7 +263,8 @@ def __verbose_describe_profile__(profile): # Uncomment the following lines to show the overridden checks # if check.overridden_by: # severity_color = get_severity_color(check.overridden_by.severity) - # override = f"[overridden by: [bold][magenta]{check.overridden_by.requirement.profile.identifier}[/magenta] "\ + # override = "[overridden by: " \ + # f"[bold][magenta]{check.overridden_by.requirement.profile.identifier}[/magenta] "\ # f"[{severity_color}]{check.overridden_by.relative_identifier}[/{severity_color}][/bold]]" if check.override: severity_color = get_severity_color(check.override.severity) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index ca28f893..21b3b651 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -275,11 +275,24 @@ def validate(ctx, logger.info("Auto-detection of the profiles to use for validation is disabled") # Prompt the user to select the profile to use for validation if the interactive mode is enabled - # and no profile is autodetected or multiple profiles are detected - if interactive and (not candidate_profiles or len(candidate_profiles) == 0 or len(candidate_profiles) == len(available_profiles)): + # and no profile is auto-detected or multiple profiles are detected + if interactive and ( + not candidate_profiles or + len(candidate_profiles) == 0 or + len(candidate_profiles) == len(available_profiles) + ): # Define the list of choices - 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))) + 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) + ) + ) selected_options = multiple_choice(console, available_profiles) profile_identifier = [available_profiles[int( selected_option)].identifier for selected_option in selected_options] @@ -331,7 +344,10 @@ def validate(ctx, verbose_choice = "n" if interactive and not verbose and enable_pager: verbose_choice = get_single_char(console, choices=['y', 'n'], - message="[bold] > Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]") + message=( + "[bold] > Do you want to see the validation details? " + "([magenta]y/n[/magenta]): [/bold]" + )) if verbose_choice == "y" or verbose: report_layout.show_validation_details(pager, enable_pager=enable_pager) @@ -508,8 +524,11 @@ def __init_layout__(self): base_info_layout = Layout( Align( f"\n[bold cyan]RO-Crate:[/bold cyan] [bold]{URI(settings['data_path']).uri}[/bold]" - f"\n[bold cyan]Target Profile:[/bold cyan][bold magenta] {settings['profile_identifier']}[/bold magenta] { '[italic](autodetected)[/italic]' if settings['profile_autodetected'] else ''}" - f"\n[bold cyan]Validation Severity:[/bold cyan] [bold {severity_color}]{settings['requirement_severity']}[/bold {severity_color}]", + "\n[bold cyan]Target Profile:[/bold cyan][bold magenta] " + f"{settings['profile_identifier']}[/bold magenta] " + f"{ '[italic](autodetected)[/italic]' if settings['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"), name="Base Info", size=5) # @@ -641,11 +660,13 @@ def set_overall_result(self, result: ValidationResult): self.result = result if result.passed(): self.overall_result.update( - Padding(Rule(f"[bold][[green]OK[/green]] RO-Crate is a [green]valid[/green] [magenta]{result.context.target_profile.identifier}[/magenta] !!![/bold]\n\n", + Padding(Rule("[bold][[green]OK[/green]] RO-Crate is a [green]valid[/green] " + f"[magenta]{result.context.target_profile.identifier}[/magenta] !!![/bold]\n\n", style="bold green"), (1, 1))) else: self.overall_result.update( - Padding(Rule(f"[bold][[red]FAILED[/red]] RO-Crate is [red]not[/red] a [red]valid[/red] [magenta]{result.context.target_profile.identifier}[/magenta] !!![/bold]\n", + Padding(Rule("[bold][[red]FAILED[/red]] RO-Crate is [red]not[/red] a [red]valid[/red] " + f"[magenta]{result.context.target_profile.identifier}[/magenta] !!![/bold]\n", style="bold red"), (1, 1))) def show_validation_details(self, pager: Pager, enable_pager: bool = True): @@ -674,7 +695,8 @@ def show_validation_details(self, pager: Pager, enable_pager: bool = True): f"profile: [magenta bold]{requirement.profile.name }[/magenta bold]]", align="right") ) console.print( - f" [bold][cyan][{requirement.order_number}] [u]{Markdown(requirement.name).markup}[/u][/cyan][/bold]", + f" [bold][cyan][{requirement.order_number}] " + "[u]{Markdown(requirement.name).markup}[/u][/cyan][/bold]", style="white", ) console.print(Padding(Markdown(requirement.description), (1, 7))) @@ -685,7 +707,10 @@ def show_validation_details(self, pager: Pager, enable_pager: bool = True): key=lambda x: (-x.severity.value, x)): issue_color = get_severity_color(check.level.severity) console.print( - Padding(f"[bold][{issue_color}][{check.relative_identifier.center(16)}][/{issue_color}] [magenta]{check.name}[/magenta][/bold]:", (0, 7)), style="white bold") + Padding( + f"[bold][{issue_color}][{check.relative_identifier.center(16)}][/{issue_color}] " + f"[magenta]{check.name}[/magenta][/bold]:", (0, 7)), + style="white bold") console.print(Padding(Markdown(check.description), (0, 27))) console.print(Padding("[u] Detected issues [/u]", (0, 8)), style="white bold") for issue in sorted(result.get_issues_by_check(check), @@ -743,7 +768,8 @@ def __compute_profile_stats__(validation_settings: dict): check_count_by_severity[severity] = 0 if severity_validation <= severity: num_checks = len( - [_ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name)) if not _.overridden]) + [_ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name)) + if not _.overridden]) check_count_by_severity[severity] += num_checks total_checks += num_checks diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index db90d22b..0c2fec69 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -150,8 +150,10 @@ def get(name: str) -> RequirementLevel: class Profile: # store the map of profiles: profile URI -> Profile instance - __profiles_map: MultiIndexMap = MultiIndexMap("uri", - indexes=[MapIndex("name"), MapIndex("token", unique=False), MapIndex("identifier", unique=True)]) + __profiles_map: MultiIndexMap = \ + MultiIndexMap("uri", indexes=[ + MapIndex("name"), MapIndex("token", unique=False), MapIndex("identifier", unique=True) + ]) def __init__(self, profiles_base_path: Path, @@ -191,13 +193,17 @@ def __init__(self, self._token, self._version = self.__init_token_version__() # add the profile to the profiles map self.__profiles_map.add( - self._profile_node.toPython(), self, token=self.token, name=self.name, identifier=self.identifier) # add the profile to the profiles map + self._profile_node.toPython(), + self, token=self.token, + name=self.name, identifier=self.identifier + ) # add the profile to the profiles map else: raise ProfileSpecificationError( message=f"Profile specification file {spec_file} must contain exactly one profile") - def __get_specification_property__(self, property: str, namespace: Namespace, - pop_first: bool = True, as_Python_object: bool = True) -> Union[str, list[Union[str, URIRef]]]: + def __get_specification_property__( + self, property: str, namespace: Namespace, + pop_first: bool = True, as_Python_object: bool = True) -> Union[str, list[Union[str, URIRef]]]: assert self._profile_specification_graph is not None, "Profile specification graph not loaded" values = list(self._profile_specification_graph.objects(self._profile_node, namespace[property])) if values and as_Python_object: @@ -1234,7 +1240,8 @@ def parse(cls, settings: Union[dict, ValidationSettings]) -> ValidationSettings: class ValidationEvent(Event): - def __init__(self, event_type: EventType, validation_result: Optional[ValidationResult] = None, message: Optional[str] = None): + def __init__(self, event_type: EventType, + validation_result: Optional[ValidationResult] = None, message: Optional[str] = None): super().__init__(event_type, message) self._validation_result = validation_result @@ -1275,7 +1282,9 @@ def validation_result(self) -> Optional[bool]: class RequirementCheckValidationEvent(Event): - def __init__(self, event_type: EventType, requirement_check: RequirementCheck, validation_result: Optional[bool] = None, message: Optional[str] = None): + def __init__(self, event_type: EventType, + requirement_check: RequirementCheck, + validation_result: Optional[bool] = None, message: Optional[str] = None): assert event_type in (EventType.REQUIREMENT_CHECK_VALIDATION_START, EventType.REQUIREMENT_CHECK_VALIDATION_END) super().__init__(event_type, message) self._requirement_check = requirement_check @@ -1545,8 +1554,9 @@ def __load_profiles__(self) -> list[Profile]: # raised when the profile is not found if logger.isEnabledFor(logging.DEBUG): logger.exception(e) - raise ProfileNotFound(self.profile_identifier, - message=f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'") from e + raise ProfileNotFound( + self.profile_identifier, + message=f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'") from e # Set the profiles to validate against as the target profile and its inherited profiles profiles = profile.inherited_profiles + [profile] diff --git a/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py index c5122d38..e58b5fce 100644 --- a/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.py @@ -24,13 +24,14 @@ @requirement(name="Web-based Data Entity: RECOMMENDED resource availability") class WebDataEntityRecommendedChecker(PyFunctionCheck): """ - Web-based Data Entity instances SHOULD be available at the URIs specified in the `@id` property of the Web-based Data Entity. + Web-based Data Entity instances SHOULD be available + at the URIs specified in the `@id` property of the Web-based Data Entity. """ @check(name="Web-based Data Entity: resource availability") def check_availability(self, context: ValidationContext) -> bool: """ - Check if the Web-based Data Entity is directly downloadable + Check if the Web-based Data Entity is directly downloadable by a simple retrieval (e.g. HTTP GET) permitting redirection and HTTP/HTTPS URIs """ result = True @@ -62,7 +63,8 @@ def check_content_size(self, context: ValidationContext) -> bool: content_size = entity.get_property("contentSize") if content_size and int(content_size) != context.ro_crate.get_external_file_size(entity.id): context.result.add_check_issue( - f'The property contentSize={content_size} of the Web-based Data Entity {entity.id} does not match the actual size of ' + 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., {entity.content_size} (bytes)', self, focusNode=entity.id, resultPath='contentSize', value=content_size) result = False diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index f833594f..b3d162ed 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -161,12 +161,13 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): if requirementCheck.requirement.profile == shacl_context.current_validation_profile or \ shacl_context.settings.get("target_only_validation", False): for violation in failed_requirements_checks_violations[requirementCheck.identifier]: - c = shacl_context.result.add_check_issue(message=violation.get_result_message(shacl_context.rocrate_path), - check=requirementCheck, - severity=violation.get_result_severity(), - resultPath=violation.resultPath.toPython() if violation.resultPath else None, - focusNode=make_uris_relative( - violation.focusNode.toPython(), shacl_context.publicID), + c = shacl_context.result.add_check_issue( + message=violation.get_result_message(shacl_context.rocrate_path), + check=requirementCheck, + severity=violation.get_result_severity(), + resultPath=violation.resultPath.toPython() if violation.resultPath else None, + focusNode=make_uris_relative( + violation.focusNode.toPython(), shacl_context.publicID), value=violation.value) # if the fail fast mode is enabled, stop the validation after the first issue if shacl_context.fail_fast: diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 738a40d6..70213ba4 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -195,7 +195,8 @@ def __get_ontology_path__(self, profile_path: Path, ontology_filename: str = DEF self._ontology_path = Path(supported_path) return self._ontology_path - def __load_ontology_graph__(self, profile_path: Path, ontology_filename: Optional[str] = DEFAULT_ONTOLOGY_FILE) -> Graph: + def __load_ontology_graph__(self, profile_path: Path, + ontology_filename: Optional[str] = DEFAULT_ONTOLOGY_FILE) -> Graph: # load the graph of ontologies ontology_graph = None ontology_path = self.__get_ontology_path__(profile_path, ontology_filename) @@ -283,7 +284,8 @@ def sourceConstraintComponent(self): if not self._source_constraint_component: self._source_constraint_component = self.graph.value( self._violation_node, URIRef(f"{SHACL_NS}sourceConstraintComponent")) - assert self._source_constraint_component is not None, f"Unable to get source constraint component from violation node {self._violation_node}" + assert self._source_constraint_component is not None, \ + f"Unable to get source constraint component from violation node {self._violation_node}" return self._source_constraint_component def get_result_message(self, ro_crate_path: Union[Path, str]) -> str: @@ -297,7 +299,8 @@ def get_result_message(self, ro_crate_path: Union[Path, str]) -> str: def sourceShape(self) -> Union[URIRef, BNode]: if not self._source_shape_node: self._source_shape_node = self.graph.value(self._violation_node, URIRef(f"{SHACL_NS}sourceShape")) - assert self._source_shape_node is not None, f"Unable to get source shape node from violation node {self._violation_node}" + assert self._source_shape_node is not None, \ + f"Unable to get source shape node from violation node {self._violation_node}" return self._source_shape_node diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 00ed6525..3b94a86e 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -161,7 +161,8 @@ def __extract_and_validate_rocrate__(rocrate_path: Path): def get_profiles(profiles_path: Path = DEFAULT_PROFILES_PATH, publicID: str = None, severity=Severity.OPTIONAL, - allow_requirement_check_override: bool = ValidationSettings.allow_requirement_check_override) -> list[Profile]: + allow_requirement_check_override: bool = + ValidationSettings.allow_requirement_check_override) -> list[Profile]: """ Load the profiles from the given path """ @@ -175,7 +176,8 @@ def get_profiles(profiles_path: Path = DEFAULT_PROFILES_PATH, def get_profile(profiles_path: Path = DEFAULT_PROFILES_PATH, profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, publicID: str = None, - allow_requirement_check_override: bool = ValidationSettings.allow_requirement_check_override) -> Profile: + allow_requirement_check_override: bool = + ValidationSettings.allow_requirement_check_override) -> Profile: """ Load the profiles from the given path """ diff --git a/tests/integration/profiles/process-run-crate/test_procrc_action.py b/tests/integration/profiles/process-run-crate/test_procrc_action.py index 0c6cbf2e..bc32c4eb 100644 --- a/tests/integration/profiles/process-run-crate/test_procrc_action.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_action.py @@ -261,7 +261,8 @@ def test_procrc_action_bad_actionstatus(): Severity.RECOMMENDED, False, ["Process Run Crate Action SHOULD"], - ["If the Action has an actionStatus, it should be http://schema.org/CompletedActionStatus or http://schema.org/FailedActionStatus"], + ["If the Action has an actionStatus, it should be " + "http://schema.org/CompletedActionStatus or http://schema.org/FailedActionStatus"], profile_identifier="process-run-crate" ) @@ -291,7 +292,8 @@ def test_procrc_action_obj_res_bad_type(): Severity.RECOMMENDED, False, ["Process Run Crate Action object and result types"], - ["object and result SHOULD point to entities of type MediaObject, Dataset, Collection, CreativeWork or PropertyValue"], + ["object and result SHOULD point to entities of type " + "MediaObject, Dataset, Collection, CreativeWork or PropertyValue"], profile_identifier="process-run-crate" ) diff --git a/tests/integration/profiles/process-run-crate/test_procrc_containerimage.py b/tests/integration/profiles/process-run-crate/test_procrc_containerimage.py index 7af7ba9b..e8165733 100644 --- a/tests/integration/profiles/process-run-crate/test_procrc_containerimage.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_containerimage.py @@ -31,7 +31,9 @@ def test_procrc_containerimage_no_additionaltype(): Severity.RECOMMENDED, False, ["Process Run Crate ContainerImage SHOULD"], - ["The ContainerImage SHOULD have an additionalType pointing to or "], + ["The ContainerImage SHOULD have an additionalType pointing " + "to or " + ""], profile_identifier="process-run-crate" ) @@ -46,7 +48,9 @@ def test_procrc_containerimage_bad_additionaltype(): Severity.RECOMMENDED, False, ["Process Run Crate ContainerImage SHOULD"], - ["The ContainerImage SHOULD have an additionalType pointing to or "], + ["The ContainerImage SHOULD have an additionalType pointing " + "to or " + ""], profile_identifier="process-run-crate" ) diff --git a/tests/integration/profiles/process-run-crate/test_procrc_root_data_entity.py b/tests/integration/profiles/process-run-crate/test_procrc_root_data_entity.py index 7ac3fc0b..255523b0 100644 --- a/tests/integration/profiles/process-run-crate/test_procrc_root_data_entity.py +++ b/tests/integration/profiles/process-run-crate/test_procrc_root_data_entity.py @@ -32,7 +32,8 @@ def test_procrc_no_conformsto(): Severity.REQUIRED, False, ["Root Data Entity Metadata"], - ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile"], + ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI " + "that is consistent with the versioned permalink of the profile"], profile_identifier="process-run-crate" ) @@ -47,7 +48,8 @@ def test_procrc_conformsto_bad_type(): Severity.REQUIRED, False, ["Root Data Entity Metadata"], - ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile"], + ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI " + "that is consistent with the versioned permalink of the profile"], profile_identifier="process-run-crate" ) @@ -62,6 +64,7 @@ def test_procrc_conformsto_bad_profile(): Severity.REQUIRED, False, ["Root Data Entity Metadata"], - ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI that is consistent with the versioned permalink of the profile"], + ["The Root Data Entity MUST reference a CreativeWork entity with an @id URI " + "that is consistent with the versioned permalink of the profile"], profile_identifier="process-run-crate" ) 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 792aa5f3..95a2c1de 100644 --- a/tests/integration/profiles/ro-crate/test_root_data_entity.py +++ b/tests/integration/profiles/ro-crate/test_root_data_entity.py @@ -66,7 +66,8 @@ def test_invalid_root_date(): models.Severity.RECOMMENDED, False, ["RO-Crate Root Data Entity RECOMMENDED properties"], - ["The Root Data Entity SHOULD have a `datePublished` property (as specified by schema.org) with a valid ISO 8601 date and the precision of at least the day level"] + ["The Root Data Entity SHOULD have a `datePublished` property (as specified by schema.org) " + "with a valid ISO 8601 date and the precision of at least the day level"] ) 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 f7b1bd6b..4c14a564 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py @@ -32,6 +32,7 @@ def test_wroc_descriptor_bad_conforms_to(): Severity.RECOMMENDED, False, ["WROC Metadata File Descriptor properties"], - ["The Metadata File Descriptor conformsTo SHOULD contain https://w3id.org/ro/crate/1.1 and https://w3id.org/workflowhub/workflow-ro-crate/1.0"], + ["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" ) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index d0f1c21e..648089d2 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -205,7 +205,8 @@ def __perform_test__(profile_identifier: str, expected_inherited_profiles: list[ # The number of profiles should be 1 profiles_names = [_.token for _ in profile.inherited_profiles] - assert profiles_names == expected_inherited_profiles, f"The number of profiles should be {expected_inherited_profiles}" + assert profiles_names == expected_inherited_profiles, \ + f"The number of profiles should be {expected_inherited_profiles}" # Test the inheritance mode with 1 profile __perform_test__("a", []) diff --git a/tests/unit/test_rocrate.py b/tests/unit/test_rocrate.py index d48924f6..00ac70e4 100644 --- a/tests/unit/test_rocrate.py +++ b/tests/unit/test_rocrate.py @@ -17,7 +17,14 @@ from rocrate_validator import log as logging from rocrate_validator.errors import ROCrateInvalidURIError -from rocrate_validator.rocrate import ROCrate, ROCrateEntity, ROCrateLocalFolder, ROCrateLocalZip, ROCrateMetadata, ROCrateRemoteZip +from rocrate_validator.rocrate import ( + ROCrate, + ROCrateEntity, + ROCrateLocalFolder, + ROCrateLocalZip, + ROCrateMetadata, + ROCrateRemoteZip, +) from tests.ro_crates import ValidROC # set up logging @@ -83,7 +90,8 @@ def test_valid_local_rocrate(): assert isinstance(root_data_entity, ROCrateEntity), "Entity should be ROCrateEntity" assert root_data_entity.id == "./", "Id should be ./" assert root_data_entity.type == "Dataset", "Type should be Dataset" - assert root_data_entity.name == "Recording provenance of workflow runs with RO-Crate (RO-Crate and mapping)", "Name should be wrroc-paper" + assert root_data_entity.name == "Recording provenance of workflow runs with RO-Crate (RO-Crate and mapping)", \ + "Name should be wrroc-paper" # check metadata consistency assert root_data_entity.metadata == metadata, "Metadata should be the same" From 553e5385390c98085792d07bfd2879d62313ce14 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Sep 2024 13:42:53 +0200 Subject: [PATCH 807/902] refactor: :rotating_light: fix flake8 warning F541 --- rocrate_validator/cli/commands/validate.py | 6 +++--- rocrate_validator/errors.py | 2 +- .../profiles/workflow-ro-crate/may/1_main_workflow.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 21b3b651..0a43bba0 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -97,7 +97,7 @@ def get_single_char(console: Optional[Console] = None, end: str = "\n", console.print(char, end=end if choices and char in choices else "") if choices and char not in choices: if console: - console.print(f" [bold red]INVALID CHOICE[/bold red]", end=end) + console.print(" [bold red]INVALID CHOICE[/bold red]", end=end) return char @@ -576,12 +576,12 @@ def __init_layout__(self): # Create the main layout self.checks_stats_layout = Layout( - Panel(report_container_layout, title=f"[bold]- Validation Report -[/bold]", + Panel(report_container_layout, title="[bold]- Validation Report -[/bold]", border_style="cyan", title_align="center", padding=(1, 2))) # Create the overall result layout self.overall_result = Layout( - Padding(Rule(f"\n[italic][cyan]Validating ROCrate...[/cyan][/italic]"), (1, 1)), size=3) + Padding(Rule("\n[italic][cyan]Validating ROCrate...[/cyan][/italic]"), (1, 1)), size=3) group_layout = Layout() group_layout.add_split(self.checks_stats_layout) diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 9c5c8722..92844e87 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -97,7 +97,7 @@ def __str__(self) -> str: return msg def __repr__(self): - return f"ProfileSpecificationNotFound()" + return "ProfileSpecificationNotFound()" class ProfileSpecificationError(ROCValidatorError): 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 dd5be1f5..08a3685e 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 @@ -33,7 +33,7 @@ def check_workflow_diagram(self, context: ValidationContext) -> bool: image = main_workflow.get_property("image") diagram_relpath = image.id if image else None if not diagram_relpath: - context.result.add_error(f"main workflow does not have an 'image' property", self) + context.result.add_error("main workflow does not have an 'image' property", self) return False if not image.is_available(): context.result.add_error(f"Workflow diagram '{image.id}' not found in crate", self) From 116b2ce65970175f73a1823bafe89109ac691037 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Sep 2024 13:45:42 +0200 Subject: [PATCH 808/902] refactor: :rotating_light: fix flake8 warning F401 --- tests/integration/profiles/ro-crate/test_valid_ro-crate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration/profiles/ro-crate/test_valid_ro-crate.py b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py index edc7dfb0..e44586b3 100644 --- a/tests/integration/profiles/ro-crate/test_valid_ro-crate.py +++ b/tests/integration/profiles/ro-crate/test_valid_ro-crate.py @@ -14,8 +14,6 @@ import logging -import pytest - from rocrate_validator.models import Severity from tests.ro_crates import ValidROC from tests.shared import do_entity_test From 9770003bf3fcc7837feb22fa92c38ad94ffe3ffc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Sep 2024 13:48:47 +0200 Subject: [PATCH 809/902] refactor: :rotating_light: fix flake8 warning F841 --- rocrate_validator/requirements/shacl/checks.py | 2 ++ tests/unit/requirements/test_profiles.py | 1 + 2 files changed, 3 insertions(+) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index b3d162ed..5ed2ed54 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -73,6 +73,8 @@ def execute_check(self, context: ValidationContext): return ctx.current_validation_result except SHACLValidationAlreadyProcessed as e: logger.debug("SHACL Validation of profile %s already processed", self.requirement.profile.identifier) + if logger.isEnabledFor(logging.DEBUG): + logger.exception(e) # 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 not in [i.check for i in context.result.get_issues()] diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 648089d2..f80c39aa 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -172,6 +172,7 @@ def test_versioned_profiles_loading(fake_versioned_profiles_path): def test_conflicting_versioned_profiles_loading(fake_conflicting_versioned_profiles_path): """Test the loaded profiles from the validator context.""" with pytest.raises(ProfileSpecificationError) as excinfo: + logger.debug("result: %r", excinfo) # Load the profiles Profile.load_profiles(profiles_path=fake_conflicting_versioned_profiles_path) # Check that the conflicting versions are found From 34c3acc5ad2da928341bbffe621f05cadf5598ab Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Sep 2024 13:50:54 +0200 Subject: [PATCH 810/902] refactor: :rotating_light: fix flake8 warning E266 --- tests/unit/test_rocrate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_rocrate.py b/tests/unit/test_rocrate.py index 00ac70e4..7350b253 100644 --- a/tests/unit/test_rocrate.py +++ b/tests/unit/test_rocrate.py @@ -35,7 +35,7 @@ ################################ -###### ROCrateLocalFolder ###### +# ROCrateLocalFolder ################################ @@ -102,7 +102,7 @@ def test_valid_local_rocrate(): ################################ -###### ROCrateLocalZip ######### +# ROCrateLocalZip ################################ def test_valid_zip_rocrate(): roc = ROCrateLocalZip(ValidROC().sort_and_change_archive) @@ -166,7 +166,7 @@ def test_valid_zip_rocrate(): ################################ -###### ROCrateRemote ########### +# ROCrateRemoteZip ################################ From 18b650dbe3582280029bed42e657fec997667339 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Sep 2024 13:54:43 +0200 Subject: [PATCH 811/902] feat(ci): :sparkles: enable a step to check code linting --- .github/workflows/testing.yaml | 37 +++++++++++++++++----------------- setup.cfg | 11 ++++++++++ 2 files changed, 29 insertions(+), 19 deletions(-) create mode 100644 setup.cfg diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 30a2bd0f..44ea6858 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -28,30 +28,29 @@ env: VENV_PATH: .venv jobs: - # TODO: refactor this using poetry # Verifies pep8, pyflakes and circular complexity - # flake8: - # name: Lint Python Code (Flake8) (python ${{ matrix.python-version }}) - # runs-on: ubuntu-latest - # strategy: - # matrix: - # python-version: ["3.11"] - # steps: - # # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - # - uses: actions/checkout@v4 - # - name: Set up Python v${{ matrix.python-version }} - # uses: actions/setup-python@v5 - # with: - # python-version: ${{ matrix.python-version }} - # - name: Install flake8 - # run: pip install flake8 - # - name: Run checks - # run: flake8 -v . + flake8: + name: Lint Python Code (Flake8) (python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11"] + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + - name: Set up Python v${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install flake8 + run: pip install flake8 + - name: Run checks + run: flake8 -v rocrate_validator tests test: name: "Tests" runs-on: ubuntu-latest - # needs: [flake8] + needs: [flake8] steps: - name: Checkout uses: actions/checkout@v4 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..68664469 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,11 @@ +[flake8] +max-line-length = 120 +exclude = + .git + .github + .vscode + .venv + __pycache__ + build + dist + rocrate_validator.egg-info From 6f0e0923cb2267c02d50dc4836bc3a232b9fda0d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Sep 2024 13:59:31 +0200 Subject: [PATCH 812/902] refactor: :rotating_light: fix flake8 warning W291 --- rocrate_validator/cli/main.py | 2 +- rocrate_validator/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index 778373be..59ae62ba 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -86,7 +86,7 @@ def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool, no_ 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. + console.print("""This error may be due to a bug. Please report it to the issue tracker along with the following stack trace: """) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 0c2fec69..22119a3e 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1240,7 +1240,7 @@ def parse(cls, settings: Union[dict, ValidationSettings]) -> ValidationSettings: class ValidationEvent(Event): - def __init__(self, event_type: EventType, + def __init__(self, event_type: EventType, validation_result: Optional[ValidationResult] = None, message: Optional[str] = None): super().__init__(event_type, message) self._validation_result = validation_result From e86563a3fd2377ef47ed7ef975cecc879441cf70 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 6 Sep 2024 14:02:38 +0200 Subject: [PATCH 813/902] fix: :rotating_light: fix flake8 warning F601 --- rocrate_validator/cli/commands/validate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 0a43bba0..7a2892a9 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -394,7 +394,6 @@ def multiple_choice(console: Console, ] console.print("\n") selected = prompt(question, style={"questionmark": "#ff9d00 bold", - "questionmark": "#e5c07b", "question": "bold", "checkbox": "magenta", "answer": "magenta"}, From 22818f16f514aa3c3e1ebbc20c8831790f16f9b0 Mon Sep 17 00:00:00 2001 From: simleo Date: Fri, 26 Jul 2024 17:08:44 +0200 Subject: [PATCH 814/902] wtroc: profile.ttl --- .../workflow-testing-ro-crate/profile.ttl | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 rocrate_validator/profiles/workflow-testing-ro-crate/profile.ttl diff --git a/rocrate_validator/profiles/workflow-testing-ro-crate/profile.ttl b/rocrate_validator/profiles/workflow-testing-ro-crate/profile.ttl new file mode 100644 index 00000000..0b7d64f0 --- /dev/null +++ b/rocrate_validator/profiles/workflow-testing-ro-crate/profile.ttl @@ -0,0 +1,67 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Workflow Testing RO-Crate Metadata Specification 0.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 Workflow RO-Crate + prof:isProfileOf ; + + # This profile is a transitive profile of Workflow RO-Crate and the RO-Crate Metadata Specification + 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 refered 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 refered 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.1 + prof:isInheritedFrom ; + ] ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "workflow-testing-ro-crate" ; +. From c29b20a49e455ab31dc9314e30398fae12f48ee3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 10 Sep 2024 11:41:58 +0200 Subject: [PATCH 815/902] feat(cli): :sparkles: wrap console object --- rocrate_validator/cli/commands/validate.py | 3 +-- rocrate_validator/cli/main.py | 3 +-- rocrate_validator/cli/utils.py | 13 +++++++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 7a2892a9..e31a2295 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -24,7 +24,6 @@ from InquirerPy import prompt from InquirerPy.base.control import Choice from rich.align import Align -from rich.console import Console from rich.layout import Layout from rich.live import Live from rich.markdown import Markdown @@ -38,7 +37,7 @@ from rocrate_validator import services from rocrate_validator.cli.commands.errors import handle_error from rocrate_validator.cli.main import cli, click -from rocrate_validator.cli.utils import get_app_header_rule +from rocrate_validator.cli.utils import Console, get_app_header_rule from rocrate_validator.colors import get_severity_color from rocrate_validator.events import Event, EventType, Subscriber from rocrate_validator.models import (LevelCollection, Profile, Severity, diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index 59ae62ba..c3876e19 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -15,10 +15,9 @@ import sys import rich_click as click -from rich.console import Console import rocrate_validator.log as logging -from rocrate_validator.cli.utils import SystemPager +from rocrate_validator.cli.utils import Console, SystemPager from rocrate_validator.utils import get_version # set up logging diff --git a/rocrate_validator/cli/utils.py b/rocrate_validator/cli/utils.py index 42e3a386..0a065d3f 100644 --- a/rocrate_validator/cli/utils.py +++ b/rocrate_validator/cli/utils.py @@ -18,6 +18,7 @@ import textwrap from typing import Any, Optional +from rich.console import Console as BaseConsole from rich.padding import Padding from rich.pager import Pager from rich.rule import Rule @@ -62,3 +63,15 @@ def _pager(self, content: str) -> Any: def show(self, content: str) -> None: """Use the same pager used by pydoc.""" self._pager(content) + + +class Console(BaseConsole): + """Rich Console with optional color disabling.""" + + def __init__(self, *args, disabled: bool = False, **kwargs): + super().__init__(*args, **kwargs) + self.disabled = disabled + + def print(self, *args, **kwargs): + if not self.disabled: + super().print(*args, **kwargs) From 7ec5ddb409cadd38389774659a82955c0e3be962 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 10 Sep 2024 11:48:06 +0200 Subject: [PATCH 816/902] feat(cli): :sparkles: do not print formatted validation report when format is different `text` --- rocrate_validator/cli/commands/validate.py | 23 ++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index e31a2295..d4cb0ffe 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -255,7 +255,8 @@ def validate(ctx, } # Print the application header - console.print(get_app_header_rule()) + if output_format == "text": + console.print(get_app_header_rule()) # Get the available profiles available_profiles = services.get_profiles(profiles_path) @@ -328,12 +329,18 @@ def validate(ctx, report_layout = ValidationReportLayout(console, validation_settings, profile_stats, None) # Validate RO-Crate against the profile and get the validation result - result: ValidationResult = report_layout.live( - lambda: services.validate( - validation_settings, - subscribers=[report_layout.progress_monitor] + result: ValidationResult = None + if output_format == "text": + result: ValidationResult = report_layout.live( + lambda: services.validate( + validation_settings, + subscribers=[report_layout.progress_monitor] + ) + ) + else: + result: ValidationResult = services.validate( + validation_settings ) - ) # store the cumulative validation result is_valid = is_valid and result.passed(LevelCollection.get(requirement_severity).severity) @@ -341,12 +348,12 @@ def validate(ctx, # Print the validation result if not result.passed(): verbose_choice = "n" - if interactive and not verbose and enable_pager: + if interactive and not verbose and enable_pager and not output_format == "json": verbose_choice = get_single_char(console, choices=['y', 'n'], message=( "[bold] > Do you want to see the validation details? " "([magenta]y/n[/magenta]): [/bold]" - )) + )) if verbose_choice == "y" or verbose: report_layout.show_validation_details(pager, enable_pager=enable_pager) From c4a2276fdc4136d04ee13220f9e7f2c1e243b38d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 10 Sep 2024 11:49:03 +0200 Subject: [PATCH 817/902] feat(cli): :sparkles: output JSON to stdout when `--format=json` --- rocrate_validator/cli/commands/validate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index d4cb0ffe..73df2171 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -357,6 +357,9 @@ def validate(ctx, if verbose_choice == "y" or verbose: report_layout.show_validation_details(pager, enable_pager=enable_pager) + if output_format == "json": + console.print(result.to_json()) + if output_file: # Print the validation report to a file if output_format == "json": From af846fc746399b27efa828e94cdb76371942efd5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 10 Sep 2024 12:09:31 +0200 Subject: [PATCH 818/902] refactor(cli): :wastebasket: remove internal settings from the JSON report --- rocrate_validator/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 22119a3e..8c356783 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1138,9 +1138,11 @@ def __eq__(self, other: object) -> bool: return self._issues == other._issues def to_dict(self) -> dict: + allowed_properties = ["data_path", "profiles_path", + "profile_identifier", "inherit_profiles", "requirement_severity", "abort_on_first"] return { "rocrate": str(self.rocrate_path), - "validation_settings": self.validation_settings, + "validation_settings": {key: self.validation_settings[key] for key in allowed_properties if key in self.validation_settings}, "passed": self.passed(self.context.settings["requirement_severity"]), "issues": [issue.to_dict() for issue in self.issues] } From c6dd4530608ff7f85670ac0b72d438c1d09b22dc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 10 Sep 2024 12:14:43 +0200 Subject: [PATCH 819/902] refactor(docs): :bulb: update comment --- rocrate_validator/cli/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/cli/utils.py b/rocrate_validator/cli/utils.py index 0a065d3f..5ad48c7c 100644 --- a/rocrate_validator/cli/utils.py +++ b/rocrate_validator/cli/utils.py @@ -66,7 +66,7 @@ def show(self, content: str) -> None: class Console(BaseConsole): - """Rich Console with optional color disabling.""" + """Rich console that can be disabled.""" def __init__(self, *args, disabled: bool = False, **kwargs): super().__init__(*args, **kwargs) From d5cabfd29f498fe479bfa0fbb11a5c0290a5957a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 10 Sep 2024 12:26:40 +0200 Subject: [PATCH 820/902] build(core): :arrow_up: upgrade PySHACL to 0.26.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6bd39cb1..d7b7296d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ include = [ [tool.poetry.dependencies] python = "^3.8.1" rdflib = "^7.0.0" -pyshacl = "^0.25.0" +pyshacl = "^0.26.0" click = "^8.1.7" rich = "^13.7.1" toml = "^0.10.2" From 9385af81d135c1a5f87d265b40c5dd5b2ceb1a64 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 10 Sep 2024 12:27:12 +0200 Subject: [PATCH 821/902] build(core): :arrow_up: upgrade rich to 13.8.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d7b7296d..59f3cfab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ python = "^3.8.1" rdflib = "^7.0.0" pyshacl = "^0.26.0" click = "^8.1.7" -rich = "^13.7.1" +rich = "^13.8.0" toml = "^0.10.2" rich-click = "^1.7.3" colorlog = "^6.8" From f092d73cc98fc3b3382c537cc74cf7e1eafca88a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 10 Sep 2024 12:28:06 +0200 Subject: [PATCH 822/902] build(core): :arrow_up: upgrade rich-click to 1.8.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 59f3cfab..fab6119e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ pyshacl = "^0.26.0" click = "^8.1.7" rich = "^13.8.0" toml = "^0.10.2" -rich-click = "^1.7.3" +rich-click = "^1.8.3" colorlog = "^6.8" requests = "^2.32.3" requests-cache = "^1.2.1" From 337aab77c0c12c635e3a1f200ced8f14ad8fc961 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 10 Sep 2024 12:30:14 +0200 Subject: [PATCH 823/902] refactor: :art: reformat --- pyproject.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fab6119e..56fdca63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,11 @@ name = "rocrate-validator" version = "0.1.0" description = "" -authors = ["Marco Enrico Piras ", "Luca Pireddu ", "Simone Leo "] +authors = [ + "Marco Enrico Piras ", + "Luca Pireddu ", + "Simone Leo ", +] readme = "README.md" packages = [{ include = "rocrate_validator", from = "." }] include = [ @@ -31,8 +35,8 @@ rich-click = "^1.8.3" colorlog = "^6.8" requests = "^2.32.3" requests-cache = "^1.2.1" - inquirerpy = "^0.3.4" + [tool.poetry.group.dev.dependencies] pyproject-flake8 = "^6.1.0" pylint = "^3.1.0" From 73c944cf40a2c11d9ebf564d2783b0e768f801d3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 10 Sep 2024 12:31:24 +0200 Subject: [PATCH 824/902] build: :construction_worker: update lock file --- poetry.lock | 649 ++++++++++++++++++++++++++++------------------------ 1 file changed, 355 insertions(+), 294 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4d3d2f45..5ac8c403 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,13 +13,13 @@ files = [ [[package]] name = "astroid" -version = "3.2.2" +version = "3.2.4" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.8.0" files = [ - {file = "astroid-3.2.2-py3-none-any.whl", hash = "sha256:e8a0083b4bb28fcffb6207a3bfc9e5d0a68be951dd7e336d5dcf639c682388c0"}, - {file = "astroid-3.2.2.tar.gz", hash = "sha256:8ead48e31b92b2e217b6c9733a21afafe479d52d6e164dd25fb1a770c7c3cf94"}, + {file = "astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"}, + {file = "astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a"}, ] [package.dependencies] @@ -45,22 +45,22 @@ test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] [[package]] name = "attrs" -version = "23.2.0" +version = "24.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, ] [package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "backcall" @@ -75,13 +75,13 @@ files = [ [[package]] name = "cattrs" -version = "23.2.3" +version = "24.1.0" description = "Composable complex class support for attrs and dataclasses." optional = false python-versions = ">=3.8" files = [ - {file = "cattrs-23.2.3-py3-none-any.whl", hash = "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108"}, - {file = "cattrs-23.2.3.tar.gz", hash = "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"}, + {file = "cattrs-24.1.0-py3-none-any.whl", hash = "sha256:043bb8af72596432a7df63abcff0055ac0f198a4d2e95af8db5a936a7074a761"}, + {file = "cattrs-24.1.0.tar.gz", hash = "sha256:8274f18b253bf7674a43da851e3096370d67088165d23138b04a1c04c8eaf48e"}, ] [package.dependencies] @@ -93,6 +93,7 @@ typing-extensions = {version = ">=4.1.0,<4.6.3 || >4.6.3", markers = "python_ver bson = ["pymongo (>=4.4.0)"] cbor2 = ["cbor2 (>=5.4.6)"] msgpack = ["msgpack (>=1.0.5)"] +msgspec = ["msgspec (>=0.18.5)"] orjson = ["orjson (>=3.9.2)"] pyyaml = ["pyyaml (>=6.0)"] tomlkit = ["tomlkit (>=0.11.8)"] @@ -100,74 +101,89 @@ ujson = ["ujson (>=5.7.0)"] [[package]] name = "certifi" -version = "2024.6.2" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] name = "cffi" -version = "1.16.0" +version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [package.dependencies] @@ -333,63 +349,83 @@ test = ["pytest"] [[package]] name = "coverage" -version = "7.5.4" +version = "7.6.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"}, - {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"}, - {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"}, - {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"}, - {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"}, - {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"}, - {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"}, - {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"}, - {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"}, - {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"}, - {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"}, - {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"}, - {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"}, - {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"}, - {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"}, - {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"}, - {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"}, - {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"}, - {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"}, - {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"}, - {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"}, - {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"}, - {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"}, - {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"}, - {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"}, - {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"}, - {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"}, - {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"}, - {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"}, - {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"}, - {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"}, - {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"}, - {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"}, - {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"}, - {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"}, - {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"}, - {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"}, - {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"}, - {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"}, - {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"}, - {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"}, - {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"}, - {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"}, - {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"}, - {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"}, - {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"}, - {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"}, - {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"}, - {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"}, - {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"}, - {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"}, - {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, ] [package.dependencies] @@ -400,33 +436,33 @@ toml = ["tomli"] [[package]] name = "debugpy" -version = "1.8.2" +version = "1.8.5" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" files = [ - {file = "debugpy-1.8.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:7ee2e1afbf44b138c005e4380097d92532e1001580853a7cb40ed84e0ef1c3d2"}, - {file = "debugpy-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f8c3f7c53130a070f0fc845a0f2cee8ed88d220d6b04595897b66605df1edd6"}, - {file = "debugpy-1.8.2-cp310-cp310-win32.whl", hash = "sha256:f179af1e1bd4c88b0b9f0fa153569b24f6b6f3de33f94703336363ae62f4bf47"}, - {file = "debugpy-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:0600faef1d0b8d0e85c816b8bb0cb90ed94fc611f308d5fde28cb8b3d2ff0fe3"}, - {file = "debugpy-1.8.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8a13417ccd5978a642e91fb79b871baded925d4fadd4dfafec1928196292aa0a"}, - {file = "debugpy-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acdf39855f65c48ac9667b2801234fc64d46778021efac2de7e50907ab90c634"}, - {file = "debugpy-1.8.2-cp311-cp311-win32.whl", hash = "sha256:2cbd4d9a2fc5e7f583ff9bf11f3b7d78dfda8401e8bb6856ad1ed190be4281ad"}, - {file = "debugpy-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:d3408fddd76414034c02880e891ea434e9a9cf3a69842098ef92f6e809d09afa"}, - {file = "debugpy-1.8.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:5d3ccd39e4021f2eb86b8d748a96c766058b39443c1f18b2dc52c10ac2757835"}, - {file = "debugpy-1.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62658aefe289598680193ff655ff3940e2a601765259b123dc7f89c0239b8cd3"}, - {file = "debugpy-1.8.2-cp312-cp312-win32.whl", hash = "sha256:bd11fe35d6fd3431f1546d94121322c0ac572e1bfb1f6be0e9b8655fb4ea941e"}, - {file = "debugpy-1.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:15bc2f4b0f5e99bf86c162c91a74c0631dbd9cef3c6a1d1329c946586255e859"}, - {file = "debugpy-1.8.2-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:5a019d4574afedc6ead1daa22736c530712465c0c4cd44f820d803d937531b2d"}, - {file = "debugpy-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40f062d6877d2e45b112c0bbade9a17aac507445fd638922b1a5434df34aed02"}, - {file = "debugpy-1.8.2-cp38-cp38-win32.whl", hash = "sha256:c78ba1680f1015c0ca7115671fe347b28b446081dada3fedf54138f44e4ba031"}, - {file = "debugpy-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:cf327316ae0c0e7dd81eb92d24ba8b5e88bb4d1b585b5c0d32929274a66a5210"}, - {file = "debugpy-1.8.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1523bc551e28e15147815d1397afc150ac99dbd3a8e64641d53425dba57b0ff9"}, - {file = "debugpy-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e24ccb0cd6f8bfaec68d577cb49e9c680621c336f347479b3fce060ba7c09ec1"}, - {file = "debugpy-1.8.2-cp39-cp39-win32.whl", hash = "sha256:7f8d57a98c5a486c5c7824bc0b9f2f11189d08d73635c326abef268f83950326"}, - {file = "debugpy-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:16c8dcab02617b75697a0a925a62943e26a0330da076e2a10437edd9f0bf3755"}, - {file = "debugpy-1.8.2-py2.py3-none-any.whl", hash = "sha256:16e16df3a98a35c63c3ab1e4d19be4cbc7fdda92d9ddc059294f18910928e0ca"}, - {file = "debugpy-1.8.2.zip", hash = "sha256:95378ed08ed2089221896b9b3a8d021e642c24edc8fef20e5d4342ca8be65c00"}, + {file = "debugpy-1.8.5-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7e4d594367d6407a120b76bdaa03886e9eb652c05ba7f87e37418426ad2079f7"}, + {file = "debugpy-1.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4413b7a3ede757dc33a273a17d685ea2b0c09dbd312cc03f5534a0fd4d40750a"}, + {file = "debugpy-1.8.5-cp310-cp310-win32.whl", hash = "sha256:dd3811bd63632bb25eda6bd73bea8e0521794cda02be41fa3160eb26fc29e7ed"}, + {file = "debugpy-1.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:b78c1250441ce893cb5035dd6f5fc12db968cc07f91cc06996b2087f7cefdd8e"}, + {file = "debugpy-1.8.5-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:606bccba19f7188b6ea9579c8a4f5a5364ecd0bf5a0659c8a5d0e10dcee3032a"}, + {file = "debugpy-1.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db9fb642938a7a609a6c865c32ecd0d795d56c1aaa7a7a5722d77855d5e77f2b"}, + {file = "debugpy-1.8.5-cp311-cp311-win32.whl", hash = "sha256:4fbb3b39ae1aa3e5ad578f37a48a7a303dad9a3d018d369bc9ec629c1cfa7408"}, + {file = "debugpy-1.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:345d6a0206e81eb68b1493ce2fbffd57c3088e2ce4b46592077a943d2b968ca3"}, + {file = "debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156"}, + {file = "debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb"}, + {file = "debugpy-1.8.5-cp312-cp312-win32.whl", hash = "sha256:c9f7c15ea1da18d2fcc2709e9f3d6de98b69a5b0fff1807fb80bc55f906691f7"}, + {file = "debugpy-1.8.5-cp312-cp312-win_amd64.whl", hash = "sha256:28ced650c974aaf179231668a293ecd5c63c0a671ae6d56b8795ecc5d2f48d3c"}, + {file = "debugpy-1.8.5-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:3df6692351172a42af7558daa5019651f898fc67450bf091335aa8a18fbf6f3a"}, + {file = "debugpy-1.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd04a73eb2769eb0bfe43f5bfde1215c5923d6924b9b90f94d15f207a402226"}, + {file = "debugpy-1.8.5-cp38-cp38-win32.whl", hash = "sha256:8f913ee8e9fcf9d38a751f56e6de12a297ae7832749d35de26d960f14280750a"}, + {file = "debugpy-1.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:a697beca97dad3780b89a7fb525d5e79f33821a8bc0c06faf1f1289e549743cf"}, + {file = "debugpy-1.8.5-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0a1029a2869d01cb777216af8c53cda0476875ef02a2b6ff8b2f2c9a4b04176c"}, + {file = "debugpy-1.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84c276489e141ed0b93b0af648eef891546143d6a48f610945416453a8ad406"}, + {file = "debugpy-1.8.5-cp39-cp39-win32.whl", hash = "sha256:ad84b7cde7fd96cf6eea34ff6c4a1b7887e0fe2ea46e099e53234856f9d99a34"}, + {file = "debugpy-1.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:7b0fe36ed9d26cb6836b0a51453653f8f2e347ba7348f2bbfe76bfeb670bfb1c"}, + {file = "debugpy-1.8.5-py2.py3-none-any.whl", hash = "sha256:55919dce65b471eff25901acf82d328bbd5b833526b6c1364bd5133754777a44"}, + {file = "debugpy-1.8.5.zip", hash = "sha256:b2112cfeb34b4507399d298fe7023a16656fc553ed5246536060ca7bd0e668d0"}, ] [[package]] @@ -457,13 +493,13 @@ profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "exceptiongroup" -version = "1.2.1" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, - {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -485,13 +521,13 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "executing" -version = "2.0.1" +version = "2.1.0" description = "Get the currently executing AST node of a frame, and other information" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, - {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, + {file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"}, + {file = "executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"}, ] [package.extras] @@ -536,24 +572,24 @@ lxml = ["lxml"] [[package]] name = "idna" -version = "3.7" +version = "3.8" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, + {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, ] [[package]] name = "importlib-metadata" -version = "8.0.0" +version = "8.4.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, - {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, + {file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"}, + {file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"}, ] [package.dependencies] @@ -595,13 +631,13 @@ docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17-beta.43,<2022.0.0)", "myst [[package]] name = "ipykernel" -version = "6.29.4" +version = "6.29.5" description = "IPython Kernel for Jupyter" optional = false python-versions = ">=3.8" files = [ - {file = "ipykernel-6.29.4-py3-none-any.whl", hash = "sha256:1181e653d95c6808039c509ef8e67c4126b3b3af7781496c7cbfb5ed938a27da"}, - {file = "ipykernel-6.29.4.tar.gz", hash = "sha256:3d44070060f9475ac2092b760123fadf105d2e2493c24848b6691a7c4f42af5c"}, + {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, + {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"}, ] [package.dependencies] @@ -907,19 +943,19 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.2" +version = "4.3.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"}, + {file = "platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pluggy" @@ -938,13 +974,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "prettytable" -version = "3.10.0" +version = "3.11.0" description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" optional = false python-versions = ">=3.8" files = [ - {file = "prettytable-3.10.0-py3-none-any.whl", hash = "sha256:6536efaf0757fdaa7d22e78b3aac3b69ea1b7200538c2c6995d649365bddab92"}, - {file = "prettytable-3.10.0.tar.gz", hash = "sha256:9665594d137fb08a1117518c25551e0ede1687197cf353a4fdc78d27e1073568"}, + {file = "prettytable-3.11.0-py3-none-any.whl", hash = "sha256:aa17083feb6c71da11a68b2c213b04675c4af4ce9c541762632ca3f2cb3546dd"}, + {file = "prettytable-3.11.0.tar.gz", hash = "sha256:7e23ca1e68bbfd06ba8de98bf553bf3493264c96d5e8a615c0471025deeba722"}, ] [package.dependencies] @@ -1009,13 +1045,13 @@ files = [ [[package]] name = "pure-eval" -version = "0.2.2" +version = "0.2.3" description = "Safely evaluate AST nodes without side effects" optional = false python-versions = "*" files = [ - {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, - {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, + {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, + {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, ] [package.extras] @@ -1070,22 +1106,22 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "3.2.4" +version = "3.2.7" description = "python code static checker" optional = false python-versions = ">=3.8.0" files = [ - {file = "pylint-3.2.4-py3-none-any.whl", hash = "sha256:43b8ffdf1578e4e4439fa1f6ace402281f5dd61999192280fa12fe411bef2999"}, - {file = "pylint-3.2.4.tar.gz", hash = "sha256:5753d27e49a658b12a48c2883452751a2ecfc7f38594e0980beb03a6e77e6f86"}, + {file = "pylint-3.2.7-py3-none-any.whl", hash = "sha256:02f4aedeac91be69fb3b4bea997ce580a4ac68ce58b89eaefeaf06749df73f4b"}, + {file = "pylint-3.2.7.tar.gz", hash = "sha256:1b7a721b575eaeaa7d39db076b6e7743c993ea44f57979127c517c6c572c803e"}, ] [package.dependencies] -astroid = ">=3.2.2,<=3.3.0-dev0" +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.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, ] isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" mccabe = ">=0.6,<0.8" @@ -1100,13 +1136,13 @@ testutils = ["gitpython (>3)"] [[package]] name = "pyparsing" -version = "3.1.2" +version = "3.1.4" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.6.8" files = [ - {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, - {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, + {file = "pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c"}, + {file = "pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032"}, ] [package.extras] @@ -1129,13 +1165,13 @@ tomli = {version = "*", markers = "python_version < \"3.11\""} [[package]] name = "pyshacl" -version = "0.25.0" +version = "0.26.0" description = "Python SHACL Validator" optional = false -python-versions = ">=3.8.1,<4.0.0" +python-versions = "<4.0.0,>=3.8.1" files = [ - {file = "pyshacl-0.25.0-py3-none-any.whl", hash = "sha256:716b65397486b1a306efefd018d772d3c112a3828ea4e1be27aae16aee524243"}, - {file = "pyshacl-0.25.0.tar.gz", hash = "sha256:91e87ed04ccb29aa47abfcf8a3e172d35a8831fce23a011cfbf35534ce4c940b"}, + {file = "pyshacl-0.26.0-py3-none-any.whl", hash = "sha256:a4bef4296d56305a30e0a97509e541ebe4f2cc2d5da73536d0541233e28f2d22"}, + {file = "pyshacl-0.26.0.tar.gz", hash = "sha256:48d44f317cd9aad8e3fdb5df8aa5706fa92dc6b2746419698035e84a320fb89d"}, ] [package.dependencies] @@ -1151,20 +1187,20 @@ rdflib = {version = ">=6.3.2,<8.0", markers = "python_full_version >= \"3.8.1\"" [package.extras] dev-coverage = ["coverage (>6.1,!=6.1.1,<7)", "platformdirs", "pytest-cov (>=2.8.1,<3.0.0)"] -dev-lint = ["black (==23.11.0)", "platformdirs", "ruff (>=0.1.5,<0.2.0)"] +dev-lint = ["black (==24.3.0)", "platformdirs", "ruff (>=0.1.5,<0.2.0)"] dev-type-checking = ["mypy (>=0.812,<0.900)", "mypy (>=0.900,<0.1000)", "platformdirs", "types-setuptools"] http = ["sanic (>=22.12,<23)", "sanic-cors (==2.2.0)", "sanic-ext (>=23.3,<23.6)"] js = ["pyduktape2 (>=0.4.6,<0.5.0)"] [[package]] name = "pytest" -version = "8.2.2" +version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [package.dependencies] @@ -1172,7 +1208,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.5,<2.0" +pluggy = ">=1.5,<2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] @@ -1255,99 +1291,120 @@ files = [ [[package]] name = "pyzmq" -version = "26.0.3" +version = "26.2.0" description = "Python bindings for 0MQ" optional = false python-versions = ">=3.7" files = [ - {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:44dd6fc3034f1eaa72ece33588867df9e006a7303725a12d64c3dff92330f625"}, - {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:acb704195a71ac5ea5ecf2811c9ee19ecdc62b91878528302dd0be1b9451cc90"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbb9c997932473a27afa93954bb77a9f9b786b4ccf718d903f35da3232317de"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bcb34f869d431799c3ee7d516554797f7760cb2198ecaa89c3f176f72d062be"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ece17ec5f20d7d9b442e5174ae9f020365d01ba7c112205a4d59cf19dc38ee"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ba6e5e6588e49139a0979d03a7deb9c734bde647b9a8808f26acf9c547cab1bf"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3bf8b000a4e2967e6dfdd8656cd0757d18c7e5ce3d16339e550bd462f4857e59"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2136f64fbb86451dbbf70223635a468272dd20075f988a102bf8a3f194a411dc"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e8918973fbd34e7814f59143c5f600ecd38b8038161239fd1a3d33d5817a38b8"}, - {file = "pyzmq-26.0.3-cp310-cp310-win32.whl", hash = "sha256:0aaf982e68a7ac284377d051c742610220fd06d330dcd4c4dbb4cdd77c22a537"}, - {file = "pyzmq-26.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:f1a9b7d00fdf60b4039f4455afd031fe85ee8305b019334b72dcf73c567edc47"}, - {file = "pyzmq-26.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:80b12f25d805a919d53efc0a5ad7c0c0326f13b4eae981a5d7b7cc343318ebb7"}, - {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:a72a84570f84c374b4c287183debc776dc319d3e8ce6b6a0041ce2e400de3f32"}, - {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ca684ee649b55fd8f378127ac8462fb6c85f251c2fb027eb3c887e8ee347bcd"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e222562dc0f38571c8b1ffdae9d7adb866363134299264a1958d077800b193b7"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f17cde1db0754c35a91ac00b22b25c11da6eec5746431d6e5092f0cd31a3fea9"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7c0c0b3244bb2275abe255d4a30c050d541c6cb18b870975553f1fb6f37527"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac97a21de3712afe6a6c071abfad40a6224fd14fa6ff0ff8d0c6e6cd4e2f807a"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:88b88282e55fa39dd556d7fc04160bcf39dea015f78e0cecec8ff4f06c1fc2b5"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:72b67f966b57dbd18dcc7efbc1c7fc9f5f983e572db1877081f075004614fcdd"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4b6cecbbf3b7380f3b61de3a7b93cb721125dc125c854c14ddc91225ba52f83"}, - {file = "pyzmq-26.0.3-cp311-cp311-win32.whl", hash = "sha256:eed56b6a39216d31ff8cd2f1d048b5bf1700e4b32a01b14379c3b6dde9ce3aa3"}, - {file = "pyzmq-26.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:3191d312c73e3cfd0f0afdf51df8405aafeb0bad71e7ed8f68b24b63c4f36500"}, - {file = "pyzmq-26.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:b6907da3017ef55139cf0e417c5123a84c7332520e73a6902ff1f79046cd3b94"}, - {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:068ca17214038ae986d68f4a7021f97e187ed278ab6dccb79f837d765a54d753"}, - {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7821d44fe07335bea256b9f1f41474a642ca55fa671dfd9f00af8d68a920c2d4"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb438a26d87c123bb318e5f2b3d86a36060b01f22fbdffd8cf247d52f7c9a2b"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69ea9d6d9baa25a4dc9cef5e2b77b8537827b122214f210dd925132e34ae9b12"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7daa3e1369355766dea11f1d8ef829905c3b9da886ea3152788dc25ee6079e02"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6ca7a9a06b52d0e38ccf6bca1aeff7be178917893f3883f37b75589d42c4ac20"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1b7d0e124948daa4d9686d421ef5087c0516bc6179fdcf8828b8444f8e461a77"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e746524418b70f38550f2190eeee834db8850088c834d4c8406fbb9bc1ae10b2"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6b3146f9ae6af82c47a5282ac8803523d381b3b21caeae0327ed2f7ecb718798"}, - {file = "pyzmq-26.0.3-cp312-cp312-win32.whl", hash = "sha256:2b291d1230845871c00c8462c50565a9cd6026fe1228e77ca934470bb7d70ea0"}, - {file = "pyzmq-26.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:926838a535c2c1ea21c903f909a9a54e675c2126728c21381a94ddf37c3cbddf"}, - {file = "pyzmq-26.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:5bf6c237f8c681dfb91b17f8435b2735951f0d1fad10cc5dfd96db110243370b"}, - {file = "pyzmq-26.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c0991f5a96a8e620f7691e61178cd8f457b49e17b7d9cfa2067e2a0a89fc1d5"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dbf012d8fcb9f2cf0643b65df3b355fdd74fc0035d70bb5c845e9e30a3a4654b"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:01fbfbeb8249a68d257f601deb50c70c929dc2dfe683b754659569e502fbd3aa"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c8eb19abe87029c18f226d42b8a2c9efdd139d08f8bf6e085dd9075446db450"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5344b896e79800af86ad643408ca9aa303a017f6ebff8cee5a3163c1e9aec987"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:204e0f176fd1d067671157d049466869b3ae1fc51e354708b0dc41cf94e23a3a"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a42db008d58530efa3b881eeee4991146de0b790e095f7ae43ba5cc612decbc5"}, - {file = "pyzmq-26.0.3-cp37-cp37m-win32.whl", hash = "sha256:8d7a498671ca87e32b54cb47c82a92b40130a26c5197d392720a1bce1b3c77cf"}, - {file = "pyzmq-26.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:3b4032a96410bdc760061b14ed6a33613ffb7f702181ba999df5d16fb96ba16a"}, - {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2cc4e280098c1b192c42a849de8de2c8e0f3a84086a76ec5b07bfee29bda7d18"}, - {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bde86a2ed3ce587fa2b207424ce15b9a83a9fa14422dcc1c5356a13aed3df9d"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34106f68e20e6ff253c9f596ea50397dbd8699828d55e8fa18bd4323d8d966e6"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ebbbd0e728af5db9b04e56389e2299a57ea8b9dd15c9759153ee2455b32be6ad"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b1d1c631e5940cac5a0b22c5379c86e8df6a4ec277c7a856b714021ab6cfad"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e891ce81edd463b3b4c3b885c5603c00141151dd9c6936d98a680c8c72fe5c67"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9b273ecfbc590a1b98f014ae41e5cf723932f3b53ba9367cfb676f838038b32c"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b32bff85fb02a75ea0b68f21e2412255b5731f3f389ed9aecc13a6752f58ac97"}, - {file = "pyzmq-26.0.3-cp38-cp38-win32.whl", hash = "sha256:f6c21c00478a7bea93caaaef9e7629145d4153b15a8653e8bb4609d4bc70dbfc"}, - {file = "pyzmq-26.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:3401613148d93ef0fd9aabdbddb212de3db7a4475367f49f590c837355343972"}, - {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:2ed8357f4c6e0daa4f3baf31832df8a33334e0fe5b020a61bc8b345a3db7a606"}, - {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1c8f2a2ca45292084c75bb6d3a25545cff0ed931ed228d3a1810ae3758f975f"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b63731993cdddcc8e087c64e9cf003f909262b359110070183d7f3025d1c56b5"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b3cd31f859b662ac5d7f4226ec7d8bd60384fa037fc02aee6ff0b53ba29a3ba8"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115f8359402fa527cf47708d6f8a0f8234f0e9ca0cab7c18c9c189c194dbf620"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:715bdf952b9533ba13dfcf1f431a8f49e63cecc31d91d007bc1deb914f47d0e4"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e1258c639e00bf5e8a522fec6c3eaa3e30cf1c23a2f21a586be7e04d50c9acab"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15c59e780be8f30a60816a9adab900c12a58d79c1ac742b4a8df044ab2a6d920"}, - {file = "pyzmq-26.0.3-cp39-cp39-win32.whl", hash = "sha256:d0cdde3c78d8ab5b46595054e5def32a755fc028685add5ddc7403e9f6de9879"}, - {file = "pyzmq-26.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ce828058d482ef860746bf532822842e0ff484e27f540ef5c813d516dd8896d2"}, - {file = "pyzmq-26.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:788f15721c64109cf720791714dc14afd0f449d63f3a5487724f024345067381"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c18645ef6294d99b256806e34653e86236eb266278c8ec8112622b61db255de"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e6bc96ebe49604df3ec2c6389cc3876cabe475e6bfc84ced1bf4e630662cb35"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:971e8990c5cc4ddcff26e149398fc7b0f6a042306e82500f5e8db3b10ce69f84"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8416c23161abd94cc7da80c734ad7c9f5dbebdadfdaa77dad78244457448223"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:082a2988364b60bb5de809373098361cf1dbb239623e39e46cb18bc035ed9c0c"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d57dfbf9737763b3a60d26e6800e02e04284926329aee8fb01049635e957fe81"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77a85dca4c2430ac04dc2a2185c2deb3858a34fe7f403d0a946fa56970cf60a1"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4c82a6d952a1d555bf4be42b6532927d2a5686dd3c3e280e5f63225ab47ac1f5"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4496b1282c70c442809fc1b151977c3d967bfb33e4e17cedbf226d97de18f709"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e4946d6bdb7ba972dfda282f9127e5756d4f299028b1566d1245fa0d438847e6"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03c0ae165e700364b266876d712acb1ac02693acd920afa67da2ebb91a0b3c09"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3e3070e680f79887d60feeda051a58d0ac36622e1759f305a41059eff62c6da7"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6ca08b840fe95d1c2bd9ab92dac5685f949fc6f9ae820ec16193e5ddf603c3b2"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e76654e9dbfb835b3518f9938e565c7806976c07b37c33526b574cc1a1050480"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:871587bdadd1075b112e697173e946a07d722459d20716ceb3d1bd6c64bd08ce"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d0a2d1bd63a4ad79483049b26514e70fa618ce6115220da9efdff63688808b17"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0270b49b6847f0d106d64b5086e9ad5dc8a902413b5dbbb15d12b60f9c1747a4"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:703c60b9910488d3d0954ca585c34f541e506a091a41930e663a098d3b794c67"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74423631b6be371edfbf7eabb02ab995c2563fee60a80a30829176842e71722a"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4adfbb5451196842a88fda3612e2c0414134874bffb1c2ce83ab4242ec9e027d"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3516119f4f9b8671083a70b6afaa0a070f5683e431ab3dc26e9215620d7ca1ad"}, - {file = "pyzmq-26.0.3.tar.gz", hash = "sha256:dba7d9f2e047dfa2bca3b01f4f84aa5246725203d6284e3790f2ca15fba6b40a"}, + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629"}, + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea"}, + {file = "pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6"}, + {file = "pyzmq-26.2.0-cp311-cp311-win32.whl", hash = "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b"}, + {file = "pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e"}, + {file = "pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0"}, + {file = "pyzmq-26.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b55a4229ce5da9497dd0452b914556ae58e96a4381bb6f59f1305dfd7e53fc8"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9cb3a6460cdea8fe8194a76de8895707e61ded10ad0be97188cc8463ffa7e3a8"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ab5cad923cc95c87bffee098a27856c859bd5d0af31bd346035aa816b081fe1"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ed69074a610fad1c2fda66180e7b2edd4d31c53f2d1872bc2d1211563904cd9"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cccba051221b916a4f5e538997c45d7d136a5646442b1231b916d0164067ea27"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0eaa83fc4c1e271c24eaf8fb083cbccef8fde77ec8cd45f3c35a9a123e6da097"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9edda2df81daa129b25a39b86cb57dfdfe16f7ec15b42b19bfac503360d27a93"}, + {file = "pyzmq-26.2.0-cp37-cp37m-win32.whl", hash = "sha256:ea0eb6af8a17fa272f7b98d7bebfab7836a0d62738e16ba380f440fceca2d951"}, + {file = "pyzmq-26.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4ff9dc6bc1664bb9eec25cd17506ef6672d506115095411e237d571e92a58231"}, + {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2eb7735ee73ca1b0d71e0e67c3739c689067f055c764f73aac4cc8ecf958ee3f"}, + {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a534f43bc738181aa7cbbaf48e3eca62c76453a40a746ab95d4b27b1111a7d2"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aedd5dd8692635813368e558a05266b995d3d020b23e49581ddd5bbe197a8ab6"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8be4700cd8bb02cc454f630dcdf7cfa99de96788b80c51b60fe2fe1dac480289"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcc03fa4997c447dce58264e93b5aa2d57714fbe0f06c07b7785ae131512732"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:402b190912935d3db15b03e8f7485812db350d271b284ded2b80d2e5704be780"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8685fa9c25ff00f550c1fec650430c4b71e4e48e8d852f7ddcf2e48308038640"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:76589c020680778f06b7e0b193f4b6dd66d470234a16e1df90329f5e14a171cd"}, + {file = "pyzmq-26.2.0-cp38-cp38-win32.whl", hash = "sha256:8423c1877d72c041f2c263b1ec6e34360448decfb323fa8b94e85883043ef988"}, + {file = "pyzmq-26.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:76589f2cd6b77b5bdea4fca5992dc1c23389d68b18ccc26a53680ba2dc80ff2f"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b1d464cb8d72bfc1a3adc53305a63a8e0cac6bc8c5a07e8ca190ab8d3faa43c2"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4da04c48873a6abdd71811c5e163bd656ee1b957971db7f35140a2d573f6949c"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d049df610ac811dcffdc147153b414147428567fbbc8be43bb8885f04db39d98"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05590cdbc6b902101d0e65d6a4780af14dc22914cc6ab995d99b85af45362cc9"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c811cfcd6a9bf680236c40c6f617187515269ab2912f3d7e8c0174898e2519db"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6835dd60355593de10350394242b5757fbbd88b25287314316f266e24c61d073"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc6bee759a6bddea5db78d7dcd609397449cb2d2d6587f48f3ca613b19410cfc"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c530e1eecd036ecc83c3407f77bb86feb79916d4a33d11394b8234f3bd35b940"}, + {file = "pyzmq-26.2.0-cp39-cp39-win32.whl", hash = "sha256:367b4f689786fca726ef7a6c5ba606958b145b9340a5e4808132cc65759abd44"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6fa2e3e683f34aea77de8112f6483803c96a44fd726d7358b9888ae5bb394ec"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:7445be39143a8aa4faec43b076e06944b8f9d0701b669df4af200531b21e40bb"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ea4ad4e6a12e454de05f2949d4beddb52460f3de7c8b9d5c46fbb7d7222e02c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fc4f7a173a5609631bb0c42c23d12c49df3966f89f496a51d3eb0ec81f4519d6"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:878206a45202247781472a2d99df12a176fef806ca175799e1c6ad263510d57c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17c412bad2eb9468e876f556eb4ee910e62d721d2c7a53c7fa31e643d35352e6"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0d987a3ae5a71c6226b203cfd298720e0086c7fe7c74f35fa8edddfbd6597eed"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:39887ac397ff35b7b775db7201095fc6310a35fdbae85bac4523f7eb3b840e20"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fdb5b3e311d4d4b0eb8b3e8b4d1b0a512713ad7e6a68791d0923d1aec433d919"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:226af7dcb51fdb0109f0016449b357e182ea0ceb6b47dfb5999d569e5db161d5"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bed0e799e6120b9c32756203fb9dfe8ca2fb8467fed830c34c877e25638c3fc"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:29c7947c594e105cb9e6c466bace8532dc1ca02d498684128b339799f5248277"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cdeabcff45d1c219636ee2e54d852262e5c2e085d6cb476d938aee8d921356b3"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35cffef589bcdc587d06f9149f8d5e9e8859920a071df5a2671de2213bef592a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18c8dc3b7468d8b4bdf60ce9d7141897da103c7a4690157b32b60acb45e333e6"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7133d0a1677aec369d67dd78520d3fa96dd7f3dcec99d66c1762870e5ea1a50a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a96179a24b14fa6428cbfc08641c779a53f8fcec43644030328f44034c7f1f4"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4f78c88905461a9203eac9faac157a2a0dbba84a0fd09fd29315db27be40af9f"}, + {file = "pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f"}, ] [package.dependencies] @@ -1427,13 +1484,13 @@ yaml = ["pyyaml (>=6.0.1)"] [[package]] name = "rich" -version = "13.7.1" +version = "13.8.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, + {file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"}, + {file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"}, ] [package.dependencies] @@ -1518,13 +1575,13 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.5" +version = "0.13.2" description = "Style preserving TOML library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, - {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] [[package]] @@ -1628,20 +1685,24 @@ files = [ [[package]] name = "zipp" -version = "3.19.2" +version = "3.20.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, - {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, + {file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"}, + {file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"}, ] [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "f0b0854a2c87e506f581c31abedafe79628055a4e65116ce8a48907e10d29bc3" +content-hash = "3973b73c6ba7eed27a4ebe8ce3f43654a2752abd6681a96802c06321ff977057" From 5ae11fd427fa3913a6b9a798703ae9f86d1bd1f7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 10 Sep 2024 12:32:56 +0200 Subject: [PATCH 825/902] build: :bookmark: update version number --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 56fdca63..09f2d5d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "rocrate-validator" -version = "0.1.0" +version = "0.1.1" description = "" authors = [ "Marco Enrico Piras ", From 6678dd53cf3d8d6ea57d85cfbdc7726bf27f63b6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 10 Sep 2024 12:37:40 +0200 Subject: [PATCH 826/902] refactor(utils): :rotating_light: fix flake8 warning E501 --- rocrate_validator/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 8c356783..734022ce 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1142,7 +1142,8 @@ def to_dict(self) -> dict: "profile_identifier", "inherit_profiles", "requirement_severity", "abort_on_first"] return { "rocrate": str(self.rocrate_path), - "validation_settings": {key: self.validation_settings[key] for key in allowed_properties if key in self.validation_settings}, + "validation_settings": {key: self.validation_settings[key] + for key in allowed_properties if key in self.validation_settings}, "passed": self.passed(self.context.settings["requirement_severity"]), "issues": [issue.to_dict() for issue in self.issues] } From 2a3cadba59f880f53620f743d36412fb5e387dfd Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 16 Sep 2024 16:18:06 +0200 Subject: [PATCH 827/902] wtroc: update profile.ttl --- .../workflow-testing-ro-crate/profile.ttl | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/rocrate_validator/profiles/workflow-testing-ro-crate/profile.ttl b/rocrate_validator/profiles/workflow-testing-ro-crate/profile.ttl index 0b7d64f0..7e0c5160 100644 --- a/rocrate_validator/profiles/workflow-testing-ro-crate/profile.ttl +++ b/rocrate_validator/profiles/workflow-testing-ro-crate/profile.ttl @@ -1,27 +1,40 @@ +# 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. + @prefix dct: . @prefix prof: . @prefix role: . @prefix rdfs: . - - - + a prof:Profile ; # the Profile's label - rdfs:label "Workflow Testing RO-Crate Metadata Specification 0.1" ; + rdfs:label "Workflow Testing RO-Crate 0.1" ; # regular metadata, a basic description of the Profile - rdfs:comment """Workflow RO-Crate Metadata Specification."""@en ; + rdfs:comment """Workflow Testing RO-Crate Metadata Specification 0.1"""@en ; - # URI of the publisher of the Workflow RO-Crate Metadata Specification - dct:publisher ; + # URI of the publisher of the Specification + dct:publisher ; # This profile is an extension of Workflow RO-Crate prof:isProfileOf ; # This profile is a transitive profile of Workflow RO-Crate and the RO-Crate Metadata Specification - prof:isTransitiveProfileOf , ; + prof:isTransitiveProfileOf , + ; # this profile has a JSON-LD context resource prof:hasResource [ @@ -56,9 +69,9 @@ prof:hasRole role:Specification ; # this profile resource's actual file - prof:hasArtifact ; + prof:hasArtifact ; - # this profile is inherited from the RO-Crate Metadata Specification 1.1 + # this profile is inherited from Workflow RO-Crate prof:isInheritedFrom ; ] ; From ba0f73ccb1ccb6a64cadd757aaa65a8f5aa0e40e Mon Sep 17 00:00:00 2001 From: simleo Date: Mon, 16 Sep 2024 17:26:50 +0200 Subject: [PATCH 828/902] wtroc: add two shapes --- .../must/1_test_suite.ttl | 35 +++++++++++++++++++ .../must/2_root_data_entity_metadata.ttl | 34 ++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 rocrate_validator/profiles/workflow-testing-ro-crate/must/1_test_suite.ttl create mode 100644 rocrate_validator/profiles/workflow-testing-ro-crate/must/2_root_data_entity_metadata.ttl diff --git a/rocrate_validator/profiles/workflow-testing-ro-crate/must/1_test_suite.ttl b/rocrate_validator/profiles/workflow-testing-ro-crate/must/1_test_suite.ttl new file mode 100644 index 00000000..aa3049c8 --- /dev/null +++ b/rocrate_validator/profiles/workflow-testing-ro-crate/must/1_test_suite.ttl @@ -0,0 +1,35 @@ +# 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. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-testing-ro-crate: . +@prefix schema: . +@prefix bioschemas: . +@prefix sh: . +@prefix wftest: . + +workflow-testing-ro-crate:WTROCTestSuiteRequired a sh:NodeShape ; + sh:name "Workflow Testing RO-Crate Action MUST" ; + sh:description "Required properties of the Workflow Testing RO-Crate TestSuite" ; + sh:targetClass wftest:TestSuite ; + sh:property [ + a sh:PropertyShape ; + sh:name "TestSuite MUST be referenced via mentions from root" ; + sh:description "The TestSuite MUST be referenced from the Root Data Entity via mentions" ; + sh:path [ sh:inversePath schema:mentions ] ; + sh:node ro-crate:RootDataEntity ; + sh:minCount 1 ; + sh:message "The TestSuite MUST be referenced from the Root Data Entity via mentions" ; + ] . diff --git a/rocrate_validator/profiles/workflow-testing-ro-crate/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/workflow-testing-ro-crate/must/2_root_data_entity_metadata.ttl new file mode 100644 index 00000000..40d91845 --- /dev/null +++ b/rocrate_validator/profiles/workflow-testing-ro-crate/must/2_root_data_entity_metadata.ttl @@ -0,0 +1,34 @@ +# 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. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix schema: . +@prefix sh: . +@prefix wftest: . +@prefix workflow-testing-ro-crate: . + +workflow-testing-ro-crate:WTROCRootDataEntityMetadata 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 mentions" ; + sh:description "The Root Data Entity MUST refer to one or more test suites via mentions" ; + sh:path schema:mentions ; + sh:class wftest:TestSuite; + sh:minCount 1 ; + sh:message "The Root Data Entity MUST refer to one or more test suites via mentions" ; + ] . From d853b6e801ddfea345945ac99de6417f5856cd5a Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 17 Sep 2024 12:23:20 +0200 Subject: [PATCH 829/902] wtroc: add some tests --- .../ro-crate-metadata.json | 156 ++++++++++++++++++ .../sort-and-change-case.ga | 118 +++++++++++++ .../ro-crate-metadata.json | 133 +++++++++++++++ .../sort-and-change-case-tests.yml | 8 + .../sort-and-change-case.ga | 118 +++++++++++++ .../test_valid_wtroc.py | 31 ++++ .../test_wtroc_root_data_entity.py | 37 +++++ .../test_wtroc_testsuite.py | 37 +++++ tests/ro_crates.py | 13 ++ 9 files changed, 651 insertions(+) create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_not_mentioned/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_not_mentioned/sort-and-change-case.ga create mode 100644 tests/data/crates/valid/workflow-testing-ro-crate/ro-crate-metadata.json create mode 100644 tests/data/crates/valid/workflow-testing-ro-crate/sort-and-change-case-tests.yml create mode 100644 tests/data/crates/valid/workflow-testing-ro-crate/sort-and-change-case.ga create mode 100644 tests/integration/profiles/workflow-testing-ro-crate/test_valid_wtroc.py create mode 100644 tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_root_data_entity.py create mode 100644 tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_not_mentioned/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_not_mentioned/ro-crate-metadata.json new file mode 100644 index 00000000..8428b451 --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_not_mentioned/ro-crate-metadata.json @@ -0,0 +1,156 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "instance": [ + { + "@id": "#test1_1" + } + ], + "definition": { + "@id": "sort-and-change-case-tests.yml" + } + }, + { + "@id": "#test2", + "name": "test2", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "instance": [ + { + "@id": "#test2_1" + } + ] + }, + { + "@id": "#test1_1", + "name": "test1_1", + "@type": "TestInstance", + "runsOn": { + "@id": "https://w3id.org/ro/terms/test#JenkinsService" + }, + "url": "http://example.org/jenkins", + "resource": "job/tests/" + }, + { + "@id": "#test2_1", + "name": "test2_1", + "@type": "TestInstance", + "runsOn": { + "@id": "https://w3id.org/ro/terms/test#JenkinsService" + }, + "url": "http://example.org/jenkins", + "resource": "job/moretests/" + }, + { + "@id": "https://w3id.org/ro/terms/test#JenkinsService", + "@type": "TestService", + "name": "Jenkins", + "url": { + "@id": "https://www.jenkins.io" + } + }, + { + "@id": "sort-and-change-case-tests.yml", + "@type": [ + "File", + "TestDefinition" + ], + "conformsTo": { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" + }, + "engineVersion": ">=0.70" + }, + { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "@type": "SoftwareApplication", + "name": "Planemo", + "url": { + "@id": "https://github.com/galaxyproject/planemo" + } + } + ] +} diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_not_mentioned/sort-and-change-case.ga b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_not_mentioned/sort-and-change-case.ga new file mode 100644 index 00000000..5a199969 --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_not_mentioned/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" +} diff --git a/tests/data/crates/valid/workflow-testing-ro-crate/ro-crate-metadata.json b/tests/data/crates/valid/workflow-testing-ro-crate/ro-crate-metadata.json new file mode 100644 index 00000000..465d12d6 --- /dev/null +++ b/tests/data/crates/valid/workflow-testing-ro-crate/ro-crate-metadata.json @@ -0,0 +1,133 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "instance": [ + { + "@id": "#test1_1" + } + ], + "definition": { + "@id": "sort-and-change-case-tests.yml" + } + }, + { + "@id": "#test1_1", + "name": "test1_1", + "@type": "TestInstance", + "runsOn": { + "@id": "https://w3id.org/ro/terms/test#JenkinsService" + }, + "url": "http://example.org/jenkins", + "resource": "job/tests/" + }, + { + "@id": "https://w3id.org/ro/terms/test#JenkinsService", + "@type": "TestService", + "name": "Jenkins", + "url": { + "@id": "https://www.jenkins.io" + } + }, + { + "@id": "sort-and-change-case-tests.yml", + "@type": [ + "File", + "TestDefinition" + ], + "conformsTo": { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" + }, + "engineVersion": ">=0.70" + }, + { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "@type": "SoftwareApplication", + "name": "Planemo", + "url": { + "@id": "https://github.com/galaxyproject/planemo" + } + } + ] +} diff --git a/tests/data/crates/valid/workflow-testing-ro-crate/sort-and-change-case-tests.yml b/tests/data/crates/valid/workflow-testing-ro-crate/sort-and-change-case-tests.yml new file mode 100644 index 00000000..9f24ba69 --- /dev/null +++ b/tests/data/crates/valid/workflow-testing-ro-crate/sort-and-change-case-tests.yml @@ -0,0 +1,8 @@ +- doc: test with a small input + job: + bed_input: + class: File + path: http://example.com/data/input.bed + outputs: + uppercase_bed: + path: http://example.com/data/output_exp.bed diff --git a/tests/data/crates/valid/workflow-testing-ro-crate/sort-and-change-case.ga b/tests/data/crates/valid/workflow-testing-ro-crate/sort-and-change-case.ga new file mode 100644 index 00000000..5a199969 --- /dev/null +++ b/tests/data/crates/valid/workflow-testing-ro-crate/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" +} diff --git a/tests/integration/profiles/workflow-testing-ro-crate/test_valid_wtroc.py b/tests/integration/profiles/workflow-testing-ro-crate/test_valid_wtroc.py new file mode 100644 index 00000000..b61cbf0c --- /dev/null +++ b/tests/integration/profiles/workflow-testing-ro-crate/test_valid_wtroc.py @@ -0,0 +1,31 @@ +# 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.models import Severity +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 Testing RO-Crate.""" + do_entity_test( + ValidROC().workflow_testing_ro_crate, + Severity.REQUIRED, + True, + profile_identifier="workflow-testing-ro-crate" + ) diff --git a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_root_data_entity.py b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_root_data_entity.py new file mode 100644 index 00000000..517efbf7 --- /dev/null +++ b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_root_data_entity.py @@ -0,0 +1,37 @@ +# 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.models import Severity +from tests.ro_crates import ValidROC +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_wtroc_no_suites(): + """\ + Test a Workflow Testing RO-Crate where the root data entity does not refer to + any TestSuite via mentions. + """ + do_entity_test( + ValidROC().workflow_roc, # a plain workflow ro-crate, no test suites + Severity.REQUIRED, + False, + ["Root Data Entity Metadata"], + ["The Root Data Entity MUST refer to one or more test suites via mentions"], + profile_identifier="workflow-testing-ro-crate" + ) diff --git a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py new file mode 100644 index 00000000..1f7522cb --- /dev/null +++ b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py @@ -0,0 +1,37 @@ +# 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.models import Severity +from tests.ro_crates import InvalidWTROC +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_wtroc_testsuite_not_mentioned(): + """\ + Test a Workflow Testing RO-Crate where a TestSuite is not listed in the + Root Data Entity's mentions. + """ + do_entity_test( + InvalidWTROC().testsuite_not_mentioned, + Severity.REQUIRED, + False, + ["Workflow Testing RO-Crate Action MUST"], + ["The TestSuite MUST be referenced from the Root Data Entity via mentions"], + profile_identifier="workflow-testing-ro-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index d5885de8..d6df6b59 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -66,6 +66,10 @@ def process_run_crate_collections(self) -> Path: def process_run_crate_containerimage(self) -> Path: return VALID_CRATES_DATA_PATH / "process-run-crate-containerimage" + @property + def workflow_testing_ro_crate(self) -> Path: + return VALID_CRATES_DATA_PATH / "workflow-testing-ro-crate" + class InvalidFileDescriptor: @@ -463,3 +467,12 @@ def softwareapplication_no_softwarerequirements(self) -> Path: @property def softwareapplication_bad_softwarerequirements(self) -> Path: return self.base_path / "softwareapplication_bad_softwarerequirements" + + +class InvalidWTROC: + + base_path = INVALID_CRATES_DATA_PATH / "5_workflow_testing_ro_crate/" + + @property + def testsuite_not_mentioned(self) -> Path: + return self.base_path / "testsuite_not_mentioned" From be41b3be6125e11590241d93205b7574ae49006f Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 17 Sep 2024 14:55:30 +0200 Subject: [PATCH 830/902] wtroc: fix shape name --- .../profiles/workflow-testing-ro-crate/must/1_test_suite.ttl | 2 +- .../profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/profiles/workflow-testing-ro-crate/must/1_test_suite.ttl b/rocrate_validator/profiles/workflow-testing-ro-crate/must/1_test_suite.ttl index aa3049c8..291b8bd8 100644 --- a/rocrate_validator/profiles/workflow-testing-ro-crate/must/1_test_suite.ttl +++ b/rocrate_validator/profiles/workflow-testing-ro-crate/must/1_test_suite.ttl @@ -21,7 +21,7 @@ @prefix wftest: . workflow-testing-ro-crate:WTROCTestSuiteRequired a sh:NodeShape ; - sh:name "Workflow Testing RO-Crate Action MUST" ; + sh:name "Workflow Testing RO-Crate TestSuite MUST" ; sh:description "Required properties of the Workflow Testing RO-Crate TestSuite" ; sh:targetClass wftest:TestSuite ; sh:property [ diff --git a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py index 1f7522cb..7b5e4414 100644 --- a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py +++ b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py @@ -31,7 +31,7 @@ def test_wtroc_testsuite_not_mentioned(): InvalidWTROC().testsuite_not_mentioned, Severity.REQUIRED, False, - ["Workflow Testing RO-Crate Action MUST"], + ["Workflow Testing RO-Crate TestSuite MUST"], ["The TestSuite MUST be referenced from the Root Data Entity via mentions"], profile_identifier="workflow-testing-ro-crate" ) From de309f6a490edc37f88b42d4c5ef0172cbbe0de2 Mon Sep 17 00:00:00 2001 From: simleo Date: Tue, 17 Sep 2024 15:39:08 +0200 Subject: [PATCH 831/902] wtroc: add shape to state that suites must have an instance or definition --- .../must/1_test_suite.ttl | 21 +++++ .../ro-crate-metadata.json | 88 +++++++++++++++++++ .../test_wtroc_testsuite.py | 15 ++++ tests/ro_crates.py | 4 + 4 files changed, 128 insertions(+) create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_no_instance_no_def/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/workflow-testing-ro-crate/must/1_test_suite.ttl b/rocrate_validator/profiles/workflow-testing-ro-crate/must/1_test_suite.ttl index 291b8bd8..d431ebec 100644 --- a/rocrate_validator/profiles/workflow-testing-ro-crate/must/1_test_suite.ttl +++ b/rocrate_validator/profiles/workflow-testing-ro-crate/must/1_test_suite.ttl @@ -33,3 +33,24 @@ workflow-testing-ro-crate:WTROCTestSuiteRequired a sh:NodeShape ; sh:minCount 1 ; sh:message "The TestSuite MUST be referenced from the Root Data Entity via mentions" ; ] . + + +workflow-testing-ro-crate:WTROCTestSuiteInstanceOrDefinition a sh:NodeShape ; + sh:name "TestSuite instance or definition" ; + sh:description "The TestSuite MUST refer to a TestInstance or TestDefinition" ; + sh:message "The TestSuite MUST refer to a TestInstance or TestDefinition" ; + sh:targetClass wftest:TestSuite ; + sh:or ( + [ sh:property [ + a sh:PropertyShape ; + sh:path wftest:instance ; + sh:class wftest:TestInstance ; + sh:minCount 1 ; + ]] + [ sh:property [ + a sh:PropertyShape ; + sh:path wftest:definition ; + sh:class wftest:TestDefinition ; + sh:minCount 1 ; + ]] + ) . diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_no_instance_no_def/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_no_instance_no_def/ro-crate-metadata.json new file mode 100644 index 00000000..3ecf4dd3 --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_no_instance_no_def/ro-crate-metadata.json @@ -0,0 +1,88 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + } + ] +} diff --git a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py index 7b5e4414..7e693e8a 100644 --- a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py +++ b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py @@ -35,3 +35,18 @@ def test_wtroc_testsuite_not_mentioned(): ["The TestSuite MUST be referenced from the Root Data Entity via mentions"], profile_identifier="workflow-testing-ro-crate" ) + + +def test_wtroc_testsuite_no_instance_no_def(): + """\ + Test a Workflow Testing RO-Crate where a TestSuite does not refer to either a + TestSuite or a TestDefinition. + """ + do_entity_test( + InvalidWTROC().testsuite_no_instance_no_def, + Severity.REQUIRED, + False, + ["TestSuite instance or definition"], + ["The TestSuite MUST refer to a TestInstance or TestDefinition"], + profile_identifier="workflow-testing-ro-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index d6df6b59..1bd9eae9 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -476,3 +476,7 @@ class InvalidWTROC: @property def testsuite_not_mentioned(self) -> Path: return self.base_path / "testsuite_not_mentioned" + + @property + def testsuite_no_instance_no_def(self) -> Path: + return self.base_path / "testsuite_no_instance_no_def" From 5f27c2ddc0d586c59a3c618e009c783b5b2d88d6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 18 Sep 2024 10:56:02 +0200 Subject: [PATCH 832/902] fix(cli): :sparkles: suppress the `stdout` output when the `-o` option is specified --- rocrate_validator/cli/commands/validate.py | 31 ++++++++++++---------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 73df2171..500e9c46 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -255,7 +255,7 @@ def validate(ctx, } # Print the application header - if output_format == "text": + if output_format == "text" and output_file is None: console.print(get_app_header_rule()) # Get the available profiles @@ -331,12 +331,14 @@ def validate(ctx, # Validate RO-Crate against the profile and get the validation result result: ValidationResult = None if output_format == "text": + console.disabled = output_file is not None result: ValidationResult = report_layout.live( lambda: services.validate( validation_settings, subscribers=[report_layout.progress_monitor] ) ) + console.disabled = False else: result: ValidationResult = services.validate( validation_settings @@ -346,18 +348,19 @@ def validate(ctx, is_valid = is_valid and result.passed(LevelCollection.get(requirement_severity).severity) # Print the validation result - if not result.passed(): - verbose_choice = "n" - if interactive and not verbose and enable_pager and not output_format == "json": - verbose_choice = get_single_char(console, choices=['y', 'n'], - message=( - "[bold] > Do you want to see the validation details? " - "([magenta]y/n[/magenta]): [/bold]" - )) - if verbose_choice == "y" or verbose: - report_layout.show_validation_details(pager, enable_pager=enable_pager) - - if output_format == "json": + if output_format == "text" and not output_file: + if not result.passed(): + verbose_choice = "n" + if interactive and not verbose and enable_pager: + verbose_choice = get_single_char(console, choices=['y', 'n'], + message=( + "[bold] > Do you want to see the validation details? " + "([magenta]y/n[/magenta]): [/bold]" + )) + if verbose_choice == "y" or verbose: + report_layout.show_validation_details(pager, enable_pager=enable_pager) + + if output_format == "json" and not output_file: console.print(result.to_json()) if output_file: @@ -370,7 +373,7 @@ def validate(ctx, c = Console(file=f, color_system=None, width=output_line_width, height=31) c.print(report_layout.layout) report_layout.console = c - if not result.passed(): + if not result.passed() and verbose: report_layout.show_validation_details(None, enable_pager=False) # Interrupt the validation if the fail fast mode is enabled From bb9422677b053e15faaba05f41e9ff7f18aa3e4a Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 18 Sep 2024 15:55:37 +0200 Subject: [PATCH 833/902] wtroc: one more shape for test suite --- .../should/1_test_suite.ttl | 38 +++++ .../ro-crate-metadata.json | 130 ++++++++++++++++++ .../test_wtroc_testsuite.py | 19 ++- tests/ro_crates.py | 4 + 4 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 rocrate_validator/profiles/workflow-testing-ro-crate/should/1_test_suite.ttl create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_no_mainentity/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/workflow-testing-ro-crate/should/1_test_suite.ttl b/rocrate_validator/profiles/workflow-testing-ro-crate/should/1_test_suite.ttl new file mode 100644 index 00000000..f1af48c6 --- /dev/null +++ b/rocrate_validator/profiles/workflow-testing-ro-crate/should/1_test_suite.ttl @@ -0,0 +1,38 @@ +# 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. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-testing-ro-crate: . +@prefix schema: . +@prefix bioschemas: . +@prefix sh: . +@prefix wroc: . +@prefix wftest: . + +workflow-testing-ro-crate:WTROCTestSuiteRecommended a sh:NodeShape ; + sh:name "Workflow Testing RO-Crate TestSuite SHOULD" ; + sh:description "Recommended properties of the Workflow Testing RO-Crate TestSuite" ; + sh:targetClass wftest:TestSuite ; + sh:property [ + a sh:PropertyShape ; + sh:name "TestSuite mainEntity" ; + sh:description "The TestSuite SHOULD refer to the tested workflow via mainEntity" ; + sh:path schema:mainEntity ; + sh:class schema:MediaObject , + schema:SoftwareSourceCode , + bioschemas:ComputationalWorkflow ; + sh:minCount 1 ; + sh:message "The TestSuite SHOULD refer to the tested workflow via mainEntity" ; + ] . diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_no_mainentity/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_no_mainentity/ro-crate-metadata.json new file mode 100644 index 00000000..82a22990 --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_no_mainentity/ro-crate-metadata.json @@ -0,0 +1,130 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "instance": [ + { + "@id": "#test1_1" + } + ], + "definition": { + "@id": "sort-and-change-case-tests.yml" + } + }, + { + "@id": "#test1_1", + "name": "test1_1", + "@type": "TestInstance", + "runsOn": { + "@id": "https://w3id.org/ro/terms/test#JenkinsService" + }, + "url": "http://example.org/jenkins", + "resource": "job/tests/" + }, + { + "@id": "https://w3id.org/ro/terms/test#JenkinsService", + "@type": "TestService", + "name": "Jenkins", + "url": { + "@id": "https://www.jenkins.io" + } + }, + { + "@id": "sort-and-change-case-tests.yml", + "@type": [ + "File", + "TestDefinition" + ], + "conformsTo": { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" + }, + "engineVersion": ">=0.70" + }, + { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "@type": "SoftwareApplication", + "name": "Planemo", + "url": { + "@id": "https://github.com/galaxyproject/planemo" + } + } + ] +} diff --git a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py index 7e693e8a..ff673d9b 100644 --- a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py +++ b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py @@ -39,8 +39,8 @@ def test_wtroc_testsuite_not_mentioned(): def test_wtroc_testsuite_no_instance_no_def(): """\ - Test a Workflow Testing RO-Crate where a TestSuite does not refer to either a - TestSuite or a TestDefinition. + Test a Workflow Testing RO-Crate where a TestSuite does not refer to + either a TestSuite or a TestDefinition. """ do_entity_test( InvalidWTROC().testsuite_no_instance_no_def, @@ -50,3 +50,18 @@ def test_wtroc_testsuite_no_instance_no_def(): ["The TestSuite MUST refer to a TestInstance or TestDefinition"], profile_identifier="workflow-testing-ro-crate" ) + + +def test_wtroc_testsuite_no_mainentity(): + """\ + Test a Workflow Testing RO-Crate where a TestSuite does not refer to + the tested workflow via mainEntity. + """ + do_entity_test( + InvalidWTROC().testsuite_no_mainentity, + Severity.RECOMMENDED, + False, + ["Workflow Testing RO-Crate TestSuite SHOULD"], + ["The TestSuite SHOULD refer to the tested workflow via mainEntity"], + profile_identifier="workflow-testing-ro-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 1bd9eae9..a39477e4 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -480,3 +480,7 @@ def testsuite_not_mentioned(self) -> Path: @property def testsuite_no_instance_no_def(self) -> Path: return self.base_path / "testsuite_no_instance_no_def" + + @property + def testsuite_no_mainentity(self) -> Path: + return self.base_path / "testsuite_no_mainentity" From 7bf92f7d587a4d520cc73961bdc8e834b9943880 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 18 Sep 2024 16:32:58 +0200 Subject: [PATCH 834/902] feat(utils): :sparkles: enhance version detection: take Git repository state into account --- rocrate_validator/utils.py | 87 +++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/utils.py b/rocrate_validator/utils.py index a895d7db..a128722f 100644 --- a/rocrate_validator/utils.py +++ b/rocrate_validator/utils.py @@ -42,13 +42,98 @@ config = toml.load(Path(CURRENT_DIR).parent / "pyproject.toml") +def run_git_command(command: list[str]) -> Optional[str]: + """ + Run a git command and return the output + + :param command: The git command + :return: The output of the command + """ + import subprocess + + try: + output = subprocess.check_output(command).decode().strip() + return output + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.debug(e) + return None + + +def get_git_commit() -> str: + """ + Get the git commit hash + + :return: The git commit hash + """ + return run_git_command(['git', 'rev-parse', '--short', 'HEAD']) + + +def is_release_tag(git_sha: str) -> bool: + """ + Check whether a git sha corresponds to a release tag + + :param git_sha: The git sha + :return: True if the sha corresponds to a release tag, False otherwise + """ + tags = run_git_command(['git', 'tag', '--points-at', git_sha]) + return bool(tags) + + +def get_commit_distance(tag: Optional[str] = None) -> int: + """ + Get the distance in commits between the current commit and the last tag + + :return: The distance in commits + """ + if not tag: + tag = get_last_tag() + return int(run_git_command(['git', 'rev-list', '--count', 'HEAD' if not tag else f"{tag}..HEAD"])) + + +def get_last_tag() -> str: + """ + Get the last tag in the git repository + + :return: The last tag + """ + return run_git_command(['git', 'describe', '--tags', '--abbrev=0']) + +# write a function to checks whether the are any uncommitted changes in the repository + + +def has_uncommitted_changes() -> bool: + """ + Check whether there are any uncommitted changes in the repository + + :return: True if there are uncommitted changes, False otherwise + """ + return bool(run_git_command(['git', 'status', '--porcelain'])) + + def get_version() -> str: """ Get the version of the package :return: The version """ - return config["tool"]["poetry"]["version"] + version = None + declared_version = config["tool"]["poetry"]["version"] + commit_sha = get_git_commit() + is_release = is_release_tag(commit_sha) + latest_tag = get_last_tag() + 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) + if commit_sha: + version = f"{declared_version}_{commit_sha}+{commit_distance}" + else: + version = declared_version + dirty = has_uncommitted_changes() + return f"{version}-dirty" if dirty else version def get_config(property: Optional[str] = None) -> dict: From fb6220eb2dd4c40f5590e137572a620e1f5d4482 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 18 Sep 2024 16:41:07 +0200 Subject: [PATCH 835/902] ci(utils): :sparkles: check the declared package version --- .github/workflows/testing.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 44ea6858..4a537d01 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -46,7 +46,26 @@ jobs: run: pip install flake8 - name: Run checks run: flake8 -v rocrate_validator tests + + # Verifies the declared version of the package + version: + name: Check version + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Check version + run: | + if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref_type }}" == "tag" ]; then + TAG_NAME=$(rocrate-validator -v) + if [ "rocrate-validator ${{ github.ref }}" != "refs/tags/$TAG_NAME" ]; then + echo "Tag name does not match the version" + exit 1 + fi + fi + # Runs the tests test: name: "Tests" runs-on: ubuntu-latest From 10ddb0813d30aa02698183a5cac1c79dadc4cb26 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 18 Sep 2024 16:43:38 +0200 Subject: [PATCH 836/902] refactor(ci): :truck: rename jobs --- .github/workflows/testing.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 4a537d01..134d259d 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -49,7 +49,7 @@ jobs: # Verifies the declared version of the package version: - name: Check version + name: Check Python package version runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') steps: @@ -67,7 +67,7 @@ jobs: # Runs the tests test: - name: "Tests" + name: "Run tests" runs-on: ubuntu-latest needs: [flake8] steps: @@ -85,4 +85,3 @@ jobs: run: poetry install --no-interaction --no-ansi - name: Run tests run: poetry run pytest - From 3df048ff6fa71aee8f0611dfc28d5e599d7830db Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 18 Sep 2024 16:57:00 +0200 Subject: [PATCH 837/902] ci: :bug: add missing steps --- .github/workflows/testing.yaml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 134d259d..546d497e 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -46,8 +46,8 @@ jobs: run: pip install flake8 - name: Run checks run: flake8 -v rocrate_validator tests - - # Verifies the declared version of the package + + # Verifies the declared version of the package version: name: Check Python package version runs-on: ubuntu-latest @@ -55,6 +55,16 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Upgrade pip + run: pip install --upgrade pip + - name: Initialise a virtual env + run: python -m venv ${VENV_PATH} + - name: Enable virtual env + run: source ${VENV_PATH}/bin/activate + - name: Install Poetry + run: pip install poetry + - name: Install dependencies + run: poetry install --no-interaction --no-ansi - name: Check version run: | if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref_type }}" == "tag" ]; then From 96ba7052df86489a62366247961e4df75cdf6dd4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 18 Sep 2024 17:03:53 +0200 Subject: [PATCH 838/902] fix(ci): :bug: fix the command to check the tool version --- .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 546d497e..76d150de 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -68,7 +68,7 @@ jobs: - name: Check version run: | if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref_type }}" == "tag" ]; then - TAG_NAME=$(rocrate-validator -v) + TAG_NAME=$(poetry run rocrate-validator -v) if [ "rocrate-validator ${{ github.ref }}" != "refs/tags/$TAG_NAME" ]; then echo "Tag name does not match the version" exit 1 From 02f1fd13fd5a1c98facc184f81915419dc7ede61 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 18 Sep 2024 17:06:52 +0200 Subject: [PATCH 839/902] fix(ci): :recycle: update error message --- .github/workflows/testing.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 76d150de..edffce04 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -68,10 +68,13 @@ jobs: - name: Check version run: | if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref_type }}" == "tag" ]; then - TAG_NAME=$(poetry run rocrate-validator -v) - if [ "rocrate-validator ${{ github.ref }}" != "refs/tags/$TAG_NAME" ]; then - echo "Tag name does not match the version" + declared_version=$(poetry version -s) + echo "Checking tag '${{ github.ref }}' against package version $declared_version" + if [ "${{ github.ref }}" != "refs/tags/$declared_version" ]; then + echo "Tag '${{ github.ref }}' does not match the declared package version '$declared_version'" exit 1 + else + echo "Tag '${{ github.ref }}' matches the declared package version '$declared_version'" fi fi From ebf32bb2aabead6d75af45b40afe5bcd129c2b42 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 18 Sep 2024 17:40:40 +0200 Subject: [PATCH 840/902] build: :construction_worker: update package version number --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 09f2d5d8..a84360d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "rocrate-validator" -version = "0.1.1" +version = "0.1.2" description = "" authors = [ "Marco Enrico Piras ", From 0b2954b4ba30e15e288fb6c184c44108135ac6fd Mon Sep 17 00:00:00 2001 From: simleo Date: Wed, 18 Sep 2024 17:41:04 +0200 Subject: [PATCH 841/902] wtroc: add shapes for test instance --- .../must/2_test_instance.ttl | 59 +++++++++ .../ro-crate-metadata.json | 122 ++++++++++++++++++ .../test_wtroc_testinstance.py | 37 ++++++ tests/ro_crates.py | 4 + 4 files changed, 222 insertions(+) create mode 100644 rocrate_validator/profiles/workflow-testing-ro-crate/must/2_test_instance.ttl create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_no_service/ro-crate-metadata.json create mode 100644 tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testinstance.py diff --git a/rocrate_validator/profiles/workflow-testing-ro-crate/must/2_test_instance.ttl b/rocrate_validator/profiles/workflow-testing-ro-crate/must/2_test_instance.ttl new file mode 100644 index 00000000..54565c57 --- /dev/null +++ b/rocrate_validator/profiles/workflow-testing-ro-crate/must/2_test_instance.ttl @@ -0,0 +1,59 @@ +# 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. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix xsd: . +@prefix workflow-testing-ro-crate: . +@prefix schema: . +@prefix sh: . +@prefix wroc: . +@prefix wftest: . + +workflow-testing-ro-crate:WTROCTestInstanceRequired a sh:NodeShape ; + sh:name "Workflow Testing RO-Crate TestInstance MUST" ; + sh:description "Required properties of the Workflow Testing RO-Crate TestInstance" ; + sh:targetClass wftest:TestInstance ; + sh:property [ + a sh:PropertyShape ; + sh:name "TestInstance runsOn" ; + sh:description "The TestInstance MUST refer to a TestService via runsOn" ; + sh:path wftest:runsOn ; + sh:class wftest:TestService ; + sh:or ( + [ sh:hasValue wftest:GithubService ; ] + [ sh:hasValue wftest:TravisService ; ] + [ sh:hasValue wftest:JenkinsService ; ] + ) ; + sh:minCount 1 ; + sh:message "The TestInstance MUST refer to a TestService via runsOn" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "TestInstance url" ; + sh:description "The TestInstance MUST refer to the test service base URL via url" ; + sh:path schema:url ; + sh:datatype xsd:string ; + sh:pattern "^http.*" ; + sh:minCount 1 ; + sh:message "The TestInstance MUST refer to the test service base URL via url" ; + ] ; sh:property [ + a sh:PropertyShape ; + sh:name "TestInstance resource" ; + sh:description "The TestInstance MUST refer to the relative URL of the test project base URL via resource" ; + sh:path wftest:resource ; + sh:datatype xsd:string ; + sh:minCount 1 ; + sh:message "The TestInstance MUST refer to the relative URL of the test project base URL via resource" ; + ] . diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_no_service/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_no_service/ro-crate-metadata.json new file mode 100644 index 00000000..ad734119 --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_no_service/ro-crate-metadata.json @@ -0,0 +1,122 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "instance": [ + { + "@id": "#test1_1" + } + ], + "definition": { + "@id": "sort-and-change-case-tests.yml" + } + }, + { + "@id": "#test1_1", + "name": "test1_1", + "@type": "TestInstance", + "url": "http://example.org/jenkins", + "resource": "job/tests/" + }, + { + "@id": "sort-and-change-case-tests.yml", + "@type": [ + "File", + "TestDefinition" + ], + "conformsTo": { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" + }, + "engineVersion": ">=0.70" + }, + { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "@type": "SoftwareApplication", + "name": "Planemo", + "url": { + "@id": "https://github.com/galaxyproject/planemo" + } + } + ] +} diff --git a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testinstance.py b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testinstance.py new file mode 100644 index 00000000..f2cbf03d --- /dev/null +++ b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testinstance.py @@ -0,0 +1,37 @@ +# 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.models import Severity +from tests.ro_crates import InvalidWTROC +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_wtroc_testinstance_no_service(): + """\ + Test a Workflow Testing RO-Crate where a TestInstance does not refer to + a TestService. + """ + do_entity_test( + InvalidWTROC().testinstance_no_service, + Severity.REQUIRED, + False, + ["Workflow Testing RO-Crate TestInstance MUST"], + ["The TestInstance MUST refer to a TestService via runsOn"], + profile_identifier="workflow-testing-ro-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index a39477e4..29d51172 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -484,3 +484,7 @@ def testsuite_no_instance_no_def(self) -> Path: @property def testsuite_no_mainentity(self) -> Path: return self.base_path / "testsuite_no_mainentity" + + @property + def testinstance_no_service(self) -> Path: + return self.base_path / "testinstance_no_service" From 8cfb73e48d5444b77488e9c380fc8f2c262b67ad Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 18 Sep 2024 17:47:32 +0200 Subject: [PATCH 842/902] feat(core): :sparkles: declare package version --- rocrate_validator/__init__.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 rocrate_validator/__init__.py diff --git a/rocrate_validator/__init__.py b/rocrate_validator/__init__.py new file mode 100644 index 00000000..52268bac --- /dev/null +++ b/rocrate_validator/__init__.py @@ -0,0 +1,20 @@ +# 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. + +def get_version(): + from rocrate_validator.utils import get_version + return get_version() + + +__version__ = get_version() From fd27e8bedcc5c8c1684b5a4f6fc1712d7245e208 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 18 Sep 2024 18:02:51 +0200 Subject: [PATCH 843/902] ci: :construction_worker: refactor CI pipelines --- .github/workflows/release.yaml | 57 ++++++++++++++++++++++++++++++++++ .github/workflows/testing.yaml | 33 +------------------- 2 files changed, 58 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/release.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..1ddfc7c1 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,57 @@ +name: CI Release Pipeline + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + # workflow_run: + # workflows: ["CI Testing Pipeline"] + # types: + # - completed + push: + tags: + - "*.*.*" + paths: + - "**" + - "!docs/**" + - "!examples/**" + +env: + TERM: xterm + # enable Docker push only if the required secrets are defined + # ENABLE_DOCKER_PUSH: ${{ secrets.DOCKERHUB_USER != null && secrets.DOCKERHUB_TOKEN != null }} + # Base Image + # IMAGE: python:3.12-slim + # Define the virtual environment path + VENV_PATH: .venv + +jobs: + # Verifies the declared version of the package + version: + name: Check Python package version + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Upgrade pip + run: pip install --upgrade pip + - name: Initialise a virtual env + run: python -m venv ${VENV_PATH} + - name: Enable virtual env + run: source ${VENV_PATH}/bin/activate + - name: Install Poetry + run: pip install poetry + - name: Install dependencies + run: poetry install --no-interaction --no-ansi + - name: Check version + run: | + if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref_type }}" == "tag" ]; then + declared_version=$(poetry version -s) + echo "Checking tag '${{ github.ref }}' against package version $declared_version" + if [ "${{ github.ref }}" != "refs/tags/$declared_version" ]; then + echo "Tag '${{ github.ref }}' does not match the declared package version '$declared_version'" + exit 1 + else + echo "Tag '${{ github.ref }}' matches the declared package version '$declared_version'" + fi + fi diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index edffce04..1f32267d 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -1,4 +1,4 @@ -name: CI Pipeline +name: CI Testing Pipeline # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the master branch @@ -47,37 +47,6 @@ jobs: - name: Run checks run: flake8 -v rocrate_validator tests - # Verifies the declared version of the package - version: - name: Check Python package version - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Upgrade pip - run: pip install --upgrade pip - - name: Initialise a virtual env - run: python -m venv ${VENV_PATH} - - name: Enable virtual env - run: source ${VENV_PATH}/bin/activate - - name: Install Poetry - run: pip install poetry - - name: Install dependencies - run: poetry install --no-interaction --no-ansi - - name: Check version - run: | - if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref_type }}" == "tag" ]; then - declared_version=$(poetry version -s) - echo "Checking tag '${{ github.ref }}' against package version $declared_version" - if [ "${{ github.ref }}" != "refs/tags/$declared_version" ]; then - echo "Tag '${{ github.ref }}' does not match the declared package version '$declared_version'" - exit 1 - else - echo "Tag '${{ github.ref }}' matches the declared package version '$declared_version'" - fi - fi - # Runs the tests test: name: "Run tests" From d25b68d47b0754bea37b2ec195d63edb47067f83 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 17:33:25 +0200 Subject: [PATCH 844/902] refactor(core): :fire: remove `level` property from the `Requirement` model --- rocrate_validator/models.py | 9 ++++----- rocrate_validator/requirements/python/__init__.py | 4 +--- rocrate_validator/requirements/shacl/requirements.py | 10 +--------- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 734022ce..d3968df5 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -523,7 +523,6 @@ def __str__(self): class Requirement(ABC): def __init__(self, - level: RequirementLevel, profile: Profile, name: str = "", description: Optional[str] = None, @@ -534,6 +533,7 @@ def __init__(self, self._profile = profile self._description = description self._path = path # path of code implementing the requirement + self._level_from_path = None self._checks: list[RequirementCheck] = [] if not name and path: @@ -665,26 +665,25 @@ def __eq__(self, other: object) -> bool: if not isinstance(other, Requirement): raise TypeError(f"Cannot compare {type(self)} with {type(other)}") return self.name == other.name \ - and self.severity == other.severity and self.description == other.description \ + and self.description == other.description \ and self.path == other.path def __ne__(self, other: object) -> bool: return not self.__eq__(other) def __hash__(self): - return hash((self.name, self.severity, self.description, self.path)) + return hash((self.name, self.description, self.path)) def __lt__(self, other: object) -> bool: if not isinstance(other, Requirement): raise ValueError(f"Cannot compare Requirement with {type(other)}") - return (self.level, self._order_number, self.name) < (other.level, other._order_number, other.name) + return (self._order_number, self.name) < (other._order_number, other.name) def __repr__(self): return ( f'ProfileRequirement(' f'_order_number={self._order_number}, ' f'name={self.name}, ' - f'level={self.level}, ' f'description={self.description}' f', path={self.path}, ' if self.path else '' ')' diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index 0920819a..9fa8fd7f 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -60,14 +60,13 @@ def execute_check(self, context: ValidationContext) -> bool: class PyRequirement(Requirement): def __init__(self, - level: RequirementLevel, profile: Profile, requirement_check_class: Type[PyFunctionCheck], name: str = "", description: Optional[str] = None, path: Optional[Path] = None): self.requirement_check_class = requirement_check_class - super().__init__(level, profile, name, description, path, initialize_checks=True) + super().__init__(profile, name, description, path, initialize_checks=True) def __init_checks__(self): # initialize the list of checks @@ -160,7 +159,6 @@ def load(self, profile: Profile, pass logger.debug("Processing requirement: %r", requirement_name) r = PyRequirement( - requirement_level, profile, requirement_check_class=check_class, name=rq["name"], diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index de64b618..af5cb9b8 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -32,12 +32,11 @@ class SHACLRequirement(Requirement): def __init__(self, - level: RequirementLevel, shape: Shape, profile: Profile, path: Path): self._shape = shape - super().__init__(level, profile, + super().__init__(profile, shape.name if shape.name else "", shape.description if shape.description else "", path) @@ -67,13 +66,6 @@ def __init_checks__(self) -> list[RequirementCheck]: def shape(self) -> Shape: return self._shape - @property - def level(self) -> RequirementLevel: - level = super().level - if level is None: - return self.shape.level - return level - @property def hidden(self) -> bool: if self.shape.node is not None: From 72ecb6106dc100d58daf6afc9152df89a5f04f37 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 17:35:40 +0200 Subject: [PATCH 845/902] feat(core): :sparkles: add `{severity,requirement_level}_from_path` properties to the `Requirement` class --- rocrate_validator/models.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index d3968df5..d9d60e49 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -569,12 +569,17 @@ def name(self) -> str: return self._name @property - def level(self) -> RequirementLevel: - return self._level + def severity_from_path(self) -> Severity: + return self.requirement_level_from_path.severity if self.requirement_level_from_path else None @property - def severity(self) -> Severity: - return self.level.severity + def requirement_level_from_path(self) -> RequirementLevel: + if not self._level_from_path: + try: + self._level_from_path = LevelCollection.get(self._path.parent.name) + except ValueError: + logger.debug("The requirement level could not be determined from the path: %s", self._path) + return self._level_from_path @property def profile(self) -> Profile: From 2e050b61cff3f00ce785055e784a5daad7c3fea6 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 17:36:31 +0200 Subject: [PATCH 846/902] refactor(core): :recycle: update `Requirement` identitifer --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index d9d60e49..4f6a4321 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -562,7 +562,7 @@ def identifier(self) -> str: @property def relative_identifier(self) -> str: - return f"{self.level.name} {self.order_number}" + return f"{self.order_number}" @property def name(self) -> str: From d83bcfe11b678c4038020fe3884c8564bab8cf16 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 17:39:26 +0200 Subject: [PATCH 847/902] refactor(core): :sparkles: remove `RequirementCheck.{level,severity}` dependency from `Requirement` --- rocrate_validator/models.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 4f6a4321..96fea9ef 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -779,10 +779,12 @@ class RequirementCheck(ABC): def __init__(self, requirement: Requirement, name: str, + level: Optional[RequirementLevel] = LevelCollection.REQUIRED, description: Optional[str] = None): self._requirement: Requirement = requirement self._order_number = 0 self._name = name + self._level = level self._description = description self._overridden_by: RequirementCheck = None self._override: RequirementCheck = None @@ -803,7 +805,7 @@ def identifier(self) -> str: @property def relative_identifier(self) -> str: - return f"{self.requirement.relative_identifier}.{self.order_number}" + return f"{self.level.name} {self.requirement.relative_identifier}.{self.order_number}" @property def name(self) -> str: @@ -823,11 +825,11 @@ def requirement(self) -> Requirement: @property def level(self) -> RequirementLevel: - return self.requirement.level + return self._level @property def severity(self) -> Severity: - return self.requirement.level.severity + return self.level.severity @property def overridden_by(self) -> RequirementCheck: From abf957a99fd7ebfcfadfc5b282548775c2d46f6a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 17:42:10 +0200 Subject: [PATCH 848/902] fix(core): :bug: fix LevelCollection getter --- rocrate_validator/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 96fea9ef..514ae3c2 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -143,7 +143,10 @@ def all() -> list[RequirementLevel]: @staticmethod def get(name: str) -> RequirementLevel: - return getattr(LevelCollection, name.upper()) + try: + return getattr(LevelCollection, name.upper()) + except AttributeError: + raise ValueError(f"Invalid RequirementLevel: {name}") @total_ordering From 3a1cdae533afc22cb287ea45cebde71dc3a539e9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 17:46:53 +0200 Subject: [PATCH 849/902] refactor(core): :recycle: update `get_requirements` method --- rocrate_validator/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 514ae3c2..c21da8f2 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -299,8 +299,8 @@ def get_requirements( self, severity: Severity = Severity.REQUIRED, exact_match: bool = False) -> list[Requirement]: return [requirement for requirement in self.requirements - if (not exact_match and requirement.severity >= severity) or - (exact_match and requirement.severity == severity)] + if (not exact_match and (not requirement.severity_from_path or requirement.severity >= severity)) or + (exact_match and requirement.severity_from_path == severity)] def get_requirement(self, name: str) -> Optional[Requirement]: for requirement in self.requirements: From 794885beb5d46f80b3dd915d08bc43308a656e0c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 17:47:50 +0200 Subject: [PATCH 850/902] fix(logging): :loud_sound: update log message --- rocrate_validator/models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index c21da8f2..c6f845aa 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -629,8 +629,7 @@ def __do_validate__(self, context: ValidationContext) -> bool: Internal method to perform the validation Returns whether all checks in this requirement passed. """ - logger.debug("Validating Requirement %s (level=%s) with %s checks", - self.name, self.level, len(self._checks)) + logger.debug("Validating Requirement %s with %s checks", self.name, len(self._checks)) logger.debug("Running %s checks for Requirement '%s'", len(self._checks), self.name) all_passed = True From ab05ef3eb43cfef7de1626a59aaa184a32c7f6c3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 17:49:36 +0200 Subject: [PATCH 851/902] fix(core): :wastebasket: clean up --- rocrate_validator/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index c6f845aa..cda2c12e 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -532,7 +532,6 @@ def __init__(self, path: Optional[Path] = None, initialize_checks: bool = True): self._order_number: Optional[int] = None - self._level = level self._profile = profile self._description = description self._path = path # path of code implementing the requirement From 8da0a64331869b28b136e17ea179258d7468530c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 17:53:04 +0200 Subject: [PATCH 852/902] fix(core): :recycle: update default `severity` of `CheckIssue` instances --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index cda2c12e..1b9049ce 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1105,7 +1105,7 @@ def add_check_issue(self, resultPath: Optional[str] = None, focusNode: Optional[str] = None, value: Optional[str] = None) -> CheckIssue: - sev_value = severity if severity is not None else check.requirement.severity + sev_value = severity if severity is not None else check.severity c = CheckIssue(sev_value, check, message, resultPath=resultPath, focusNode=focusNode, value=value) # self._issues.append(c) bisect.insort(self._issues, c) From 81e25fc999c370a7fd4d9cc423e1a39603caffeb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 17:59:59 +0200 Subject: [PATCH 853/902] feat(shacl): :sparkles: allow to get declared `severity` of a Shape Node --- .../requirements/shacl/models.py | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 0e0c1d89..5cb89cbe 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -22,7 +22,7 @@ from rocrate_validator.constants import SHACL_NS import rocrate_validator.log as logging -from rocrate_validator.models import LevelCollection, RequirementLevel +from rocrate_validator.models import LevelCollection, RequirementLevel, Severity from rocrate_validator.requirements.shacl.utils import (ShapesList, compute_key, inject_attributes) @@ -101,15 +101,28 @@ def parent(self) -> Optional[SHACLNode]: @property def level(self) -> RequirementLevel: """Return the requirement level of the shape""" + return self.get_declared_level() or LevelCollection.REQUIRED + + def get_declared_level(self) -> Optional[RequirementLevel]: + """Return the declared level of the shape""" + severity = self.get_declared_severity() + if severity: + try: + return LevelCollection.get(severity.name) + except ValueError: + pass + return None + + def get_declared_severity(self) -> Optional[Severity]: + """Return the declared severity of the shape""" severity = getattr(self, "severity", None) - if not severity: - return LevelCollection.REQUIRED if severity == f"{SHACL_NS}Violation": - return LevelCollection.REQUIRED + return Severity.REQUIRED elif severity == f"{SHACL_NS}Warning": - return LevelCollection.RECOMMENDED + return Severity.RECOMMENDED elif severity == f"{SHACL_NS}Info": - return LevelCollection.OPTIONAL + return Severity.OPTIONAL + return None def __str__(self): class_name = self.__class__.__name__ From 06030ff0af66ff380e3f3df471380fa4918e57f3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 18:05:33 +0200 Subject: [PATCH 854/902] feat(shacl): :sparkles: set the check severity based on the declared value, or infer it from the path --- .../requirements/shacl/checks.py | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 5ed2ed54..f8e4a4b4 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -19,7 +19,8 @@ import rocrate_validator.log as logging from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.events import EventType -from rocrate_validator.models import (Requirement, RequirementCheck, +from rocrate_validator.models import (LevelCollection, Requirement, + RequirementCheck, RequirementCheckValidationEvent, SkipRequirementCheck, ValidationContext) from rocrate_validator.requirements.shacl.models import Shape @@ -42,7 +43,7 @@ class SHACLCheck(RequirementCheck): def __init__(self, requirement: Requirement, - shape: Optional[Shape]) -> None: + shape: Shape) -> None: self._shape = shape # init the check super().__init__(requirement, @@ -55,10 +56,34 @@ def __init__(self, # store the instance SHACLCheck.__add_instance__(shape, self) + # set the check level + requirement_level_from_path = self.requirement.requirement_level_from_path + if requirement_level_from_path: + declared_level = shape.get_declared_level() + if declared_level: + if shape.level != requirement_level_from_path: + logger.warning("Mismatch in requirement level for check \"%s\": " + "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 = declared_level + else: + self._level = requirement_level_from_path + else: + self._level = shape.level + @property def shape(self) -> Shape: return self._shape + @property + def level(self) -> str: + return self._shape.level if self._shape else LevelCollection.REQUIRED + + @property + def severity(self) -> str: + return self.level.severity + def execute_check(self, context: ValidationContext): logger.debug("Starting check %s", self) try: @@ -71,10 +96,8 @@ def execute_check(self, context: ValidationContext): result = self.__do_execute_check__(ctx) ctx.current_validation_result = self not in result return ctx.current_validation_result - except SHACLValidationAlreadyProcessed as e: + except SHACLValidationAlreadyProcessed: logger.debug("SHACL Validation of profile %s already processed", self.requirement.profile.identifier) - if logger.isEnabledFor(logging.DEBUG): - logger.exception(e) # 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 not in [i.check for i in context.result.get_issues()] From 9fb8fa267c3ff5161867eaca15766224fa8f38b9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 18:09:47 +0200 Subject: [PATCH 855/902] feat(shacl): :sparkles: filter shapes based on the requirement level --- rocrate_validator/requirements/shacl/requirements.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index af5cb9b8..28dec2a0 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -94,5 +94,6 @@ def load(self, profile: Profile, logger.debug("Loaded %s shapes: %s", len(shapes), shapes) requirements = [] for shape in shapes: - requirements.append(SHACLRequirement(requirement_level, shape, profile, file_path)) + if shape is not None and shape.level >= requirement_level: + requirements.append(SHACLRequirement(shape, profile, file_path)) return requirements From cf50ac0d1b84df4bbe555d134010f212a8ef3cae Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 18:12:41 +0200 Subject: [PATCH 856/902] refactor(cli): :recycle: fix output of `profiles describe` command --- rocrate_validator/cli/commands/profiles.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 4335f0cf..335133e4 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -210,14 +210,20 @@ def __compacted_describe_profile__(profile): requirements = [_ for _ in profile.requirements if not _.hidden] for requirement in requirements: # add the requirement to the list - color = get_severity_color(requirement.severity) - level_info = f"[{color}]{requirement.severity.name}[/{color}]" - levels_list.add(level_info) + levels = (LevelCollection.REQUIRED, LevelCollection.RECOMMENDED, LevelCollection.OPTIONAL) + levels_count = [] + for level in levels: + count = len(requirement.get_checks_by_level(level)) + levels_count.append(count) + if count > 0: + color = get_severity_color(level.severity) + level_info = f"[{color}]{level.severity.name}[/{color}]" + levels_list.add(level_info) table_rows.append((str(requirement.order_number), requirement.name, Markdown(requirement.description.strip()), - f"{len(requirement.get_checks_by_level(LevelCollection.REQUIRED))}", - f"{len(requirement.get_checks_by_level(LevelCollection.RECOMMENDED))}", - f"{len(requirement.get_checks_by_level(LevelCollection.OPTIONAL))}")) + f"{levels_count[0]}", + f"{levels_count[1]}", + f"{levels_count[2]}")) table = Table(show_header=True, # renderer=renderer, From e4e9dc2a017ef560355a5024287d1318dad0df27 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 18:26:44 +0200 Subject: [PATCH 857/902] fix(core): :bug: properly initialize `PyRequirement` instances --- rocrate_validator/requirements/python/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index 9fa8fd7f..db017ac6 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -41,7 +41,7 @@ def __init__(self, """ check_function: a function that accepts an instance of PyFunctionCheck and a ValidationContext. """ - super().__init__(requirement, name, description) + super().__init__(requirement, name, description=description) sig = inspect.signature(check_function) if len(sig.parameters) != 2: From c3dd7c8fe43f4706272b5ce9395a9f77c4d9c80b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 18:29:52 +0200 Subject: [PATCH 858/902] fix(core): :bug: wrong property name --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 1b9049ce..66e1892f 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -299,7 +299,7 @@ def get_requirements( self, severity: Severity = Severity.REQUIRED, exact_match: bool = False) -> list[Requirement]: return [requirement for requirement in self.requirements - if (not exact_match and (not requirement.severity_from_path or requirement.severity >= severity)) or + if (not exact_match and (not requirement.severity_from_path or requirement.severity_from_path >= severity)) or (exact_match and requirement.severity_from_path == severity)] def get_requirement(self, name: str) -> Optional[Requirement]: From 3c06d044f60afb9f422fcf1c64dd68a453bf4a50 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 18:32:06 +0200 Subject: [PATCH 859/902] fix(core): :rotating_light: fix flake8 warning --- rocrate_validator/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 66e1892f..b66f13d6 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -299,7 +299,8 @@ def get_requirements( self, severity: Severity = Severity.REQUIRED, exact_match: bool = False) -> list[Requirement]: return [requirement for requirement in self.requirements - if (not exact_match and (not requirement.severity_from_path or requirement.severity_from_path >= severity)) or + if (not exact_match and + (not requirement.severity_from_path or requirement.severity_from_path >= severity)) or (exact_match and requirement.severity_from_path == severity)] def get_requirement(self, name: str) -> Optional[Requirement]: From c8311bb132af69dfe330ebf20118c7b9c84496cd Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 18:45:25 +0200 Subject: [PATCH 860/902] refactor(shacl): :recycle: set the conforms property to be computed based on the presence of issues --- rocrate_validator/requirements/shacl/validator.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index 70213ba4..a4d5a65b 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -307,7 +307,7 @@ def sourceShape(self) -> Union[URIRef, BNode]: class SHACLValidationResult: def __init__(self, results_graph: Graph, - conforms: Optional[bool] = None, results_text: str = None) -> None: + results_text: str = None) -> None: # validate the results graph input assert results_graph is not None, "Invalid graph" assert isinstance(results_graph, Graph), "Invalid graph type" @@ -320,18 +320,11 @@ def __init__(self, results_graph: Graph, # parse the results graph self._violations = self._parse_results_graph(results_graph) # initialize the conforms property - if conforms is None: - self._conforms = len(self._violations) == 0 - else: - self._conforms = conforms + self._conforms = len(self._violations) == 0 logger.debug("Validation report. N. violations: %s, Conforms: %s; Text: %s", len(self._violations), self._conforms, self._text) - # TODO: why allow setting conforms through an argument if the value is to be - # computed based on the presence of Violations? - assert self._conforms == (len(self._violations) == 0), "Invalid validation result" - def _parse_results_graph(self, results_graph: Graph): # parse the violations from the results graph violations = [] @@ -486,7 +479,7 @@ def validate( serialization_output_path, format=serialization_output_format ) # return the validation result - return SHACLValidationResult(results_graph, conforms, results_text) + return SHACLValidationResult(results_graph, results_text) __all__ = ["SHACLValidator", "SHACLValidationResult", "SHACLViolation"] From 9cd93b03d2963804ef66932b5890b84a21dd3148 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 19:57:45 +0200 Subject: [PATCH 861/902] fix(shacl): :bug: fix condition to print the mismatch warning --- rocrate_validator/requirements/shacl/checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index f8e4a4b4..730be395 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -61,7 +61,7 @@ def __init__(self, if requirement_level_from_path: declared_level = shape.get_declared_level() if declared_level: - if shape.level != requirement_level_from_path: + if shape.level.severity != requirement_level_from_path.severity: logger.warning("Mismatch in requirement level for check \"%s\": " "shape level %s does not match the level from the containing folder %s. " "Consider moving the shape property or removing the severity property.", From d7e07aa5f14fe908d0ea7a922da7116a8978845a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 19:58:49 +0200 Subject: [PATCH 862/902] fix(shacl): :bug: use the shape description --- rocrate_validator/requirements/shacl/checks.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 730be395..bd603b00 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -76,6 +76,10 @@ def __init__(self, def shape(self) -> Shape: return self._shape + @property + def description(self) -> str: + return self._shape.description + @property def level(self) -> str: return self._shape.level if self._shape else LevelCollection.REQUIRED From d826fc7b3b8be9331cc69f95e22cc0a3644c83dd Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 20:03:16 +0200 Subject: [PATCH 863/902] refactor(core): :recycle: update the requirements loading process --- rocrate_validator/models.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index b66f13d6..11f0adf1 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -750,14 +750,16 @@ def ok_file(p: Path) -> bool: files = sorted((p for p in profile.path.rglob('*.*') if ok_file(p)), key=lambda x: (not x.suffix == '.py', x)) + # set the requirement level corresponding to the severity + requirement_level = LevelCollection.get(severity.name) + requirements = [] for requirement_path in files: - requirement_level = None try: - requirement_level = LevelCollection.get(requirement_path.parent.name) - if requirement_level.severity < severity: + requirement_level_from_path = LevelCollection.get(requirement_path.parent.name) + if requirement_level_from_path < requirement_level: continue - except AttributeError: + except ValueError: logger.debug("The requirement level could not be determined from the path: %s", requirement_path) requirement_loader = RequirementLoader.__get_requirement_loader__(profile, requirement_path) for requirement in requirement_loader.load( @@ -765,7 +767,9 @@ def ok_file(p: Path) -> bool: requirement_path, publicID=profile.publicID): requirements.append(requirement) # sort the requirements by severity - requirements = sorted(requirements, key=lambda x: x.level.severity, reverse=True) + requirements = sorted(requirements, + key=lambda x: (x.severity_from_path, x.name) + if x.severity_from_path is not None else x.name, reverse=True) # assign order numbers to requirements for i, requirement in enumerate(requirements): requirement._order_number = i + 1 From 505e8d0b441f385b925b66a4353543843ecde4b3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 20:04:09 +0200 Subject: [PATCH 864/902] fix(core): :recycle: update `Requirement.level` definition --- rocrate_validator/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 11f0adf1..3d88ffa5 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -831,7 +831,9 @@ def requirement(self) -> Requirement: @property def level(self) -> RequirementLevel: - return self._level + return self._level or \ + self.requirement.requirement_level_from_path or \ + LevelCollection.REQUIRED @property def severity(self) -> Severity: From 7338b5af7c4fc1d693715613cf4d8c418452153a Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 20:06:31 +0200 Subject: [PATCH 865/902] feat(shacl): :sparkles: enable info and warning severity levels in PySHACL --- rocrate_validator/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 3d88ffa5..ceb82935 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1216,8 +1216,8 @@ class ValidationSettings: # Requirement severity settings requirement_severity: Union[str, Severity] = Severity.REQUIRED requirement_severity_only: bool = False - allow_infos: Optional[bool] = False - allow_warnings: Optional[bool] = False + allow_infos: Optional[bool] = True + allow_warnings: Optional[bool] = True # Output serialization settings serialization_output_path: Optional[Path] = None serialization_output_format: RDF_SERIALIZATION_FORMATS_TYPES = "turtle" From 33b72e24d94a8dc2a102907c3ebbfd071bb7ee9e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 20:35:34 +0200 Subject: [PATCH 866/902] refactor(cli): :lipstick: update output of `validate` command --- rocrate_validator/cli/commands/validate.py | 64 +++++++++++++--------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 500e9c46..703a3ed0 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -697,20 +697,16 @@ def show_validation_details(self, pager: Pager, enable_pager: bool = True): with console.pager(pager=pager, styles=not console.no_color) if enable_pager else console: # Print the list of failed requirements console.print( - Padding("\n[bold]The following requirements have not meet: [/bold]", (0, 0)), style="white") - for requirement in sorted(result.failed_requirements, - key=lambda x: (-x.severity.value, x)): - issue_color = get_severity_color(requirement.severity) + Padding("\n[bold]The following requirements have not meet: [/bold]", (0, 2)), style="white") + for requirement in sorted(result.failed_requirements, key=lambda x: x.identifier): console.print( - Align(f"\n [severity: [{issue_color}]{requirement.severity.name}[/{issue_color}], " - f"profile: [magenta bold]{requirement.profile.name }[/magenta bold]]", align="right") + Align(f"\n[profile: [magenta bold]{requirement.profile.name }[/magenta bold]]", align="right") ) console.print( - f" [bold][cyan][{requirement.order_number}] " - "[u]{Markdown(requirement.name).markup}[/u][/cyan][/bold]", - style="white", - ) - console.print(Padding(Markdown(requirement.description), (1, 7))) + Padding( + f"[bold][cyan][u][ {requirement.identifier} ]: " + f"{Markdown(requirement.name).markup}[/u][/cyan][/bold]", (0, 5)), style="white") + console.print(Padding(Markdown(requirement.description), (1, 6))) console.print(Padding("[white bold u] Failed checks [/white bold u]\n", (0, 8)), style="white bold") @@ -756,38 +752,56 @@ def __compute_profile_stats__(validation_settings: dict): total_requirements = 0 total_checks = 0 - requirement_count_by_severity = {} + # requirement_count_by_severity = {} check_count_by_severity = {} + # Initialize the counters + for severity in (Severity.REQUIRED, Severity.RECOMMENDED, Severity.OPTIONAL): + # requirement_count_by_severity[severity] = 0 + check_count_by_severity[severity] = 0 + for profile in profiles: for requirement in profile.requirements: if requirement.hidden: continue - severity = requirement.severity + requirement_checks_count = 0 + for severity in (Severity.REQUIRED, Severity.RECOMMENDED, Severity.OPTIONAL): + if severity_validation > severity: + continue - # Count the number of requirements by severity - if severity not in requirement_count_by_severity: - requirement_count_by_severity[severity] = 0 + # severity = requirement.severity - if severity_validation <= severity: - requirement_count_by_severity[severity] += 1 - total_requirements += 1 + # Count the number of requirements by severity + # if severity not in requirement_count_by_severity: + # requirement_count_by_severity[severity] = 0 - # Count the number of checks by severity - if severity not in check_count_by_severity: - check_count_by_severity[severity] = 0 - if severity_validation <= severity: + # if severity_validation <= severity: + # # requirement_count_by_severity[severity] += 1 + # total_requirements += 1 + + # Count the number of checks by severity + # if severity not in check_count_by_severity: + # check_count_by_severity[severity] = 0 + # if severity_validation <= severity: num_checks = len( [_ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name)) if not _.overridden]) check_count_by_severity[severity] += num_checks - total_checks += num_checks + # total_checks += num_checks + requirement_checks_count += num_checks + + if requirement_checks_count == 0: + logger.warning(f"No checks for requirement: {requirement}") + else: + logger.debug(f"Requirement: {requirement} checks count: {requirement_checks_count}") + total_requirements += 1 + total_checks += requirement_checks_count return { "profile": profile, "profiles": profiles, - "requirement_count_by_severity": requirement_count_by_severity, + # "requirement_count_by_severity": requirement_count_by_severity, "check_count_by_severity": check_count_by_severity, "total_requirements": total_requirements, "total_checks": total_checks, From 500cc493b568be1617b5f2afd354a32025325a0c Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 20:36:50 +0200 Subject: [PATCH 867/902] fix(shacl): :sparkles: update SHACLRequirement description --- rocrate_validator/requirements/shacl/models.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 5cb89cbe..1eb7e117 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -69,7 +69,7 @@ def name(self, value: str): def description(self) -> str: """Return the description of the shape""" if not self._description: - self._description = f"Check properties of the \"**{self.name}**\" entity" + self._description = getattr(self, "description", None) return self._description @description.setter @@ -243,12 +243,13 @@ def description(self) -> str: """Return the description of the shape property""" if not self._description: # get the object of the predicate sh:description - property_name = self.name - if self._short_name: - property_name = self._short_name - self._description = f"Check the property \"**{property_name}**\"" - if self.parent and self.parent.name not in property_name: - self._description += f" of the entity \"**{self.parent.name}**\"" + if not self._description: + property_name = self.name + if self._short_name: + property_name = self._short_name + self._description = f"Check the property \"**{property_name}**\"" + if self.parent and self.parent.name not in property_name: + self._description += f" of the entity \"**{self.parent.name}**\"" return self._description @description.setter From 1fb4cea703535ffedbbfe8258bb2d8f26cfe8d39 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Fri, 13 Sep 2024 20:38:24 +0200 Subject: [PATCH 868/902] fix(profiles/ro-crate): :bug: fix inconsistent severity level --- .../profiles/ro-crate/should/6_contextual_entity_metadata.ttl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl index 57d29b4f..1f9d401b 100644 --- a/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/should/6_contextual_entity_metadata.ttl @@ -37,7 +37,7 @@ ro-crate:CreativeWorkAuthorMinimuRecommendedProperties a sh:NodeShape ; [ sh:dataType xsd:string ; ] [ sh:class schema:Organization ;] ) ; - sh:severity sh:Violation ; + 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." ; From fa99fbbd1ed9b7a131cf8d32b67da35a896d1515 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 16 Sep 2024 10:04:43 +0200 Subject: [PATCH 869/902] refactor(core): :recycle: safer way to add candidate profiles --- rocrate_validator/models.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index ceb82935..bcf1fcd0 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -1337,8 +1337,16 @@ def detect_rocrate_profiles(self) -> list[Profile]: try: # initialize the validation context context = ValidationContext(self, self.validation_settings.to_dict()) - candidate_profiles_uris = set(context.ro_crate.metadata.get_conforms_to( - ) + context.ro_crate.metadata.get_root_data_entity_conforms_to()) + candidate_profiles_uris = set() + try: + candidate_profiles_uris.add(context.ro_crate.metadata.get_conforms_to()) + except Exception as e: + logger.debug("Error while getting candidate profiles URIs: %s", e) + try: + candidate_profiles_uris.add(context.ro_crate.metadata.get_root_data_entity_conforms_to()) + 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") From 84dfb93fe4f17543ee716a3114a91d592565b989 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 16 Sep 2024 10:06:31 +0200 Subject: [PATCH 870/902] fix(core): :sparkles: fix the sorting criteria of the requirements --- rocrate_validator/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index bcf1fcd0..09391766 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -768,8 +768,9 @@ def ok_file(p: Path) -> bool: requirements.append(requirement) # sort the requirements by severity requirements = sorted(requirements, - key=lambda x: (x.severity_from_path, x.name) - if x.severity_from_path is not None else x.name, reverse=True) + key=lambda x: (-x.severity_from_path.value, x.path.name, x.name) + if x.severity_from_path is not None else (x.path.name, x.name), + reverse=False) # assign order numbers to requirements for i, requirement in enumerate(requirements): requirement._order_number = i + 1 From 3ab6c35dfd73ba25dd31ea645d04a7a071ee293e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 16 Sep 2024 10:11:57 +0200 Subject: [PATCH 871/902] fix(shacl): :ambulance: report a generic error when the metadata is invalid --- rocrate_validator/requirements/shacl/checks.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index bd603b00..4a2ea301 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -132,8 +132,11 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): end_time = timer() logger.debug(f"Execution time for getting data graph: {end_time - start_time} seconds") except json.decoder.JSONDecodeError as e: - logger.debug("Unable to perform metadata validation due to an error in the JSON-LD data file: %s", e) - return False + logger.debug("Unable to perform metadata validation due to one or more errors in the JSON-LD data file: %s", e) + shacl_context.result.add_error( + "Unable to perform metadata validation due to one or more errors in the JSON-LD data file", self) + raise ROCrateMetadataNotFoundError( + "Unable to perform metadata validation due to one or more errors in the JSON-LD data file") # Begin the timer start_time = timer() From 1dc97fb0b565a1b0f0dd40ce3be19d2b9b896993 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 16 Sep 2024 10:20:16 +0200 Subject: [PATCH 872/902] fix(core): :rotating_light: fix flake8 warning --- rocrate_validator/requirements/shacl/checks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 4a2ea301..f4ce709e 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -132,7 +132,8 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): end_time = timer() logger.debug(f"Execution time for getting data graph: {end_time - start_time} seconds") except json.decoder.JSONDecodeError as e: - logger.debug("Unable to perform metadata validation due to one or more errors in the JSON-LD data file: %s", e) + logger.debug("Unable to perform metadata validation " + "due to one or more errors in the JSON-LD data file: %s", e) shacl_context.result.add_error( "Unable to perform metadata validation due to one or more errors in the JSON-LD data file", self) raise ROCrateMetadataNotFoundError( From f86907d4912d9fac29d199c3e127171aab43bb5f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 16 Sep 2024 10:27:35 +0200 Subject: [PATCH 873/902] test(core): :white_check_mark: test the loading of the requirements --- tests/test_models.py | 2 +- .../requirements/test_load_requirements.py | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tests/unit/requirements/test_load_requirements.py diff --git a/tests/test_models.py b/tests/test_models.py index 2154955d..ed44b254 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -85,7 +85,7 @@ def test_sortability_requirements(validation_settings: ValidationSettings): failed_requirements = sorted(result.failed_requirements, reverse=True) assert len(failed_requirements) > 1 assert failed_requirements[0] >= failed_requirements[1] - assert failed_requirements[0].level >= failed_requirements[1].level + assert failed_requirements[0].order_number >= failed_requirements[1].order_number def test_sortability_checks(validation_settings: ValidationSettings): diff --git a/tests/unit/requirements/test_load_requirements.py b/tests/unit/requirements/test_load_requirements.py new file mode 100644 index 00000000..e3aa0ef0 --- /dev/null +++ b/tests/unit/requirements/test_load_requirements.py @@ -0,0 +1,56 @@ +# 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 +import os + +from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER +from rocrate_validator.models import Profile +from tests.ro_crates import InvalidFileDescriptorEntity + +# set up logging +logger = logging.getLogger(__name__) + +# ย Global set up the paths +paths = InvalidFileDescriptorEntity() + + +def test_order_of_loaded_profile_requirements(profiles_path: str): + """Test the order of the loaded profiles.""" + logger.debug("The profiles path: %r", profiles_path) + assert os.path.exists(profiles_path) + profiles = Profile.load_profiles(profiles_path=profiles_path) + # 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 + + # Get the first profile + profile = profiles[0] + + # The first profile should have the following requirements + requirements = profile.get_requirements() + assert len(requirements) > 0 + for requirement in requirements: + logger.error("%r The requirement: %r -> severity: %r (path: %s)", requirement.order_number, + requirement.name, requirement.severity_from_path, requirement.path) + + # Sort requirements by their order + requirements = sorted(requirements, key=lambda x: (-x.severity_from_path.value, x.path.name, x.name)) + + # Check the order of the requirements + for i, requirement in enumerate(requirements): + if i < len(requirements) - 1: + assert requirement < requirements[i + 1] From 1516166ba253497ad308bf422c0fb0d67e8e3f0f Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 16 Sep 2024 10:31:02 +0200 Subject: [PATCH 874/902] fix(profiles/ro-crate): :bug: fix mismatch in the requirement level --- .../profiles/ro-crate/must/5_web_data_entity_metadata.ttl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl index fda7dc0c..5662828e 100644 --- a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl @@ -62,7 +62,7 @@ ro-crate:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; 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:severity sh:Violation ; sh:message """Web-based Data Entities SHOULD have a `contentSize` property""" ; sh:sparql [ sh:message "If the value is a string it must be a string representing an integer." ; @@ -86,6 +86,6 @@ ro-crate:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; 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:severity sh:Violation ; sh:message """Web-based Data Entities SHOULD have a `sdDatePublished` property""" ; ] . From fe5d996ac073e5096ac5f6b8cc25f7fd1852ed4e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 16 Sep 2024 16:24:50 +0200 Subject: [PATCH 875/902] fix(shacl): :bug: always parse the result graph --- .../requirements/shacl/checks.py | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index f4ce709e..3060cdcf 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -167,24 +167,22 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): # store the validation result in the context start_time = timer() - result = shacl_result.conforms # if the validation fails, process the failed checks failed_requirements_checks = set() failed_requirements_checks_violations: dict[str, SHACLViolation] = {} failed_requirement_checks_notified = [] - if not shacl_result.conforms: - logger.debug("Parsing Validation with result: %s", result) - # process the failed checks to extract the requirement checks involved - for violation in shacl_result.violations: - shape = shapes_registry.get_shape(Shape.compute_key(shapes_graph, violation.sourceShape)) - assert shape is not None, "Unable to map the violation to a shape" - requirementCheck = SHACLCheck.get_instance(shape) - assert requirementCheck is not None, "The requirement check cannot be None" - failed_requirements_checks.add(requirementCheck) - violations = failed_requirements_checks_violations.get(requirementCheck.identifier, None) - if violations is None: - failed_requirements_checks_violations[requirementCheck.identifier] = violations = [] - violations.append(violation) + logger.debug("Parsing Validation with result: %s", shacl_result) + # process the failed checks to extract the requirement checks involved + for violation in shacl_result.violations: + shape = shapes_registry.get_shape(Shape.compute_key(shapes_graph, violation.sourceShape)) + assert shape is not None, "Unable to map the violation to a shape" + requirementCheck = SHACLCheck.get_instance(shape) + assert requirementCheck is not None, "The requirement check cannot be None" + failed_requirements_checks.add(requirementCheck) + violations = failed_requirements_checks_violations.get(requirementCheck.identifier, None) + if violations is None: + failed_requirements_checks_violations[requirementCheck.identifier] = violations = [] + violations.append(violation) # sort the failed checks by identifier and severity # to ensure a consistent order of the issues # and to make the fail fast mode deterministic From 390335bcfc199cf010de496b05db6585d8273ffe Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 16 Sep 2024 16:27:20 +0200 Subject: [PATCH 876/902] feat(services): :sparkles: expose the severity property in the `get_profile` service --- rocrate_validator/services.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 3b94a86e..346d8a62 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -176,12 +176,13 @@ def get_profiles(profiles_path: Path = DEFAULT_PROFILES_PATH, def get_profile(profiles_path: Path = DEFAULT_PROFILES_PATH, profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, publicID: str = None, + severity=Severity.OPTIONAL, allow_requirement_check_override: bool = ValidationSettings.allow_requirement_check_override) -> Profile: """ Load the profiles from the given path """ - profiles = get_profiles(profiles_path, publicID=publicID, + profiles = get_profiles(profiles_path, publicID=publicID, severity=severity, allow_requirement_check_override=allow_requirement_check_override) 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) From f8de6f88a6f70562c7e02e4a8106508c24cc80f0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 16 Sep 2024 17:30:01 +0200 Subject: [PATCH 877/902] fix(shacl): :ambulance: fix the override of the base method --- rocrate_validator/requirements/shacl/checks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index 3060cdcf..aa428d34 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -82,7 +82,8 @@ def description(self) -> str: @property def level(self) -> str: - return self._shape.level if self._shape else LevelCollection.REQUIRED + return self.requirement.requirement_level_from_path or \ + (self._shape.level if self._shape else LevelCollection.REQUIRED) @property def severity(self) -> str: From f01e11125302573ed18e87aba70bbd83da4e66a2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 16 Sep 2024 17:33:04 +0200 Subject: [PATCH 878/902] refactor(cli): :recycle: restructure fn to generate checks stats --- rocrate_validator/cli/commands/validate.py | 53 ++++++++++++---------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 703a3ed0..bade6af2 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -741,67 +741,68 @@ def __compute_profile_stats__(validation_settings: dict): """ Compute the statistics of the profile """ - profiles = services.get_profiles(validation_settings.get("profiles_path")) - profile = services.get_profile(validation_settings.get("profiles_path"), - validation_settings.get("profile_identifier")) + # extract the validation settings severity_validation = Severity.get(validation_settings.get("requirement_severity")) + profiles = services.get_profiles(validation_settings.get("profiles_path"), severity=severity_validation) + profile = services.get_profile(validation_settings.get("profiles_path"), + validation_settings.get("profile_identifier"), + severity=severity_validation) + # initialize the profiles list profiles = [profile] + # add inherited profiles if enabled if validation_settings.get("inherit_profiles"): profiles.extend(profile.inherited_profiles) + logger.debug("Inherited profiles: %r", profile.inherited_profiles) + # Initialize the counters total_requirements = 0 total_checks = 0 - # requirement_count_by_severity = {} check_count_by_severity = {} # Initialize the counters for severity in (Severity.REQUIRED, Severity.RECOMMENDED, Severity.OPTIONAL): - # requirement_count_by_severity[severity] = 0 check_count_by_severity[severity] = 0 + # Process the requirements and checks + processed_requirements = [] for profile in profiles: for requirement in profile.requirements: + processed_requirements.append(requirement) if requirement.hidden: continue requirement_checks_count = 0 for severity in (Severity.REQUIRED, Severity.RECOMMENDED, Severity.OPTIONAL): - if severity_validation > severity: + logger.debug( + f"Checking requirement: {requirement} severity: {severity} {severity < severity_validation}") + # skip requirements with lower severity + if severity < severity_validation: continue - - # severity = requirement.severity - - # Count the number of requirements by severity - # if severity not in requirement_count_by_severity: - # requirement_count_by_severity[severity] = 0 - - # if severity_validation <= severity: - # # requirement_count_by_severity[severity] += 1 - # total_requirements += 1 - - # Count the number of checks by severity - # if severity not in check_count_by_severity: - # check_count_by_severity[severity] = 0 - # if severity_validation <= severity: + # count the checks num_checks = len( [_ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name)) if not _.overridden]) check_count_by_severity[severity] += num_checks - # total_checks += num_checks requirement_checks_count += num_checks + # count the requirements and checks if requirement_checks_count == 0: - logger.warning(f"No checks for requirement: {requirement}") + logger.debug(f"No checks for requirement: {requirement}") else: logger.debug(f"Requirement: {requirement} checks count: {requirement_checks_count}") + assert not requirement.hidden, "Hidden requirements should not be counted" total_requirements += 1 total_checks += requirement_checks_count - return { + # log processed requirements + logger.debug("Processed requirements %r: %r", len(processed_requirements), processed_requirements) + + # Prepare the result + result = { "profile": profile, "profiles": profiles, - # "requirement_count_by_severity": requirement_count_by_severity, + "severity": severity_validation, "check_count_by_severity": check_count_by_severity, "total_requirements": total_requirements, "total_checks": total_checks, @@ -810,3 +811,5 @@ def __compute_profile_stats__(validation_settings: dict): "passed_requirements": [], "passed_checks": [] } + logger.debug(result) + return result From 8703ffd05623bac02d5fa3c5151c376b18ba716b Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 16 Sep 2024 17:34:14 +0200 Subject: [PATCH 879/902] refactor(cli): :lipstick: update `profiles list` to show the number of checks by severity --- rocrate_validator/cli/commands/profiles.py | 36 +++++++++++++--------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index 335133e4..a465576f 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -26,7 +26,7 @@ from rocrate_validator.cli.utils import get_app_header_rule from rocrate_validator.colors import get_severity_color from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER -from rocrate_validator.models import LevelCollection, RequirementLevel +from rocrate_validator.models import LevelCollection, RequirementLevel, Severity from rocrate_validator.utils import get_profiles_path # set the default profiles path @@ -89,7 +89,7 @@ def list_profiles(ctx, no_paging: bool = False): # , profiles_path: Path = DEFA border_style="bright_black", show_footer=False, caption_style="italic bold", - caption="[cyan](*)[/cyan] Number of requirements by severity") + caption="[cyan](*)[/cyan] Number of requirements checks by severity") # Define columns table.add_column("Identifier", style="magenta bold", justify="center") @@ -98,27 +98,35 @@ def list_profiles(ctx, no_paging: bool = False): # , profiles_path: Path = DEFA table.add_column("Name", style="white bold", justify="center") table.add_column("Description", style="white italic") table.add_column("Based on", style="white", justify="center") - table.add_column("Requirements (*)", style="white", justify="center") + table.add_column("Requirements Checks (*)", style="white", justify="center") + + # Define levels + levels = (LevelCollection.REQUIRED, LevelCollection.RECOMMENDED, LevelCollection.OPTIONAL) # Add data to the table for profile in profiles: # Count requirements by severity - requirements = {} - logger.debug("Requirements: %s", requirements) - for req in profile.requirements: - if not requirements.get(req.severity.name, None): - requirements[req.severity.name] = 0 - requirements[req.severity.name] += 1 - requirements = "\n".join( - [f"[bold][{get_severity_color(severity)}]{severity}: " - f"{count}[/{get_severity_color(severity)}][/bold]" - for severity, count in requirements.items() if count > 0]) + checks_info = {} + for level in levels: + checks_info[level.severity.name] = { + "count": 0, + "color": get_severity_color(level.severity) + } + + requirements = [_ for _ in profile.get_requirements(severity=Severity.OPTIONAL) if not _.hidden] + for requirement in requirements: + for level in levels: + count = len(requirement.get_checks_by_level(level)) + checks_info[level.severity.name]["count"] += count + + checks_summary = "\n".join( + [f"[{v['color']}]{k}[/{v['color']}]: {v['count']}" for k, v in checks_info.items()]) # Add the row to the table table.add_row(profile.identifier, profile.uri, profile.version, profile.name, Markdown(profile.description.strip()), "\n".join([p.identifier for p in profile.inherited_profiles]), - requirements) + checks_summary) table.add_row() # Print the table From a649bbfaf1978d0d58c63d9ded5793e7b8f40c3e Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Mon, 16 Sep 2024 17:39:58 +0200 Subject: [PATCH 880/902] test(cli): :white_check_mark: add more unit tests for the cli internals --- tests/unit/test_cli_internals.py | 67 ++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/unit/test_cli_internals.py diff --git a/tests/unit/test_cli_internals.py b/tests/unit/test_cli_internals.py new file mode 100644 index 00000000..7685ea33 --- /dev/null +++ b/tests/unit/test_cli_internals.py @@ -0,0 +1,67 @@ +# 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 os + + +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 + +# set up logging +logger = logging.getLogger(__name__) + + +def test_compute_stats(): + + settings = { + "fail_fast": False, + "profiles_path": DEFAULT_PROFILES_PATH, + "profile_identifier": "ro-crate", + "inherit_profiles": True, + "allow_requirement_check_override": True, + "requirement_severity": "REQUIRED", + } + + profiles_path = DEFAULT_PROFILES_PATH + logger.debug("The profiles path: %r", DEFAULT_PROFILES_PATH) + assert os.path.exists(profiles_path) + profiles = Profile.load_profiles(profiles_path) + # The number of profiles should be greater than 0 + assert len(profiles) > 0 + + # Get the profile ro-crate + profile = profiles[0] + logger.debug("The profile: %r", profile) + assert profile is not None + assert profile.identifier == "ro-crate-1.1" + + # extract the list of not hidden requirements + logger.error("The number of requirements: %r", len(profile.get_requirements())) + requirements = [r for r in profile.get_requirements() if not r.hidden] + logger.debug("The requirements: %r", requirements) + assert len(requirements) > 0 + + stats = __compute_profile_stats__(settings) + + # Check severity + assert stats["severity"].name == "REQUIRED" + + # Check the number of profiles + assert len(stats["profiles"]) == 1 + + # check the number of requirements in stats and the number of requirements in the profile + assert stats["total_requirements"] == len(requirements) + + logger.error(stats) From d46ca385ed1305c6f166f883616b130308cdb1a5 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 17 Sep 2024 13:28:32 +0200 Subject: [PATCH 881/902] refactor(shacl): :recycle: restructure the logic to set and retrieve the requirement level in SHACL checks logic to set/get requirement level on SHACL checks --- rocrate_validator/requirements/shacl/checks.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index aa428d34..409946ce 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -66,11 +66,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 = declared_level - else: - self._level = requirement_level_from_path - else: - self._level = shape.level + self._level = None @property def shape(self) -> Shape: @@ -80,10 +76,18 @@ def shape(self) -> Shape: def description(self) -> str: return self._shape.description + def __compute_requirement_level__(self) -> LevelCollection: + if self._shape and self._shape.get_declared_level(): + return self._shape.get_declared_level() + if self.requirement and self.requirement.requirement_level_from_path: + return self.requirement.requirement_level_from_path + return LevelCollection.REQUIRED + @property def level(self) -> str: - return self.requirement.requirement_level_from_path or \ - (self._shape.level if self._shape else LevelCollection.REQUIRED) + if not self._level: + self._level = self.__compute_requirement_level__() + return self._level @property def severity(self) -> str: From 2cbc3973be9a443fbbdadac252879b61c13caadc Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 17 Sep 2024 13:30:00 +0200 Subject: [PATCH 882/902] fix(core): :bug: fix missing param to sort requirements --- rocrate_validator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocrate_validator/models.py b/rocrate_validator/models.py index 09391766..b9bf500b 100644 --- a/rocrate_validator/models.py +++ b/rocrate_validator/models.py @@ -769,7 +769,7 @@ def ok_file(p: Path) -> bool: # sort the requirements by severity requirements = sorted(requirements, key=lambda x: (-x.severity_from_path.value, x.path.name, x.name) - if x.severity_from_path is not None else (x.path.name, x.name), + if x.severity_from_path is not None else (0, x.path.name, x.name), reverse=False) # assign order numbers to requirements for i, requirement in enumerate(requirements): From fa9e7ff3924e7d529e7412751f1211dd241cbb5d Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 17 Sep 2024 13:31:10 +0200 Subject: [PATCH 883/902] feat(core): :sparkles: allow to specify the `level` of a Python requirement --- .../requirements/python/__init__.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index db017ac6..fc38f01c 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -13,14 +13,13 @@ # limitations under the License. import inspect - import re from pathlib import Path from typing import Callable, Optional, Type import rocrate_validator.log as logging -from ...models import (Profile, Requirement, RequirementCheck, +from ...models import (LevelCollection, Profile, Requirement, RequirementCheck, RequirementLevel, RequirementLoader, ValidationContext) from ...utils import get_classes_from_file @@ -37,11 +36,12 @@ def __init__(self, requirement: Requirement, name: str, check_function: Callable[[RequirementCheck, ValidationContext], bool], - description: Optional[str] = None): + description: Optional[str] = None, + level: Optional[LevelCollection] = LevelCollection.REQUIRED): """ check_function: a function that accepts an instance of PyFunctionCheck and a ValidationContext. """ - super().__init__(requirement, name, description=description) + super().__init__(requirement, name, description=description, level=level) sig = inspect.signature(check_function) if len(sig.parameters) != 2: @@ -80,10 +80,17 @@ def __init_checks__(self): except Exception: check_name = name.strip() check_description = member.__doc__.strip() if member.__doc__ else "" - check = self.requirement_check_class(requirement=self, - name=check_name, - check_function=member, - description=check_description) + # init the check with the requirement level + check_level = None + try: + check_level = member.level + except Exception: + check_level = self.requirement_level_from_path or LevelCollection.REQUIRED + check = self.requirement_check_class(self, + check_name, + member, + description=check_description, + level=check_level) self._checks.append(check) logger.debug("Added check: %s %r", check_name, check) @@ -109,7 +116,7 @@ def decorator(cls): return decorator -def check(name: Optional[str] = None): +def check(name: Optional[str] = None, level: Optional[LevelCollection] = None): """ A decorator to mark functions as "checks" (by setting an attribute `check=True`) and optionally annotating them with a human-legible name. @@ -125,6 +132,7 @@ def decorator(func): f"return bool but this only returns {sig.return_annotation}") func.check = True func.name = check_name + func.level = level return func return decorator From b2005d11c08d93fdf3faf431448f015cdf8f92cf Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 17 Sep 2024 13:34:41 +0200 Subject: [PATCH 884/902] test(core): :white_check_mark: add unit tests to verify the loading of requirements --- tests/conftest.py | 5 ++ .../requirement_loading/x/must/a_must.ttl | 46 +++++++++++ .../requirement_loading/x/must/b_must.py | 48 +++++++++++ .../requirement_loading/x/profile.ttl | 28 +++++++ .../profiles/requirement_loading/x/req_b.py | 48 +++++++++++ .../requirement_loading/x/shape_a.ttl | 46 +++++++++++ .../requirements/test_load_requirements.py | 80 ++++++++++++++++++- 7 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 tests/data/profiles/requirement_loading/x/must/a_must.ttl create mode 100644 tests/data/profiles/requirement_loading/x/must/b_must.py create mode 100644 tests/data/profiles/requirement_loading/x/profile.ttl create mode 100644 tests/data/profiles/requirement_loading/x/req_b.py create mode 100644 tests/data/profiles/requirement_loading/x/shape_a.ttl diff --git a/tests/conftest.py b/tests/conftest.py index 162d343d..47a867cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,6 +52,11 @@ def fake_profiles_path(): return f"{TEST_DATA_PATH}/profiles/fake" +@fixture +def profiles_requirement_loading(): + return f"{TEST_DATA_PATH}/profiles/requirement_loading" + + @fixture def profiles_with_free_folder_structure_path(): return f"{TEST_DATA_PATH}/profiles/free_folder_structure" diff --git a/tests/data/profiles/requirement_loading/x/must/a_must.ttl b/tests/data/profiles/requirement_loading/x/must/a_must.ttl new file mode 100644 index 00000000..d6e4bd9d --- /dev/null +++ b/tests/data/profiles/requirement_loading/x/must/a_must.ttl @@ -0,0 +1,46 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +ro:A_MUST + a sh:NodeShape ; + sh:name "A_MUST" ; + sh:description "This is the requirement A_MUST" ; + sh:targetNode ro:ro-crate-metadata.json ; + sh:property [ + a sh:PropertyShape ; + sh:name "A_MUST_0" ; + sh:description "Check A_MUST_0: no sh:severity declared" ; + sh:path rdf:type ; + sh:minCount 1 ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "A_MUST_1" ; + sh:description "Check A_MUST_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_MUST_2" ; + sh:description "Check A_MUST_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_MUST_3" ; + sh:description "Check A_MUST_3: sh:severity set to sh:Info" ; + sh:path rdf:type ; + sh:minCount 1 ; + sh:severity sh:Info ; + ] . + diff --git a/tests/data/profiles/requirement_loading/x/must/b_must.py b/tests/data/profiles/requirement_loading/x/must/b_must.py new file mode 100644 index 00000000..87edca48 --- /dev/null +++ b/tests/data/profiles/requirement_loading/x/must/b_must.py @@ -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 LevelCollection, ValidationContext +from rocrate_validator.requirements.python import (PyFunctionCheck, check, + requirement) + +# set up logging +logger = logging.getLogger(__name__) + + +@requirement(name="B_MUST") +class B_MUST(PyFunctionCheck): + """ + Test requirement outside requirement level folder + """ + + @check(name="B_MUST_0") + def check_b0(self, context: ValidationContext) -> bool: + """Check B_MUST_0: no requirement level""" + return True + + @check(name="B_MUST_1", level=LevelCollection.REQUIRED) + def check_b1(self, context: ValidationContext) -> bool: + """Check B_MUST_1: REQUIRED requirement level""" + return True + + @check(name="B_MUST_2", level=LevelCollection.RECOMMENDED) + def check_b2(self, context: ValidationContext) -> bool: + """Check B_MUST_2: RECOMMENDED requirement level""" + return True + + @check(name="B_MUST_3", level=LevelCollection.OPTIONAL) + def check_b3(self, context: ValidationContext) -> bool: + """Check B_MUST_3: OPTIONAL requirement level""" + return True diff --git a/tests/data/profiles/requirement_loading/x/profile.ttl b/tests/data/profiles/requirement_loading/x/profile.ttl new file mode 100644 index 00000000..4a79c64e --- /dev/null +++ b/tests/data/profiles/requirement_loading/x/profile.ttl @@ -0,0 +1,28 @@ +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Profile X" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Comment for the Profile A."""@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.1 profile + # prof:isProfileOf ; + + # # Explicitly state that this profile is a transitive profile of the RO-Crate Metadata Specification 1.1 profile + # prof:isTransitiveProfileOf , ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "x" ; +. diff --git a/tests/data/profiles/requirement_loading/x/req_b.py b/tests/data/profiles/requirement_loading/x/req_b.py new file mode 100644 index 00000000..a1282e19 --- /dev/null +++ b/tests/data/profiles/requirement_loading/x/req_b.py @@ -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 LevelCollection, ValidationContext +from rocrate_validator.requirements.python import (PyFunctionCheck, check, + requirement) + +# set up logging +logger = logging.getLogger(__name__) + + +@requirement(name="B") +class B(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", level=LevelCollection.REQUIRED) + def check_b1(self, context: ValidationContext) -> bool: + """Check B_1: REQUIRED requirement level""" + return True + + @check(name="B_2", level=LevelCollection.RECOMMENDED) + def check_b2(self, context: ValidationContext) -> bool: + """Check B_2: RECOMMENDED requirement level""" + return True + + @check(name="B_3", level=LevelCollection.OPTIONAL) + def check_b3(self, context: ValidationContext) -> bool: + """Check B_3: OPTIONAL requirement level""" + return True diff --git a/tests/data/profiles/requirement_loading/x/shape_a.ttl b/tests/data/profiles/requirement_loading/x/shape_a.ttl new file mode 100644 index 00000000..d2fee2a1 --- /dev/null +++ b/tests/data/profiles/requirement_loading/x/shape_a.ttl @@ -0,0 +1,46 @@ +@prefix ro: <./> . +@prefix dct: . +@prefix rdf: . +@prefix schema_org: . +@prefix sh: . +@prefix xml1: . +@prefix xsd: . + + +ro:A + a sh:NodeShape ; + 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 ; + ] . + diff --git a/tests/unit/requirements/test_load_requirements.py b/tests/unit/requirements/test_load_requirements.py index e3aa0ef0..db89aef4 100644 --- a/tests/unit/requirements/test_load_requirements.py +++ b/tests/unit/requirements/test_load_requirements.py @@ -16,7 +16,7 @@ import os from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER -from rocrate_validator.models import Profile +from rocrate_validator.models import LevelCollection, Profile, Severity from tests.ro_crates import InvalidFileDescriptorEntity # set up logging @@ -26,11 +26,68 @@ paths = InvalidFileDescriptorEntity() +def test_requirements_loading(profiles_requirement_loading: str): + + # The order of the requirement levels + levels = (LevelCollection.REQUIRED, LevelCollection.REQUIRED, LevelCollection.RECOMMENDED, LevelCollection.OPTIONAL) + + # Define the list of requirements names + requirements_names = ["A", "B", "A_MUST", "B_MUST"] + + # Define the number of checks for each requirement + number_of_checks_per_requirement = 4 + + # Define the settings + settings = { + "profiles_path": profiles_requirement_loading, + "severity": Severity.OPTIONAL + } + + # Load the profiles + profiles = Profile.load_profiles(**settings) + assert len(profiles) == 1 + + # Get the first profile + profile = profiles[0] + assert profile.identifier == "x", "The profile identifier is incorrect" + + # The first profile should have the following requirements + requirements = profile.get_requirements(severity=Severity.OPTIONAL) + assert len(requirements) == len(requirements_names), "The number of requirements is incorrect" + + # Sort requirements by their order + sorted_requirements = sorted(requirements, key=lambda x: (-x.severity_from_path.value, x.path.name, + x.name) if x.severity_from_path else (0, x.path.name, x.name)) + + # Check the order of the requirements + for i, requirement in enumerate(sorted_requirements): + if i < len(sorted_requirements) - 1: + assert requirement < requirements[i + 1] + + # Check the requirements and their checks + for requirement_name in requirements_names: + logger.error("The requirement: %r", requirement_name) + requirement = profile.get_requirement(requirement_name) + assert requirement.name == requirement_name, "The name of the requirement is incorrect" + if requirement_name in ["A", "B"]: + assert requirement.severity_from_path is None, "The severity of the requirement should be None" + elif requirement_name in ["A_MUST", "B_MUST"]: + 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, "The number of requirement checks is incorrect" + + for i in range(number_of_checks_per_requirement): + logger.error("The requirement check: %r", f"{requirement_name}_{i}") + check = requirement.get_checks()[i] + 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" + + def test_order_of_loaded_profile_requirements(profiles_path: str): """Test the order of the loaded profiles.""" logger.debug("The profiles path: %r", profiles_path) assert os.path.exists(profiles_path) - profiles = Profile.load_profiles(profiles_path=profiles_path) + profiles = Profile.load_profiles(profiles_path=profiles_path, severity=Severity.RECOMMENDED) # The number of profiles should be greater than 0 assert len(profiles) > 0 @@ -44,7 +101,7 @@ def test_order_of_loaded_profile_requirements(profiles_path: str): requirements = profile.get_requirements() assert len(requirements) > 0 for requirement in requirements: - logger.error("%r The requirement: %r -> severity: %r (path: %s)", requirement.order_number, + logger.debug("%r The requirement: %r -> severity: %r (path: %s)", requirement.order_number, requirement.name, requirement.severity_from_path, requirement.path) # Sort requirements by their order @@ -54,3 +111,20 @@ def test_order_of_loaded_profile_requirements(profiles_path: str): for i, requirement in enumerate(requirements): if i < len(requirements) - 1: assert requirement < requirements[i + 1] + + # Check severity of some RequirementChecks + for r in profile.get_requirements(severity=Severity.OPTIONAL): + logger.debug("The requirement: %r -> severity: %r", r.name, r.severity_from_path) + + r = profile.get_requirement("RO-Crate Root Data Entity RECOMMENDED value") + assert r.severity_from_path == Severity.RECOMMENDED, "The severity of the requirement should be RECOMMENDED" + + # Check the number of requirement checks + r_checks = r.get_checks() + assert len(r_checks) == 1, "The number of requirement checks should be 1" + + # Inspect the first requirement check + requirement_check = r_checks[0] + assert requirement_check.name == "Root Data Entity: RECOMMENDED value", "The name of the requirement check is incorrect" + assert requirement_check.description == "Check if the Root Data Entity is denoted by the string `./` in the file descriptor JSON-LD", "The description of the requirement check is incorrect" + assert requirement_check.severity == Severity.RECOMMENDED, "The severity of the requirement check is incorrect" From eb4ef8cc7342a65cca8e82c5efbcb3bba923a4e0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 17 Sep 2024 17:46:15 +0200 Subject: [PATCH 885/902] refactor(tests): :truck: rename test data files --- tests/data/profiles/requirement_loading/x/{shape_a.ttl => a.ttl} | 0 tests/data/profiles/requirement_loading/x/{req_b.py => b.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/data/profiles/requirement_loading/x/{shape_a.ttl => a.ttl} (100%) rename tests/data/profiles/requirement_loading/x/{req_b.py => b.py} (100%) diff --git a/tests/data/profiles/requirement_loading/x/shape_a.ttl b/tests/data/profiles/requirement_loading/x/a.ttl similarity index 100% rename from tests/data/profiles/requirement_loading/x/shape_a.ttl rename to tests/data/profiles/requirement_loading/x/a.ttl diff --git a/tests/data/profiles/requirement_loading/x/req_b.py b/tests/data/profiles/requirement_loading/x/b.py similarity index 100% rename from tests/data/profiles/requirement_loading/x/req_b.py rename to tests/data/profiles/requirement_loading/x/b.py From a73be017e4e8d83b0e1f852d2138521324d50377 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 17 Sep 2024 17:48:19 +0200 Subject: [PATCH 886/902] refactor(profiles/ro-crate): :recycle: move WebDataEntity shapes --- .../must/5_web_data_entity_metadata.ttl | 41 ------------ .../should/5_web_data_entity_metadata.ttl | 65 +++++++++++++++++++ 2 files changed, 65 insertions(+), 41 deletions(-) create mode 100644 rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.ttl diff --git a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl index 5662828e..3ed7955e 100644 --- a/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/must/5_web_data_entity_metadata.ttl @@ -48,44 +48,3 @@ ro-crate:WebBasedDataEntity a sh:NodeShape, validator:HiddenShape ; sh:object ro-crate:WebDataEntity ; ] . - -ro-crate:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; - sh:name "Web-based Data Entity: RECOMMENDED properties" ; - sh:description """A Web-based Data Entity MUST be identified by an absolute URL and - SHOULD have a `contentSize` and `sdDatePublished` property""" ; - sh:targetClass ro-crate:WebDataEntity ; - # Check if the Web-based Data Entity has a contentSize property - sh:property [ - a sh:PropertyShape ; - sh:minCount 1 ; - sh:name "Web-based Data Entity: RECOMMENDED `contentSize` property string" ; - 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:Violation ; - sh:message """Web-based Data Entities SHOULD have a `contentSize` property""" ; - sh:sparql [ - sh:message "If the value is a string it must be a string representing an integer." ; - sh:select """ - SELECT ?this ?value - WHERE { - ?this schema:contentSize ?value . - FILTER NOT EXISTS { - FILTER (xsd:integer(?value) = ?value) - } - } - """ ; - ] ; - ] ; - # Check if the Web-based Data Entity has a sdDatePublished property - sh:property [ - a sh:PropertyShape ; - sh:minCount 1 ; - sh:name "Web-based Data Entity: RECOMMENDED `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:Violation ; - sh:message """Web-based Data Entities SHOULD have a `sdDatePublished` property""" ; - ] . 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 new file mode 100644 index 00000000..b32da861 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/should/5_web_data_entity_metadata.ttl @@ -0,0 +1,65 @@ +# 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. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix dct: . +@prefix schema_org: . +@prefix sh: . +@prefix owl: . +@prefix xsd: . +@prefix validator: . + + +ro-crate:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; + sh:name "Web-based Data Entity: RECOMMENDED properties" ; + sh:description """A Web-based Data Entity MUST be identified by an absolute URL and + SHOULD have a `contentSize` and `sdDatePublished` property""" ; + sh:targetClass ro-crate:WebDataEntity ; + # Check if the Web-based Data Entity has a contentSize property + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "Web-based Data Entity: `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:Violation ; + sh:message """Web-based Data Entities SHOULD have a `contentSize` property""" ; + sh:sparql [ + sh:message "If the value is a string it must be a string representing an integer." ; + sh:select """ + SELECT ?this ?value + WHERE { + ?this schema:contentSize ?value . + FILTER NOT EXISTS { + FILTER (xsd:integer(?value) = ?value) + } + } + """ ; + ] ; + ] ; + # Check if the Web-based Data Entity has a sdDatePublished property + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + 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:Violation ; + sh:message """Web-based Data Entities SHOULD have a `sdDatePublished` property""" ; + ] . From 367d1ca63098c91c64982a80a976f9e2cf4637e7 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 17 Sep 2024 17:53:28 +0200 Subject: [PATCH 887/902] fix(profiles/ro-crate): :bug: fix severity of WebDataEntity shapes --- .../profiles/ro-crate/should/5_web_data_entity_metadata.ttl | 4 ++-- 1 file changed, 2 insertions(+), 2 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 b32da861..759c1a9f 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 @@ -36,7 +36,7 @@ ro-crate:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; 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:Violation ; + sh:severity sh:Warning ; sh:message """Web-based Data Entities SHOULD have a `contentSize` property""" ; sh:sparql [ sh:message "If the value is a string it must be a string representing an integer." ; @@ -60,6 +60,6 @@ ro-crate:WebBasedDataEntityRequiredValueRestriction a sh:NodeShape ; 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:Violation ; + sh:severity sh:Warning ; sh:message """Web-based Data Entities SHOULD have a `sdDatePublished` property""" ; ] . From b0fbd56aff71c22dbe3ec568805131c956c583bb Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 18 Sep 2024 09:37:28 +0200 Subject: [PATCH 888/902] refactor(cli): :coffin: remove short option for `profiles_path` --- rocrate_validator/cli/commands/profiles.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index a465576f..f04f1802 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -38,7 +38,6 @@ @cli.group("profiles") @click.option( - "-p", "--profiles-path", type=click.Path(exists=True), default=DEFAULT_PROFILES_PATH, From a48b5eb7e7bef23f8d1e01453082e89d933847a2 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 18 Sep 2024 09:41:33 +0200 Subject: [PATCH 889/902] refactor(core): :recycle: use the `severity` property to denote the severity level of a Python requirement --- rocrate_validator/requirements/python/__init__.py | 14 +++++++------- tests/data/profiles/requirement_loading/x/b.py | 8 ++++---- .../profiles/requirement_loading/x/must/b_must.py | 8 ++++---- tests/unit/requirements/test_load_requirements.py | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index fc38f01c..fa927524 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -20,7 +20,7 @@ import rocrate_validator.log as logging from ...models import (LevelCollection, Profile, Requirement, RequirementCheck, - RequirementLevel, RequirementLoader, ValidationContext) + RequirementLevel, RequirementLoader, Severity, ValidationContext) from ...utils import get_classes_from_file # set up logging @@ -81,16 +81,16 @@ def __init_checks__(self): check_name = name.strip() check_description = member.__doc__.strip() if member.__doc__ else "" # init the check with the requirement level - check_level = None + severity = None try: - check_level = member.level + severity = member.severity except Exception: - check_level = self.requirement_level_from_path or LevelCollection.REQUIRED + severity = self.severity_from_path or Severity.REQUIRED check = self.requirement_check_class(self, check_name, member, description=check_description, - level=check_level) + level=LevelCollection.get(severity.name) if severity else None) self._checks.append(check) logger.debug("Added check: %s %r", check_name, check) @@ -116,7 +116,7 @@ def decorator(cls): return decorator -def check(name: Optional[str] = None, level: Optional[LevelCollection] = None): +def check(name: Optional[str] = None, severity: Optional[Severity] = None): """ A decorator to mark functions as "checks" (by setting an attribute `check=True`) and optionally annotating them with a human-legible name. @@ -132,7 +132,7 @@ def decorator(func): f"return bool but this only returns {sig.return_annotation}") func.check = True func.name = check_name - func.level = level + func.severity = severity return func return decorator diff --git a/tests/data/profiles/requirement_loading/x/b.py b/tests/data/profiles/requirement_loading/x/b.py index a1282e19..ea870580 100644 --- a/tests/data/profiles/requirement_loading/x/b.py +++ b/tests/data/profiles/requirement_loading/x/b.py @@ -13,7 +13,7 @@ # limitations under the License. import rocrate_validator.log as logging -from rocrate_validator.models import LevelCollection, ValidationContext +from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import (PyFunctionCheck, check, requirement) @@ -32,17 +32,17 @@ def check_b0(self, context: ValidationContext) -> bool: """Check B_0: no requirement level""" return True - @check(name="B_1", level=LevelCollection.REQUIRED) + @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", level=LevelCollection.RECOMMENDED) + @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", level=LevelCollection.OPTIONAL) + @check(name="B_3", severity=Severity.OPTIONAL) def check_b3(self, context: ValidationContext) -> bool: """Check B_3: OPTIONAL requirement level""" return True diff --git a/tests/data/profiles/requirement_loading/x/must/b_must.py b/tests/data/profiles/requirement_loading/x/must/b_must.py index 87edca48..f63b2e33 100644 --- a/tests/data/profiles/requirement_loading/x/must/b_must.py +++ b/tests/data/profiles/requirement_loading/x/must/b_must.py @@ -13,7 +13,7 @@ # limitations under the License. import rocrate_validator.log as logging -from rocrate_validator.models import LevelCollection, ValidationContext +from rocrate_validator.models import Severity, ValidationContext from rocrate_validator.requirements.python import (PyFunctionCheck, check, requirement) @@ -32,17 +32,17 @@ def check_b0(self, context: ValidationContext) -> bool: """Check B_MUST_0: no requirement level""" return True - @check(name="B_MUST_1", level=LevelCollection.REQUIRED) + @check(name="B_MUST_1", severity=Severity.REQUIRED) def check_b1(self, context: ValidationContext) -> bool: """Check B_MUST_1: REQUIRED requirement level""" return True - @check(name="B_MUST_2", level=LevelCollection.RECOMMENDED) + @check(name="B_MUST_2", severity=Severity.RECOMMENDED) def check_b2(self, context: ValidationContext) -> bool: """Check B_MUST_2: RECOMMENDED requirement level""" return True - @check(name="B_MUST_3", level=LevelCollection.OPTIONAL) + @check(name="B_MUST_3", severity=Severity.OPTIONAL) def check_b3(self, context: ValidationContext) -> bool: """Check B_MUST_3: OPTIONAL requirement level""" return True diff --git a/tests/unit/requirements/test_load_requirements.py b/tests/unit/requirements/test_load_requirements.py index db89aef4..61ef9007 100644 --- a/tests/unit/requirements/test_load_requirements.py +++ b/tests/unit/requirements/test_load_requirements.py @@ -66,7 +66,7 @@ def test_requirements_loading(profiles_requirement_loading: str): # Check the requirements and their checks for requirement_name in requirements_names: - logger.error("The requirement: %r", requirement_name) + logger.debug("The requirement: %r", requirement_name) requirement = profile.get_requirement(requirement_name) assert requirement.name == requirement_name, "The name of the requirement is incorrect" if requirement_name in ["A", "B"]: @@ -77,7 +77,7 @@ def test_requirements_loading(profiles_requirement_loading: str): assert len(requirement.get_checks()) == number_of_checks_per_requirement, "The number of requirement checks is incorrect" for i in range(number_of_checks_per_requirement): - logger.error("The requirement check: %r", f"{requirement_name}_{i}") + logger.debug("The requirement check: %r", f"{requirement_name}_{i}") check = requirement.get_checks()[i] 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" From 702cd0c5a0cbb3b6f1f3205ef7240b4608bfe887 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 18 Sep 2024 12:25:50 +0200 Subject: [PATCH 890/902] fix(shacl): :bug: fix property getter --- rocrate_validator/requirements/shacl/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index 1eb7e117..f0412426 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -68,8 +68,6 @@ def name(self, value: str): @property def description(self) -> str: """Return the description of the shape""" - if not self._description: - self._description = getattr(self, "description", None) return self._description @description.setter From e27abf009ed4b4f2bf8a094a6ab64f8f491c9333 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 18 Sep 2024 12:56:49 +0200 Subject: [PATCH 891/902] test(core): :white_check_mark: fix test --- tests/unit/requirements/test_load_requirements.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/requirements/test_load_requirements.py b/tests/unit/requirements/test_load_requirements.py index 61ef9007..d52a3e82 100644 --- a/tests/unit/requirements/test_load_requirements.py +++ b/tests/unit/requirements/test_load_requirements.py @@ -105,7 +105,8 @@ def test_order_of_loaded_profile_requirements(profiles_path: str): requirement.name, requirement.severity_from_path, requirement.path) # Sort requirements by their order - requirements = sorted(requirements, key=lambda x: (-x.severity_from_path.value, x.path.name, x.name)) + requirements = sorted(requirements, key=lambda x: (-x.severity_from_path.value, x.path.name, x.name) + if x.severity_from_path else (0, x.path.name, x.name)) # Check the order of the requirements for i, requirement in enumerate(requirements): From 83a6e99ed2a601fec48ec38b6128f2e0133bb5e0 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 18 Sep 2024 15:35:03 +0200 Subject: [PATCH 892/902] fix(tests): :rotating_light: fix flake8 warning --- .../requirements/test_load_requirements.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/unit/requirements/test_load_requirements.py b/tests/unit/requirements/test_load_requirements.py index d52a3e82..770c320d 100644 --- a/tests/unit/requirements/test_load_requirements.py +++ b/tests/unit/requirements/test_load_requirements.py @@ -56,8 +56,9 @@ def test_requirements_loading(profiles_requirement_loading: str): assert len(requirements) == len(requirements_names), "The number of requirements is incorrect" # Sort requirements by their order - sorted_requirements = sorted(requirements, key=lambda x: (-x.severity_from_path.value, x.path.name, - x.name) if x.severity_from_path else (0, x.path.name, x.name)) + sorted_requirements = sorted( + requirements, key=lambda x: (-x.severity_from_path.value, x.path.name, x.name) + if x.severity_from_path else (0, x.path.name, x.name)) # Check the order of the requirements for i, requirement in enumerate(sorted_requirements): @@ -72,9 +73,11 @@ def test_requirements_loading(profiles_requirement_loading: str): if requirement_name in ["A", "B"]: assert requirement.severity_from_path is None, "The severity of the requirement should be None" elif requirement_name in ["A_MUST", "B_MUST"]: - assert requirement.severity_from_path == Severity.REQUIRED, "The severity of the requirement should be REQUIRED" + 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, "The number of requirement checks is incorrect" + assert len(requirement.get_checks()) == number_of_checks_per_requirement, \ + "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}") @@ -126,6 +129,9 @@ def test_order_of_loaded_profile_requirements(profiles_path: str): # Inspect the first requirement check requirement_check = r_checks[0] - assert requirement_check.name == "Root Data Entity: RECOMMENDED value", "The name of the requirement check is incorrect" - assert requirement_check.description == "Check if the Root Data Entity is denoted by the string `./` in the file descriptor JSON-LD", "The description of the requirement check is incorrect" + assert requirement_check.name == "Root Data Entity: RECOMMENDED value", \ + "The name of the requirement check is incorrect" + assert requirement_check.description == \ + "Check if the Root Data Entity is denoted by the string `./` in the file descriptor JSON-LD", \ + "The description of the requirement check is incorrect" assert requirement_check.severity == Severity.RECOMMENDED, "The severity of the requirement check is incorrect" From 99c0af60592ac0b35950791a5f232adbea5ab729 Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 19 Sep 2024 11:30:44 +0200 Subject: [PATCH 893/902] wtroc: more tests for test instance --- .../must/2_test_instance.ttl | 4 +- .../ro-crate-metadata.json | 132 ++++++++++++++++++ .../ro-crate-metadata.json | 132 ++++++++++++++++++ .../test_wtroc_testinstance.py | 30 ++++ tests/ro_crates.py | 8 ++ 5 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_no_resource/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_no_url/ro-crate-metadata.json diff --git a/rocrate_validator/profiles/workflow-testing-ro-crate/must/2_test_instance.ttl b/rocrate_validator/profiles/workflow-testing-ro-crate/must/2_test_instance.ttl index 54565c57..6c31c7cf 100644 --- a/rocrate_validator/profiles/workflow-testing-ro-crate/must/2_test_instance.ttl +++ b/rocrate_validator/profiles/workflow-testing-ro-crate/must/2_test_instance.ttl @@ -51,9 +51,9 @@ workflow-testing-ro-crate:WTROCTestInstanceRequired a sh:NodeShape ; ] ; sh:property [ a sh:PropertyShape ; sh:name "TestInstance resource" ; - sh:description "The TestInstance MUST refer to the relative URL of the test project base URL via resource" ; + sh:description "The TestInstance MUST refer to the relative URL of the test project via resource" ; sh:path wftest:resource ; sh:datatype xsd:string ; sh:minCount 1 ; - sh:message "The TestInstance MUST refer to the relative URL of the test project base URL via resource" ; + sh:message "The TestInstance MUST refer to the relative URL of the test project via resource" ; ] . diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_no_resource/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_no_resource/ro-crate-metadata.json new file mode 100644 index 00000000..80fae221 --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_no_resource/ro-crate-metadata.json @@ -0,0 +1,132 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "instance": [ + { + "@id": "#test1_1" + } + ], + "definition": { + "@id": "sort-and-change-case-tests.yml" + } + }, + { + "@id": "#test1_1", + "name": "test1_1", + "@type": "TestInstance", + "runsOn": { + "@id": "https://w3id.org/ro/terms/test#JenkinsService" + }, + "url": "http://example.org/jenkins" + }, + { + "@id": "https://w3id.org/ro/terms/test#JenkinsService", + "@type": "TestService", + "name": "Jenkins", + "url": { + "@id": "https://www.jenkins.io" + } + }, + { + "@id": "sort-and-change-case-tests.yml", + "@type": [ + "File", + "TestDefinition" + ], + "conformsTo": { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" + }, + "engineVersion": ">=0.70" + }, + { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "@type": "SoftwareApplication", + "name": "Planemo", + "url": { + "@id": "https://github.com/galaxyproject/planemo" + } + } + ] +} diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_no_url/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_no_url/ro-crate-metadata.json new file mode 100644 index 00000000..f302958c --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_no_url/ro-crate-metadata.json @@ -0,0 +1,132 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "instance": [ + { + "@id": "#test1_1" + } + ], + "definition": { + "@id": "sort-and-change-case-tests.yml" + } + }, + { + "@id": "#test1_1", + "name": "test1_1", + "@type": "TestInstance", + "runsOn": { + "@id": "https://w3id.org/ro/terms/test#JenkinsService" + }, + "resource": "job/tests/" + }, + { + "@id": "https://w3id.org/ro/terms/test#JenkinsService", + "@type": "TestService", + "name": "Jenkins", + "url": { + "@id": "https://www.jenkins.io" + } + }, + { + "@id": "sort-and-change-case-tests.yml", + "@type": [ + "File", + "TestDefinition" + ], + "conformsTo": { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" + }, + "engineVersion": ">=0.70" + }, + { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "@type": "SoftwareApplication", + "name": "Planemo", + "url": { + "@id": "https://github.com/galaxyproject/planemo" + } + } + ] +} diff --git a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testinstance.py b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testinstance.py index f2cbf03d..6ec6c970 100644 --- a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testinstance.py +++ b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testinstance.py @@ -35,3 +35,33 @@ def test_wtroc_testinstance_no_service(): ["The TestInstance MUST refer to a TestService via runsOn"], profile_identifier="workflow-testing-ro-crate" ) + + +def test_wtroc_testinstance_no_url(): + """\ + Test a Workflow Testing RO-Crate where a TestInstance does not refer to + the test service base URL. + """ + do_entity_test( + InvalidWTROC().testinstance_no_url, + Severity.REQUIRED, + False, + ["Workflow Testing RO-Crate TestInstance MUST"], + ["The TestInstance MUST refer to the test service base URL via url"], + profile_identifier="workflow-testing-ro-crate" + ) + + +def test_wtroc_testinstance_no_resource(): + """\ + Test a Workflow Testing RO-Crate where a TestInstance does not refer to + the relative URL of the test project via resource. + """ + do_entity_test( + InvalidWTROC().testinstance_no_resource, + Severity.REQUIRED, + False, + ["Workflow Testing RO-Crate TestInstance MUST"], + ["The TestInstance MUST refer to the relative URL of the test project via resource"], + profile_identifier="workflow-testing-ro-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 29d51172..ac1c2b93 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -488,3 +488,11 @@ def testsuite_no_mainentity(self) -> Path: @property def testinstance_no_service(self) -> Path: return self.base_path / "testinstance_no_service" + + @property + def testinstance_no_url(self) -> Path: + return self.base_path / "testinstance_no_url" + + @property + def testinstance_no_resource(self) -> Path: + return self.base_path / "testinstance_no_resource" From 0e091cf03e495e49987d893f857ebf39dd7e960b Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 19 Sep 2024 14:35:54 +0200 Subject: [PATCH 894/902] wtroc: add test definition --- .../must/3_test_definition.ttl | 56 ++++++++ .../ro-crate-metadata.json | 130 +++++++++++++++++ .../ro-crate-metadata.json | 122 ++++++++++++++++ .../ro-crate-metadata.json | 132 ++++++++++++++++++ .../test_wtroc_testdefinition.py | 67 +++++++++ tests/ro_crates.py | 12 ++ 6 files changed, 519 insertions(+) create mode 100644 rocrate_validator/profiles/workflow-testing-ro-crate/must/3_test_definition.ttl create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_bad_type/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_no_engine/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_no_engineversion/ro-crate-metadata.json create mode 100644 tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testdefinition.py diff --git a/rocrate_validator/profiles/workflow-testing-ro-crate/must/3_test_definition.ttl b/rocrate_validator/profiles/workflow-testing-ro-crate/must/3_test_definition.ttl new file mode 100644 index 00000000..1b9f4b8c --- /dev/null +++ b/rocrate_validator/profiles/workflow-testing-ro-crate/must/3_test_definition.ttl @@ -0,0 +1,56 @@ +# 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. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix xsd: . +@prefix dct: . +@prefix workflow-testing-ro-crate: . +@prefix schema: . +@prefix sh: . +@prefix wroc: . +@prefix wftest: . + +workflow-testing-ro-crate:WTROCTestDefinitionRequired a sh:NodeShape ; + sh:name "Workflow Testing RO-Crate TestDefinition MUST" ; + sh:description "Required properties of the Workflow Testing RO-Crate TestDefinition" ; + sh:targetClass wftest:TestDefinition ; + sh:property [ + a sh:PropertyShape ; + sh:name "TestDefinition type" ; + sh:description "The TestDefinition MUST have types TestDefinition and File" ; + sh:path rdf:type ; + sh:hasValue schema:MediaObject, wftest:TestDefinition ; + sh:minCount 1 ; + sh:message "The TestDefinition MUST have types TestDefinition and File" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "TestDefinition conformsTo" ; + sh:description "The TestDefinition MUST refer to the test engine it is written for via conformsTo" ; + sh:path dct:conformsTo ; + sh:class schema:SoftwareApplication ; + sh:minCount 1 ; + sh:message "The TestDefinition MUST refer to the test engine it is written for via conformsTo" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "TestDefinition engineVersion" ; + sh:description "The TestDefinition MUST refer to the test engine version via engineVersion" ; + sh:path wftest:engineVersion ; + sh:datatype xsd:string ; + sh:minCount 1 ; + sh:message "The TestDefinition MUST refer to the test engine version via engineVersion" ; + ] . diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_bad_type/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_bad_type/ro-crate-metadata.json new file mode 100644 index 00000000..daeb9e29 --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_bad_type/ro-crate-metadata.json @@ -0,0 +1,130 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "instance": [ + { + "@id": "#test1_1" + } + ], + "definition": { + "@id": "sort-and-change-case-tests.yml" + } + }, + { + "@id": "#test1_1", + "name": "test1_1", + "@type": "TestInstance", + "runsOn": { + "@id": "https://w3id.org/ro/terms/test#JenkinsService" + }, + "url": "http://example.org/jenkins", + "resource": "job/tests/" + }, + { + "@id": "https://w3id.org/ro/terms/test#JenkinsService", + "@type": "TestService", + "name": "Jenkins", + "url": { + "@id": "https://www.jenkins.io" + } + }, + { + "@id": "sort-and-change-case-tests.yml", + "@type": "TestDefinition", + "conformsTo": { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" + }, + "engineVersion": ">=0.70" + }, + { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "@type": "SoftwareApplication", + "name": "Planemo", + "url": { + "@id": "https://github.com/galaxyproject/planemo" + } + } + ] +} diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_no_engine/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_no_engine/ro-crate-metadata.json new file mode 100644 index 00000000..78ca9ef7 --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_no_engine/ro-crate-metadata.json @@ -0,0 +1,122 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "instance": [ + { + "@id": "#test1_1" + } + ], + "definition": { + "@id": "sort-and-change-case-tests.yml" + } + }, + { + "@id": "#test1_1", + "name": "test1_1", + "@type": "TestInstance", + "runsOn": { + "@id": "https://w3id.org/ro/terms/test#JenkinsService" + }, + "url": "http://example.org/jenkins", + "resource": "job/tests/" + }, + { + "@id": "https://w3id.org/ro/terms/test#JenkinsService", + "@type": "TestService", + "name": "Jenkins", + "url": { + "@id": "https://www.jenkins.io" + } + }, + { + "@id": "sort-and-change-case-tests.yml", + "@type": [ + "File", + "TestDefinition" + ], + "engineVersion": ">=0.70" + } + ] +} diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_no_engineversion/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_no_engineversion/ro-crate-metadata.json new file mode 100644 index 00000000..0742cea9 --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_no_engineversion/ro-crate-metadata.json @@ -0,0 +1,132 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "instance": [ + { + "@id": "#test1_1" + } + ], + "definition": { + "@id": "sort-and-change-case-tests.yml" + } + }, + { + "@id": "#test1_1", + "name": "test1_1", + "@type": "TestInstance", + "runsOn": { + "@id": "https://w3id.org/ro/terms/test#JenkinsService" + }, + "url": "http://example.org/jenkins", + "resource": "job/tests/" + }, + { + "@id": "https://w3id.org/ro/terms/test#JenkinsService", + "@type": "TestService", + "name": "Jenkins", + "url": { + "@id": "https://www.jenkins.io" + } + }, + { + "@id": "sort-and-change-case-tests.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" + } + } + ] +} diff --git a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testdefinition.py b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testdefinition.py new file mode 100644 index 00000000..7e23d44c --- /dev/null +++ b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testdefinition.py @@ -0,0 +1,67 @@ +# 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.models import Severity +from tests.ro_crates import InvalidWTROC +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_wtroc_testdefinition_bad_type(): + """\ + Test a Workflow Testing RO-Crate where a TestDefinition does not have the + File (MediaObject) and TestDefinition types. + """ + do_entity_test( + InvalidWTROC().testdefinition_bad_type, + Severity.REQUIRED, + False, + ["Workflow Testing RO-Crate TestDefinition MUST"], + ["The TestDefinition MUST have types TestDefinition and File"], + profile_identifier="workflow-testing-ro-crate" + ) + + +def test_wtroc_testdefinition_no_engine(): + """\ + Test a Workflow Testing RO-Crate where a TestDefinition does not refer + to the test engine SoftwareApplication via conformsTo. + """ + do_entity_test( + InvalidWTROC().testdefinition_no_engine, + Severity.REQUIRED, + False, + ["Workflow Testing RO-Crate TestDefinition MUST"], + ["The TestDefinition MUST refer to the test engine it is written for via conformsTo"], + profile_identifier="workflow-testing-ro-crate" + ) + + +def test_wtroc_testdefinition_no_engineversion(): + """\ + Test a Workflow Testing RO-Crate where a TestDefinition does not refer + to the test engine's version via engineVersion. + """ + do_entity_test( + InvalidWTROC().testdefinition_no_engineversion, + Severity.REQUIRED, + False, + ["Workflow Testing RO-Crate TestDefinition MUST"], + ["The TestDefinition MUST refer to the test engine version via engineVersion"], + profile_identifier="workflow-testing-ro-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index ac1c2b93..809b72b6 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -496,3 +496,15 @@ def testinstance_no_url(self) -> Path: @property def testinstance_no_resource(self) -> Path: return self.base_path / "testinstance_no_resource" + + @property + def testdefinition_bad_type(self) -> Path: + return self.base_path / "testdefinition_bad_type" + + @property + def testdefinition_no_engine(self) -> Path: + return self.base_path / "testdefinition_no_engine" + + @property + def testdefinition_no_engineversion(self) -> Path: + return self.base_path / "testdefinition_no_engineversion" From 2f7ab7b84a1b76f8cf584f9d5c42b684671abca0 Mon Sep 17 00:00:00 2001 From: simleo Date: Thu, 19 Sep 2024 16:40:38 +0200 Subject: [PATCH 895/902] wtroc: more tests --- .../ro-crate-metadata.json | 133 +++++++++++++++++ .../ro-crate-metadata.json | 135 ++++++++++++++++++ .../ro-crate-metadata.json | 135 ++++++++++++++++++ .../ro-crate-metadata.json | 125 ++++++++++++++++ .../ro-crate-metadata.json | 133 +++++++++++++++++ .../ro-crate-metadata.json | 91 ++++++++++++ .../ro-crate-metadata.json | 93 ++++++++++++ .../ro-crate-metadata.json | 133 +++++++++++++++++ .../test_wtroc_testdefinition.py | 30 ++++ .../test_wtroc_testinstance.py | 45 ++++++ .../test_wtroc_testsuite.py | 45 ++++++ tests/ro_crates.py | 32 +++++ 12 files changed, 1130 insertions(+) create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_bad_conformsto/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_bad_engineversion/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_bad_resource/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_bad_runson/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_bad_url/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_bad_definition/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_bad_instance/ro-crate-metadata.json create mode 100644 tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_bad_mainentity/ro-crate-metadata.json diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_bad_conformsto/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_bad_conformsto/ro-crate-metadata.json new file mode 100644 index 00000000..87291855 --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_bad_conformsto/ro-crate-metadata.json @@ -0,0 +1,133 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "instance": [ + { + "@id": "#test1_1" + } + ], + "definition": { + "@id": "sort-and-change-case-tests.yml" + } + }, + { + "@id": "#test1_1", + "name": "test1_1", + "@type": "TestInstance", + "runsOn": { + "@id": "https://w3id.org/ro/terms/test#JenkinsService" + }, + "url": "http://example.org/jenkins", + "resource": "job/tests/" + }, + { + "@id": "https://w3id.org/ro/terms/test#JenkinsService", + "@type": "TestService", + "name": "Jenkins", + "url": { + "@id": "https://www.jenkins.io" + } + }, + { + "@id": "sort-and-change-case-tests.yml", + "@type": [ + "File", + "TestDefinition" + ], + "conformsTo": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "engineVersion": ">=0.70" + }, + { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "@type": "SoftwareApplication", + "name": "Planemo", + "url": { + "@id": "https://github.com/galaxyproject/planemo" + } + } + ] +} diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_bad_engineversion/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_bad_engineversion/ro-crate-metadata.json new file mode 100644 index 00000000..15655700 --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testdefinition_bad_engineversion/ro-crate-metadata.json @@ -0,0 +1,135 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "instance": [ + { + "@id": "#test1_1" + } + ], + "definition": { + "@id": "sort-and-change-case-tests.yml" + } + }, + { + "@id": "#test1_1", + "name": "test1_1", + "@type": "TestInstance", + "runsOn": { + "@id": "https://w3id.org/ro/terms/test#JenkinsService" + }, + "url": "http://example.org/jenkins", + "resource": "job/tests/" + }, + { + "@id": "https://w3id.org/ro/terms/test#JenkinsService", + "@type": "TestService", + "name": "Jenkins", + "url": { + "@id": "https://www.jenkins.io" + } + }, + { + "@id": "sort-and-change-case-tests.yml", + "@type": [ + "File", + "TestDefinition" + ], + "conformsTo": { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" + }, + "engineVersion": { + "@id": "http://example.com/foobar" + } + }, + { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "@type": "SoftwareApplication", + "name": "Planemo", + "url": { + "@id": "https://github.com/galaxyproject/planemo" + } + } + ] +} diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_bad_resource/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_bad_resource/ro-crate-metadata.json new file mode 100644 index 00000000..57530bee --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_bad_resource/ro-crate-metadata.json @@ -0,0 +1,135 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "instance": [ + { + "@id": "#test1_1" + } + ], + "definition": { + "@id": "sort-and-change-case-tests.yml" + } + }, + { + "@id": "#test1_1", + "name": "test1_1", + "@type": "TestInstance", + "runsOn": { + "@id": "https://w3id.org/ro/terms/test#JenkinsService" + }, + "url": "http://example.org/jenkins", + "resource": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + } + }, + { + "@id": "https://w3id.org/ro/terms/test#JenkinsService", + "@type": "TestService", + "name": "Jenkins", + "url": { + "@id": "https://www.jenkins.io" + } + }, + { + "@id": "sort-and-change-case-tests.yml", + "@type": [ + "File", + "TestDefinition" + ], + "conformsTo": { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" + }, + "engineVersion": ">=0.70" + }, + { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "@type": "SoftwareApplication", + "name": "Planemo", + "url": { + "@id": "https://github.com/galaxyproject/planemo" + } + } + ] +} diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_bad_runson/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_bad_runson/ro-crate-metadata.json new file mode 100644 index 00000000..1eb769f4 --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_bad_runson/ro-crate-metadata.json @@ -0,0 +1,125 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "instance": [ + { + "@id": "#test1_1" + } + ], + "definition": { + "@id": "sort-and-change-case-tests.yml" + } + }, + { + "@id": "#test1_1", + "name": "test1_1", + "@type": "TestInstance", + "runsOn": { + "@id": "sort-and-change-case.ga" + }, + "url": "http://example.org/jenkins", + "resource": "job/tests/" + }, + { + "@id": "sort-and-change-case-tests.yml", + "@type": [ + "File", + "TestDefinition" + ], + "conformsTo": { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" + }, + "engineVersion": ">=0.70" + }, + { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "@type": "SoftwareApplication", + "name": "Planemo", + "url": { + "@id": "https://github.com/galaxyproject/planemo" + } + } + ] +} diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_bad_url/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_bad_url/ro-crate-metadata.json new file mode 100644 index 00000000..dc4fc7b4 --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testinstance_bad_url/ro-crate-metadata.json @@ -0,0 +1,133 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "instance": [ + { + "@id": "#test1_1" + } + ], + "definition": { + "@id": "sort-and-change-case-tests.yml" + } + }, + { + "@id": "#test1_1", + "name": "test1_1", + "@type": "TestInstance", + "runsOn": { + "@id": "https://w3id.org/ro/terms/test#JenkinsService" + }, + "url": "foobar", + "resource": "job/tests/" + }, + { + "@id": "https://w3id.org/ro/terms/test#JenkinsService", + "@type": "TestService", + "name": "Jenkins", + "url": { + "@id": "https://www.jenkins.io" + } + }, + { + "@id": "sort-and-change-case-tests.yml", + "@type": [ + "File", + "TestDefinition" + ], + "conformsTo": { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" + }, + "engineVersion": ">=0.70" + }, + { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "@type": "SoftwareApplication", + "name": "Planemo", + "url": { + "@id": "https://github.com/galaxyproject/planemo" + } + } + ] +} diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_bad_definition/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_bad_definition/ro-crate-metadata.json new file mode 100644 index 00000000..be902fd9 --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_bad_definition/ro-crate-metadata.json @@ -0,0 +1,91 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "definition": { + "@id": "sort-and-change-case.ga" + } + } + ] +} diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_bad_instance/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_bad_instance/ro-crate-metadata.json new file mode 100644 index 00000000..87ad51ba --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_bad_instance/ro-crate-metadata.json @@ -0,0 +1,93 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "instance": [ + { + "@id": "sort-and-change-case.ga" + } + ] + } + ] +} diff --git a/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_bad_mainentity/ro-crate-metadata.json b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_bad_mainentity/ro-crate-metadata.json new file mode 100644 index 00000000..d29f7df9 --- /dev/null +++ b/tests/data/crates/invalid/5_workflow_testing_ro_crate/testsuite_bad_mainentity/ro-crate-metadata.json @@ -0,0 +1,133 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.1/context", + "https://w3id.org/ro/terms/test" + ], + "@graph": [ + { + "@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": "./", + "@type": "Dataset", + "datePublished": "2024-09-17T11:09:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case-tests.yml" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + }, + "mentions": [ + { + "@id": "#test1" + } + ] + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@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" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + } + }, + { + "@id": "#test1", + "name": "test1", + "@type": "TestSuite", + "mainEntity": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "instance": [ + { + "@id": "#test1_1" + } + ], + "definition": { + "@id": "sort-and-change-case-tests.yml" + } + }, + { + "@id": "#test1_1", + "name": "test1_1", + "@type": "TestInstance", + "runsOn": { + "@id": "https://w3id.org/ro/terms/test#JenkinsService" + }, + "url": "http://example.org/jenkins", + "resource": "job/tests/" + }, + { + "@id": "https://w3id.org/ro/terms/test#JenkinsService", + "@type": "TestService", + "name": "Jenkins", + "url": { + "@id": "https://www.jenkins.io" + } + }, + { + "@id": "sort-and-change-case-tests.yml", + "@type": [ + "File", + "TestDefinition" + ], + "conformsTo": { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine" + }, + "engineVersion": ">=0.70" + }, + { + "@id": "https://w3id.org/ro/terms/test#PlanemoEngine", + "@type": "SoftwareApplication", + "name": "Planemo", + "url": { + "@id": "https://github.com/galaxyproject/planemo" + } + } + ] +} diff --git a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testdefinition.py b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testdefinition.py index 7e23d44c..f7f35cab 100644 --- a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testdefinition.py +++ b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testdefinition.py @@ -65,3 +65,33 @@ def test_wtroc_testdefinition_no_engineversion(): ["The TestDefinition MUST refer to the test engine version via engineVersion"], profile_identifier="workflow-testing-ro-crate" ) + + +def test_wtroc_testdefinition_bad_conformsto(): + """\ + Test a Workflow Testing RO-Crate where a TestDefinition does not refer + to the test engine SoftwareApplication via conformsTo. + """ + do_entity_test( + InvalidWTROC().testdefinition_bad_conformsto, + Severity.REQUIRED, + False, + ["Workflow Testing RO-Crate TestDefinition MUST"], + ["The TestDefinition MUST refer to the test engine it is written for via conformsTo"], + profile_identifier="workflow-testing-ro-crate" + ) + + +def test_wtroc_testdefinition_bad_engineversion(): + """\ + Test a Workflow Testing RO-Crate where a TestDefinition does not refer + to the test engine's version as a string. + """ + do_entity_test( + InvalidWTROC().testdefinition_bad_engineversion, + Severity.REQUIRED, + False, + ["Workflow Testing RO-Crate TestDefinition MUST"], + ["The TestDefinition MUST refer to the test engine version via engineVersion"], + profile_identifier="workflow-testing-ro-crate" + ) diff --git a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testinstance.py b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testinstance.py index 6ec6c970..f90aa9a5 100644 --- a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testinstance.py +++ b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testinstance.py @@ -65,3 +65,48 @@ def test_wtroc_testinstance_no_resource(): ["The TestInstance MUST refer to the relative URL of the test project via resource"], profile_identifier="workflow-testing-ro-crate" ) + + +def test_wtroc_testinstance_bad_runson(): + """\ + Test a Workflow Testing RO-Crate where a TestInstance has a runsOn + property that does not refer to a TestService. + """ + do_entity_test( + InvalidWTROC().testinstance_bad_runson, + Severity.REQUIRED, + False, + ["Workflow Testing RO-Crate TestInstance MUST"], + ["The TestInstance MUST refer to a TestService via runsOn"], + profile_identifier="workflow-testing-ro-crate" + ) + + +def test_wtroc_testinstance_bad_url(): + """\ + Test a Workflow Testing RO-Crate where a TestInstance has a url + property that does not refer to a string with a URL pattern. + """ + do_entity_test( + InvalidWTROC().testinstance_bad_url, + Severity.REQUIRED, + False, + ["Workflow Testing RO-Crate TestInstance MUST"], + ["The TestInstance MUST refer to the test service base URL via url"], + profile_identifier="workflow-testing-ro-crate" + ) + + +def test_wtroc_testinstance_bad_resource(): + """\ + Test a Workflow Testing RO-Crate where a TestInstance has a resource + property that does not refer to a string. + """ + do_entity_test( + InvalidWTROC().testinstance_bad_resource, + Severity.REQUIRED, + False, + ["Workflow Testing RO-Crate TestInstance MUST"], + ["The TestInstance MUST refer to the relative URL of the test project via resource"], + profile_identifier="workflow-testing-ro-crate" + ) diff --git a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py index ff673d9b..bb42070f 100644 --- a/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py +++ b/tests/integration/profiles/workflow-testing-ro-crate/test_wtroc_testsuite.py @@ -65,3 +65,48 @@ def test_wtroc_testsuite_no_mainentity(): ["The TestSuite SHOULD refer to the tested workflow via mainEntity"], profile_identifier="workflow-testing-ro-crate" ) + + +def test_wtroc_testsuite_bad_instance(): + """\ + Test a Workflow Testing RO-Crate where a TestSuite has an instance + property that does not refer to a TestInstance. + """ + do_entity_test( + InvalidWTROC().testsuite_bad_instance, + Severity.REQUIRED, + False, + ["TestSuite instance or definition"], + ["The TestSuite MUST refer to a TestInstance or TestDefinition"], + profile_identifier="workflow-testing-ro-crate" + ) + + +def test_wtroc_testsuite_bad_definition(): + """\ + Test a Workflow Testing RO-Crate where a TestSuite has a definition + property that does not refer to a TestDefinition. + """ + do_entity_test( + InvalidWTROC().testsuite_bad_definition, + Severity.REQUIRED, + False, + ["TestSuite instance or definition"], + ["The TestSuite MUST refer to a TestInstance or TestDefinition"], + profile_identifier="workflow-testing-ro-crate" + ) + + +def test_wtroc_testsuite_bad_mainentity(): + """\ + Test a Workflow Testing RO-Crate where a TestSuite has a mainEntity + property that does not refer to a workflow. + """ + do_entity_test( + InvalidWTROC().testsuite_bad_mainentity, + Severity.RECOMMENDED, + False, + ["Workflow Testing RO-Crate TestSuite SHOULD"], + ["The TestSuite SHOULD refer to the tested workflow via mainEntity"], + profile_identifier="workflow-testing-ro-crate" + ) diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 809b72b6..66ae548f 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -508,3 +508,35 @@ def testdefinition_no_engine(self) -> Path: @property def testdefinition_no_engineversion(self) -> Path: return self.base_path / "testdefinition_no_engineversion" + + @property + def testsuite_bad_instance(self) -> Path: + return self.base_path / "testsuite_bad_instance" + + @property + def testsuite_bad_definition(self) -> Path: + return self.base_path / "testsuite_bad_definition" + + @property + def testsuite_bad_mainentity(self) -> Path: + return self.base_path / "testsuite_bad_mainentity" + + @property + def testinstance_bad_runson(self) -> Path: + return self.base_path / "testinstance_bad_runson" + + @property + def testinstance_bad_url(self) -> Path: + return self.base_path / "testinstance_bad_url" + + @property + def testinstance_bad_resource(self) -> Path: + return self.base_path / "testinstance_bad_resource" + + @property + def testdefinition_bad_conformsto(self) -> Path: + return self.base_path / "testdefinition_bad_conformsto" + + @property + def testdefinition_bad_engineversion(self) -> Path: + return self.base_path / "testdefinition_bad_engineversion" From 9850fc1068ebc877c767ee05ea01d557500acc85 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Thu, 19 Sep 2024 19:29:26 +0200 Subject: [PATCH 896/902] ci: :sparkles: set up automatic release process --- .github/workflows/release.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1ddfc7c1..1018e336 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -30,6 +30,8 @@ jobs: name: Check Python package version runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') + env: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} steps: - name: Checkout uses: actions/checkout@v4 @@ -55,3 +57,19 @@ jobs: echo "Tag '${{ github.ref }}' matches the declared package version '$declared_version'" fi fi + - name: Build package + run: poetry build + - name: Publish artifact + uses: actions/upload-artifact@v4 + with: + name: rocrate-validator-dist + path: | + dist/*.whl + dist/*.tar.gz + - name: Publish to PyPI + run: | + poetry config repositories.testpypi https://test.pypi.org/legacy/ + poetry config pypi-token.testpypi $PYPI_TOKEN + poetry publish -r testpypi + - name: Deactivate virtual env + run: deactivate From ec1ccbb40c36c94defcc49702c5b08071da10557 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 24 Sep 2024 11:28:52 +0200 Subject: [PATCH 897/902] feat(ci): :building_construction: restructure release pipeline --- .github/workflows/release.yaml | 197 ++++++++++++++++++++++++++------- 1 file changed, 157 insertions(+), 40 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1018e336..afaefd1a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,12 +1,14 @@ -name: CI Release Pipeline +# This workflow is triggered on push to tags and runs the following steps: +# 1. Check and Build Distribution +# 2. Publish to TestPyPI +# 3. Publish to PyPI if the previous step is successful +# 4. Sign Distribution with Sigstore +# 5. Create GitHub Release with the signed distribution +name: ๐Ÿ“ฆ CI Pipeline 2 -- Release # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the master branch on: - # workflow_run: - # workflows: ["CI Testing Pipeline"] - # types: - # - completed push: tags: - "*.*.*" @@ -17,35 +19,51 @@ on: env: TERM: xterm - # enable Docker push only if the required secrets are defined - # ENABLE_DOCKER_PUSH: ${{ secrets.DOCKERHUB_USER != null && secrets.DOCKERHUB_TOKEN != null }} - # Base Image - # IMAGE: python:3.12-slim - # Define the virtual environment path VENV_PATH: .venv jobs: - # Verifies the declared version of the package - version: - name: Check Python package version + # Wait for the testing pipeline to finish + wait-for-testing: + name: ๐Ÿ•’ Wait for Testing Pipeline runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') - env: - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + if: ${{ github.repository == 'crs4/rocrate-validator' }} steps: - - name: Checkout + - 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 + if: ${{ github.repository == 'kikkomep/test-py-pipelines' }} + steps: + # Access the tag from the first workflow's outputs + - name: โฌ‡๏ธ Checkout code uses: actions/checkout@v4 - - name: Upgrade pip - run: pip install --upgrade pip - - name: Initialise a virtual env - run: python -m venv ${VENV_PATH} - - name: Enable virtual env - run: source ${VENV_PATH}/bin/activate - - name: Install Poetry - run: pip install poetry - - name: Install dependencies + - name: ๐Ÿ Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: ๐Ÿšง Set up Python Environment + run: | + pip install --upgrade pip + pip install poetry + - name: ๐Ÿ“ฆ Install Package Dependencies run: poetry install --no-interaction --no-ansi - - name: Check version + - name: โœ… Check version run: | if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref_type }}" == "tag" ]; then declared_version=$(poetry version -s) @@ -57,19 +75,118 @@ jobs: echo "Tag '${{ github.ref }}' matches the declared package version '$declared_version'" fi fi - - name: Build package + - name: ๐Ÿ—๏ธ Build a binary wheel and a source tarball run: poetry build - - name: Publish artifact + - name: ๐Ÿ“ฆ Store the distribution packages uses: actions/upload-artifact@v4 with: - name: rocrate-validator-dist - path: | - dist/*.whl - dist/*.tar.gz - - name: Publish to PyPI - run: | - poetry config repositories.testpypi https://test.pypi.org/legacy/ - poetry config pypi-token.testpypi $PYPI_TOKEN - poetry publish -r testpypi - - name: Deactivate virtual env - run: deactivate + name: python-package-distributions + path: | + dist/*.whl + dist/*.tar.gz + + # Publish to TestPyPI + publish-to-testpypi: + name: ๐Ÿ“ฆ Publish to TestPyPI + runs-on: ubuntu-latest + needs: build + environment: + name: testpypi + url: https://test.pypi.org/p/test-py-pipelines + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + steps: + - name: โฌ‡๏ธ Download all the distribution packages + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: ๐Ÿ“ฆ Publish distribution to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + + # Publish to PyPI + publish-to-pypi: + name: ๐Ÿ“ฆ Publish to PyPI + runs-on: ubuntu-latest + needs: [build, publish-to-testpypi] + environment: + name: pypi + url: https://pypi.org/p/test-py-pipelines + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + steps: + - name: โฌ‡๏ธ Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: ๐Ÿ“ฆ Publish distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + # Sign and Upload to GitHub Release + sign-packages: + name: ๐Ÿ–Š๏ธ Sign the Python distribution with Sigstore + needs: publish-to-pypi + runs-on: ubuntu-latest + + permissions: + contents: write # IMPORTANT: mandatory for making GitHub Releases + id-token: write # IMPORTANT: mandatory for sigstore + + steps: + - name: โฌ‡๏ธ Download all the distribution packages + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: ๐Ÿ–Š๏ธ Sign the dists with Sigstore + uses: sigstore/gh-action-sigstore-python@v2.1.1 + with: + inputs: >- + ./dist/*.tar.gz + ./dist/*.whl + - name: ๐Ÿ“ฆ Store the signed distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-signatures + path: dist/*.sigstore + + # Create GitHub Release + github_release: + name: ๐ŸŽ‰ Release on GitHub + needs: sign-packages + runs-on: ubuntu-latest + permissions: + contents: write # IMPORTANT: mandatory for making GitHub Releases + id-token: write # IMPORTANT: mandatory for sigstore + steps: + - name: โฌ‡๏ธ Download all the distribution packages + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: โฌ‡๏ธ Download all the distribution signatures + uses: actions/download-artifact@v4 + with: + name: python-package-signatures + path: dist/ + - name: ๐ŸŽ‰ Create GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + gh release create + '${{ github.ref_name }}' + --repo '${{ github.repository }}' + --generate-notes + - name: ๐Ÿ“ฆ Upload artifacts to GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + # Upload to GitHub Release using the `gh` CLI. + # `dist/` contains the built packages, and the + # sigstore-produced signatures and certificates. + run: >- + gh release upload + '${{ github.ref_name }}' dist/** + --repo '${{ github.repository }}' From 4b887dfe4626fab834b6a5b937ebaf058a4727a3 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 24 Sep 2024 12:01:43 +0200 Subject: [PATCH 898/902] feat(ci): :building_construction: restructure testing pipeline --- .github/workflows/testing.yaml | 44 ++++++++++++++++------------------ 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 1f32267d..f8380778 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -1,4 +1,4 @@ -name: CI Testing Pipeline +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 @@ -20,50 +20,48 @@ on: env: TERM: xterm - # enable Docker push only if the required secrets are defined - # ENABLE_DOCKER_PUSH: ${{ secrets.DOCKERHUB_USER != null && secrets.DOCKERHUB_TOKEN != null }} - # Base Image - # IMAGE: python:3.12-slim - # Define the virtual environment path VENV_PATH: .venv + PYTHON_VERSION: "3.11" jobs: # Verifies pep8, pyflakes and circular complexity flake8: - name: Lint Python Code (Flake8) (python ${{ matrix.python-version }}) + name: ๐Ÿšจ Lint Python Code runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.11"] steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v4 - - name: Set up Python v${{ matrix.python-version }} + - name: โฌ‡๏ธ Checkout code + uses: actions/checkout@v4 + - name: ๐Ÿ Set up Python v${{ env.PYTHON_VERSION }} uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} - - name: Install flake8 + python-version: ${{ env.PYTHON_VERSION }} + - name: ๐Ÿ”ฝ Install flake8 run: pip install flake8 - - name: Run checks + - name: โœ… Run checks run: flake8 -v rocrate_validator tests # Runs the tests test: - name: "Run tests" + name: โœ… Run tests runs-on: ubuntu-latest needs: [flake8] steps: - - name: Checkout + - name: โฌ‡๏ธ Checkout uses: actions/checkout@v4 - - name: Upgrade pip + - name: ๐Ÿ Set up Python v${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: ๐Ÿ”„ Upgrade pip run: pip install --upgrade pip - - name: Initialise a virtual env + - name: ๐Ÿ Initialise a virtual env run: python -m venv ${VENV_PATH} - - name: Enable virtual env + - name: ๐Ÿ Enable virtual env run: source ${VENV_PATH}/bin/activate - - name: Install Poetry + - name: ๐Ÿ”ฝ Install Poetry run: pip install poetry - - name: Install dependencies + - name: ๐Ÿ”ฝ Install dependencies run: poetry install --no-interaction --no-ansi - - name: Run tests + - name: โœ… Run tests run: poetry run pytest From 42dd23be99312878bf8a0041325f9e14addfa1d4 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 24 Sep 2024 12:02:50 +0200 Subject: [PATCH 899/902] docs(core): :memo: add package description --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a84360d8..d8307edc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "rocrate-validator" version = "0.1.2" -description = "" +description = "A Python package to validate RO-Crates" authors = [ "Marco Enrico Piras ", "Luca Pireddu ", From a99a5487de70d2ba151f1918b28afde025c1bf32 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Tue, 24 Sep 2024 12:09:07 +0200 Subject: [PATCH 900/902] chore: :art: minor changes --- .github/workflows/release.yaml | 2 +- .github/workflows/testing.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index afaefd1a..d1f0e073 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -33,7 +33,7 @@ jobs: id: wait-for-testing with: token: ${{ secrets.GITHUB_TOKEN }} - checkName: โœ… Run tests + checkName: โŒ› Run tests ref: ${{ github.sha }} - name: Do something with a passing build diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index f8380778..8505f61a 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -38,12 +38,12 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: ๐Ÿ”ฝ Install flake8 run: pip install flake8 - - name: โœ… Run checks + - name: โŒ› Run checks run: flake8 -v rocrate_validator tests # Runs the tests test: - name: โœ… Run tests + name: โŒ› Run tests runs-on: ubuntu-latest needs: [flake8] steps: @@ -63,5 +63,5 @@ jobs: run: pip install poetry - name: ๐Ÿ”ฝ Install dependencies run: poetry install --no-interaction --no-ansi - - name: โœ… Run tests + - name: โŒ› Run tests run: poetry run pytest From 5ebe44ebb8f54558049f30ff63c153733df30662 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 25 Sep 2024 10:26:33 +0200 Subject: [PATCH 901/902] build(core): :bookmark: update version number to 0.2.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d8307edc..8978a628 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "rocrate-validator" -version = "0.1.2" +version = "0.2.0" description = "A Python package to validate RO-Crates" authors = [ "Marco Enrico Piras ", From 1755a7491b21cd22dd379e8017f7cee176db55f9 Mon Sep 17 00:00:00 2001 From: Marco Enrico Piras Date: Wed, 25 Sep 2024 11:11:04 +0200 Subject: [PATCH 902/902] ci(github): :green_heart: fix skip condition on the release pipeline --- .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 d1f0e073..4512ded8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -48,7 +48,7 @@ jobs: name: ๐Ÿ— Check and Build Distribution runs-on: ubuntu-latest needs: wait-for-testing - if: ${{ github.repository == 'kikkomep/test-py-pipelines' }} + if: ${{ github.repository == 'crs4/rocrate-validator' }} steps: # Access the tag from the first workflow's outputs - name: โฌ‡๏ธ Checkout code