From 0023c3336cac13c3a675528372791a93a2e8a149 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Mon, 3 Jun 2024 08:39:10 +1000 Subject: [PATCH 1/3] Big changes to support sparql mode, some refactors, update readme. --- README.md | 44 ++- poetry.lock | 190 ++++++----- pyshacl/cli.py | 67 +++- pyshacl/constraints/core/other_constraints.py | 95 +++++- .../core/property_pair_constraints.py | 315 ++++++++++++++---- pyshacl/constraints/core/value_constraints.py | 43 ++- pyshacl/pytypes.py | 2 + pyshacl/shape.py | 211 +++++++++++- pyshacl/validate.py | 107 +++--- test/test_dash_validate.py | 72 ++++ test/test_sht_validate.py | 16 + 11 files changed, 901 insertions(+), 261 deletions(-) diff --git a/README.md b/README.md index e757916..02076aa 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,33 @@ You can get an equivalent of the Command Line Tool using the Python3 executable $ python3 -m pyshacl ``` +## Errors +Under certain circumstances pySHACL can produce a `Validation Failure`. This is a formal error defined by the SHACL specification and is required to be produced as a result of specific conditions within the SHACL graph. +If the validator produces a `Validation Failure`, the `results_graph` variable returned by the `validate()` function will be an instance of `ValidationFailure`. +See the `message` attribute on that instance to get more information about the validation failure. + +Other errors the validator can generate: +- `ShapeLoadError`: This error is thrown when a SHACL Shape in the SHACL graph is in an invalid state and cannot be loaded into the validation engine. +- `ConstraintLoadError`: This error is thrown when a SHACL Constraint Component is in an invalid state and cannot be loaded into the validation engine. +- `ReportableRuntimeError`: An error occurred for a different reason, and the reason should be communicated back to the user of the validator. +- `RuntimeError`: The validator encountered a situation that caused it to throw an error, but the reason does concern the user. + +Unlike `ValidationFailure`, these errors are not passed back as a result by the `validate()` function, but thrown as exceptions by the validation engine and must be +caught in a `try ... except` block. +In the case of `ShapeLoadError` and `ConstraintLoadError`, see the `str()` string representation of the exception instance for the error message along with a link to the relevant section in the SHACL spec document. + +## SPARQL Remote Graph Mode + +_**PySHACL now has a built-in SPARQL Remote Graph Mode, which allows you to validate a data graph that is stored on a remote server.**_ + +- In this mode, PySHAL operates strictly in read-only mode, and does not modify the remote data graph. +- Some features are disabled when using the SPARQL Remote Graph Mode: + - "rdfs" and "owl" inferencing is not allowed (because the remote graph is read-only, it cannot be expanded) + - Extra Ontology file (Inoculation or Mix-In mode) is disabled (because the remote graph is read-only) + - SHACL Rules (Advanced mode SPARQL-Rules) are not allowed (because the remote graph is read-only) + - All SHACL-JS features are disabled (this is not safe when operating on a remote graph) + - "inplace" mode is disabled (actually all operations on the remote data graph are inherently performed in-place) + ## Integrated OpenAPI-3.0-compatible HTTP REST Service PySHACL now has a built-in validation service, exposed via an OpenAPI3.0-compatible REST API. @@ -223,23 +250,6 @@ To view the OpenAPI3 schema see `http://127.0.0.1:8099/docs/openapi.json` - `PYSHACL_SERVER_HOSTNAME=example.org` when you are hosting the server behind a reverse-proxy or in a containerised environment, use this so PySHACL server knows what your externally facing hostname is - -## Errors -Under certain circumstances pySHACL can produce a `Validation Failure`. This is a formal error defined by the SHACL specification and is required to be produced as a result of specific conditions within the SHACL graph. -If the validator produces a `Validation Failure`, the `results_graph` variable returned by the `validate()` function will be an instance of `ValidationFailure`. -See the `message` attribute on that instance to get more information about the validation failure. - -Other errors the validator can generate: -- `ShapeLoadError`: This error is thrown when a SHACL Shape in the SHACL graph is in an invalid state and cannot be loaded into the validation engine. -- `ConstraintLoadError`: This error is thrown when a SHACL Constraint Component is in an invalid state and cannot be loaded into the validation engine. -- `ReportableRuntimeError`: An error occurred for a different reason, and the reason should be communicated back to the user of the validator. -- `RuntimeError`: The validator encountered a situation that caused it to throw an error, but the reason does concern the user. - -Unlike `ValidationFailure`, these errors are not passed back as a result by the `validate()` function, but thrown as exceptions by the validation engine and must be -caught in a `try ... except` block. -In the case of `ShapeLoadError` and `ConstraintLoadError`, see the `str()` string representation of the exception instance for the error message along with a link to the relevant section in the SHACL spec document. - - ## Windows CLI [Pyinstaller](https://www.pyinstaller.org/) can be diff --git a/poetry.lock b/poetry.lock index 4511efa..3f6c1bb 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.7.0 and should not be changed by hand. [[package]] name = "aiofiles" @@ -146,13 +146,13 @@ toml = ["tomli"] [[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] @@ -518,28 +518,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 = true 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] @@ -945,98 +946,111 @@ files = [ [[package]] name = "types-setuptools" -version = "69.2.0.20240317" +version = "70.0.0.20240524" description = "Typing stubs for setuptools" optional = true python-versions = ">=3.8" files = [ - {file = "types-setuptools-69.2.0.20240317.tar.gz", hash = "sha256:b607c4c48842ef3ee49dc0c7fe9c1bad75700b071e1018bb4d7e3ac492d47048"}, - {file = "types_setuptools-69.2.0.20240317-py3-none-any.whl", hash = "sha256:cf91ff7c87ab7bf0625c3f0d4d90427c9da68561f3b0feab77977aaf0bbf7531"}, + {file = "types-setuptools-70.0.0.20240524.tar.gz", hash = "sha256:e31fee7b9d15ef53980526579ac6089b3ae51a005a281acf97178e90ac71aff6"}, + {file = "types_setuptools-70.0.0.20240524-py3-none-any.whl", hash = "sha256:8f5379b9948682d72a9ab531fbe52932e84c4f38deda570255f9bae3edd766bc"}, ] [[package]] name = "typing-extensions" -version = "4.10.0" +version = "4.12.1" description = "Backported and Experimental Type Hints for Python 3.8+" optional = true 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.1-py3-none-any.whl", hash = "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a"}, + {file = "typing_extensions-4.12.1.tar.gz", hash = "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1"}, ] [[package]] name = "ujson" -version = "5.9.0" +version = "5.10.0" description = "Ultra fast JSON encoder and decoder for Python" optional = true python-versions = ">=3.8" files = [ - {file = "ujson-5.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab71bf27b002eaf7d047c54a68e60230fbd5cd9da60de7ca0aa87d0bccead8fa"}, - {file = "ujson-5.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a365eac66f5aa7a7fdf57e5066ada6226700884fc7dce2ba5483538bc16c8c5"}, - {file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e015122b337858dba5a3dc3533af2a8fc0410ee9e2374092f6a5b88b182e9fcc"}, - {file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:779a2a88c53039bebfbccca934430dabb5c62cc179e09a9c27a322023f363e0d"}, - {file = "ujson-5.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10ca3c41e80509fd9805f7c149068fa8dbee18872bbdc03d7cca928926a358d5"}, - {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a566e465cb2fcfdf040c2447b7dd9718799d0d90134b37a20dff1e27c0e9096"}, - {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f833c529e922577226a05bc25b6a8b3eb6c4fb155b72dd88d33de99d53113124"}, - {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b68a0caab33f359b4cbbc10065c88e3758c9f73a11a65a91f024b2e7a1257106"}, - {file = "ujson-5.9.0-cp310-cp310-win32.whl", hash = "sha256:7cc7e605d2aa6ae6b7321c3ae250d2e050f06082e71ab1a4200b4ae64d25863c"}, - {file = "ujson-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6d3f10eb8ccba4316a6b5465b705ed70a06011c6f82418b59278fbc919bef6f"}, - {file = "ujson-5.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b23bbb46334ce51ddb5dded60c662fbf7bb74a37b8f87221c5b0fec1ec6454b"}, - {file = "ujson-5.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6974b3a7c17bbf829e6c3bfdc5823c67922e44ff169851a755eab79a3dd31ec0"}, - {file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5964ea916edfe24af1f4cc68488448fbb1ec27a3ddcddc2b236da575c12c8ae"}, - {file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ba7cac47dd65ff88571eceeff48bf30ed5eb9c67b34b88cb22869b7aa19600d"}, - {file = "ujson-5.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bbd91a151a8f3358c29355a491e915eb203f607267a25e6ab10531b3b157c5e"}, - {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:829a69d451a49c0de14a9fecb2a2d544a9b2c884c2b542adb243b683a6f15908"}, - {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a807ae73c46ad5db161a7e883eec0fbe1bebc6a54890152ccc63072c4884823b"}, - {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8fc2aa18b13d97b3c8ccecdf1a3c405f411a6e96adeee94233058c44ff92617d"}, - {file = "ujson-5.9.0-cp311-cp311-win32.whl", hash = "sha256:70e06849dfeb2548be48fdd3ceb53300640bc8100c379d6e19d78045e9c26120"}, - {file = "ujson-5.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:7309d063cd392811acc49b5016728a5e1b46ab9907d321ebbe1c2156bc3c0b99"}, - {file = "ujson-5.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:20509a8c9f775b3a511e308bbe0b72897ba6b800767a7c90c5cca59d20d7c42c"}, - {file = "ujson-5.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b28407cfe315bd1b34f1ebe65d3bd735d6b36d409b334100be8cdffae2177b2f"}, - {file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d302bd17989b6bd90d49bade66943c78f9e3670407dbc53ebcf61271cadc399"}, - {file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f21315f51e0db8ee245e33a649dd2d9dce0594522de6f278d62f15f998e050e"}, - {file = "ujson-5.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5635b78b636a54a86fdbf6f027e461aa6c6b948363bdf8d4fbb56a42b7388320"}, - {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82b5a56609f1235d72835ee109163c7041b30920d70fe7dac9176c64df87c164"}, - {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5ca35f484622fd208f55041b042d9d94f3b2c9c5add4e9af5ee9946d2d30db01"}, - {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:829b824953ebad76d46e4ae709e940bb229e8999e40881338b3cc94c771b876c"}, - {file = "ujson-5.9.0-cp312-cp312-win32.whl", hash = "sha256:25fa46e4ff0a2deecbcf7100af3a5d70090b461906f2299506485ff31d9ec437"}, - {file = "ujson-5.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:60718f1720a61560618eff3b56fd517d107518d3c0160ca7a5a66ac949c6cf1c"}, - {file = "ujson-5.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d581db9db9e41d8ea0b2705c90518ba623cbdc74f8d644d7eb0d107be0d85d9c"}, - {file = "ujson-5.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ff741a5b4be2d08fceaab681c9d4bc89abf3c9db600ab435e20b9b6d4dfef12e"}, - {file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdcb02cabcb1e44381221840a7af04433c1dc3297af76fde924a50c3054c708c"}, - {file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e208d3bf02c6963e6ef7324dadf1d73239fb7008491fdf523208f60be6437402"}, - {file = "ujson-5.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4b3917296630a075e04d3d07601ce2a176479c23af838b6cf90a2d6b39b0d95"}, - {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0c4d6adb2c7bb9eb7c71ad6f6f612e13b264942e841f8cc3314a21a289a76c4e"}, - {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0b159efece9ab5c01f70b9d10bbb77241ce111a45bc8d21a44c219a2aec8ddfd"}, - {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0cb4a7814940ddd6619bdce6be637a4b37a8c4760de9373bac54bb7b229698b"}, - {file = "ujson-5.9.0-cp38-cp38-win32.whl", hash = "sha256:dc80f0f5abf33bd7099f7ac94ab1206730a3c0a2d17549911ed2cb6b7aa36d2d"}, - {file = "ujson-5.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:506a45e5fcbb2d46f1a51fead991c39529fc3737c0f5d47c9b4a1d762578fc30"}, - {file = "ujson-5.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0fd2eba664a22447102062814bd13e63c6130540222c0aa620701dd01f4be81"}, - {file = "ujson-5.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bdf7fc21a03bafe4ba208dafa84ae38e04e5d36c0e1c746726edf5392e9f9f36"}, - {file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2f909bc08ce01f122fd9c24bc6f9876aa087188dfaf3c4116fe6e4daf7e194f"}, - {file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd4ea86c2afd41429751d22a3ccd03311c067bd6aeee2d054f83f97e41e11d8f"}, - {file = "ujson-5.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:63fb2e6599d96fdffdb553af0ed3f76b85fda63281063f1cb5b1141a6fcd0617"}, - {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:32bba5870c8fa2a97f4a68f6401038d3f1922e66c34280d710af00b14a3ca562"}, - {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:37ef92e42535a81bf72179d0e252c9af42a4ed966dc6be6967ebfb929a87bc60"}, - {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f69f16b8f1c69da00e38dc5f2d08a86b0e781d0ad3e4cc6a13ea033a439c4844"}, - {file = "ujson-5.9.0-cp39-cp39-win32.whl", hash = "sha256:3382a3ce0ccc0558b1c1668950008cece9bf463ebb17463ebf6a8bfc060dae34"}, - {file = "ujson-5.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:6adef377ed583477cf005b58c3025051b5faa6b8cc25876e594afbb772578f21"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ffdfebd819f492e48e4f31c97cb593b9c1a8251933d8f8972e81697f00326ff1"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4eec2ddc046360d087cf35659c7ba0cbd101f32035e19047013162274e71fcf"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbb90aa5c23cb3d4b803c12aa220d26778c31b6e4b7a13a1f49971f6c7d088e"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0823cb70866f0d6a4ad48d998dd338dce7314598721bc1b7986d054d782dfd"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4e35d7885ed612feb6b3dd1b7de28e89baaba4011ecdf995e88be9ac614765e9"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b048aa93eace8571eedbd67b3766623e7f0acbf08ee291bef7d8106210432427"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:323279e68c195110ef85cbe5edce885219e3d4a48705448720ad925d88c9f851"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ac92d86ff34296f881e12aa955f7014d276895e0e4e868ba7fddebbde38e378"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6eecbd09b316cea1fd929b1e25f70382917542ab11b692cb46ec9b0a26c7427f"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:473fb8dff1d58f49912323d7cb0859df5585cfc932e4b9c053bf8cf7f2d7c5c4"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f91719c6abafe429c1a144cfe27883eace9fb1c09a9c5ef1bcb3ae80a3076a4e"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b1c0991c4fe256f5fdb19758f7eac7f47caac29a6c57d0de16a19048eb86bad"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a8ea0f55a1396708e564595aaa6696c0d8af532340f477162ff6927ecc46e21"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:07e0cfdde5fd91f54cd2d7ffb3482c8ff1bf558abf32a8b953a5d169575ae1cd"}, - {file = "ujson-5.9.0.tar.gz", hash = "sha256:89cc92e73d5501b8a7f48575eeb14ad27156ad092c2e9fc7e3cf949f07e75532"}, + {file = "ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd"}, + {file = "ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf"}, + {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6"}, + {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569"}, + {file = "ujson-5.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770"}, + {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1"}, + {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5"}, + {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51"}, + {file = "ujson-5.10.0-cp310-cp310-win32.whl", hash = "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518"}, + {file = "ujson-5.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f"}, + {file = "ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00"}, + {file = "ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126"}, + {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8"}, + {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b"}, + {file = "ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9"}, + {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f"}, + {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4"}, + {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1"}, + {file = "ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f"}, + {file = "ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720"}, + {file = "ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5"}, + {file = "ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e"}, + {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043"}, + {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1"}, + {file = "ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3"}, + {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21"}, + {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2"}, + {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e"}, + {file = "ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e"}, + {file = "ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc"}, + {file = "ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287"}, + {file = "ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e"}, + {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557"}, + {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988"}, + {file = "ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816"}, + {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20"}, + {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0"}, + {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f"}, + {file = "ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165"}, + {file = "ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539"}, + {file = "ujson-5.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050"}, + {file = "ujson-5.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd"}, + {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb"}, + {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a"}, + {file = "ujson-5.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d"}, + {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe"}, + {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7"}, + {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4"}, + {file = "ujson-5.10.0-cp38-cp38-win32.whl", hash = "sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8"}, + {file = "ujson-5.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc"}, + {file = "ujson-5.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b"}, + {file = "ujson-5.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27"}, + {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76"}, + {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5"}, + {file = "ujson-5.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0"}, + {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1"}, + {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1"}, + {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996"}, + {file = "ujson-5.10.0-cp39-cp39-win32.whl", hash = "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9"}, + {file = "ujson-5.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7"}, + {file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"}, ] [[package]] @@ -1188,18 +1202,18 @@ 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)"] [extras] dev-coverage = ["coverage", "platformdirs", "pytest-cov"] diff --git a/pyshacl/cli.py b/pyshacl/cli.py index c91c943..37a75ac 100644 --- a/pyshacl/cli.py +++ b/pyshacl/cli.py @@ -7,8 +7,6 @@ from typing import Union from prettytable import PrettyTable -from rdflib import Graph -from rdflib.namespace import SH from pyshacl import __version__, validate from pyshacl.errors import ( @@ -42,8 +40,7 @@ def str_is_true(s_var: str): parser.add_argument( 'data', metavar='DataGraph', - type=argparse.FileType('rb'), - help='The file containing the Target Data Graph.', + help='The file or endpoint containing the Target Data Graph.', default=None, nargs='?', ) @@ -82,6 +79,14 @@ def str_is_true(s_var: str): default=False, help='Validate the SHACL Shapes graph against the shacl-shacl Shapes Graph before validating the Data Graph.', ) +parser.add_argument( + '-q', + '--sparql-mode', + dest='sparql_mode', + action='store_true', + default=False, + help='Treat the DataGraph as a SPARQL endpoint, validate the graph at the SPARQL endpoint.', +) parser.add_argument( '-im', '--imports', @@ -211,13 +216,33 @@ def main(prog: Union[str, None] = None) -> None: if str_is_true(do_server) or args.server: from pyshacl.sh_http import main as http_main + # http_main calls sys.exit(0) and never returns http_main() - elif not args.data: + if not args.data: # No datafile give, and not starting in server mode. - sys.stderr.write('Validation Error. No DataGraph file supplied.\n') + sys.stderr.write('Validation Error. No DataGraph file or endpoint supplied.\n') parser.print_usage(sys.stderr) sys.exit(1) validator_kwargs = {'debug': args.debug} + data_file = None + if args.sparql_mode is not None and args.sparql_mode is True: + endpoint = str(args.data).strip() + if not endpoint.lower().startswith("http:") and not endpoint.lower().startswith("https:"): + sys.stderr.write("Validation Error. SPARQL Endpoint must start with http:// or https://.\n") + sys.exit(1) + data_graph = endpoint + validator_kwargs['sparql_mode'] = True + else: + try: + data_file = open(args.data, 'rb') + except FileNotFoundError: + sys.stderr.write('Validation Error. DataGraph file not found.\n') + sys.exit(1) + except PermissionError: + sys.stderr.write('Validation Error. DataGraph file not readable.\n') + sys.exit(1) + else: + data_graph = data_file if args.shacl is not None: validator_kwargs['shacl_graph'] = args.shacl if args.ont is not None: @@ -257,37 +282,38 @@ def main(prog: Union[str, None] = None) -> None: _f = args.data_file_format if _f != "auto": validator_kwargs['data_graph_format'] = _f + exit_code: Union[int, None] = None try: - is_conform, v_graph, v_text = validate(args.data, **validator_kwargs) + is_conform, v_graph, v_text = validate(data_graph, **validator_kwargs) if isinstance(v_graph, BaseException): raise v_graph except ValidationFailure as vf: args.output.write("Validator generated a Validation Failure result:\n") args.output.write(str(vf.message)) args.output.write("\n") - sys.exit(1) + exit_code = 1 except ShapeLoadError as sle: sys.stderr.write("Validator encountered a Shape Load Error:\n") sys.stderr.write(str(sle)) - sys.exit(2) + exit_code = 2 except ConstraintLoadError as cle: sys.stderr.write("Validator encountered a Constraint Load Error:\n") sys.stderr.write(str(cle)) - sys.exit(2) + exit_code = 2 except RuleLoadError as rle: sys.stderr.write("Validator encountered a Rule Load Error:\n") sys.stderr.write(str(rle)) - sys.exit(2) + exit_code = 2 except ReportableRuntimeError as rre: sys.stderr.write("Validator encountered a Runtime Error:\n") sys.stderr.write(str(rre.message)) sys.stderr.write("\nIf you believe this is a bug in pyshacl, open an Issue on the pyshacl github page.\n") - sys.exit(2) + exit_code = 2 except NotImplementedError as nie: sys.stderr.write("Validator feature is not implemented:\n") sys.stderr.write(str(nie.args[0])) sys.stderr.write("\nIf your use-case requires this feature, open an Issue on the pyshacl github page.\n") - sys.exit(3) + exit_code = 3 except RuntimeError as re: import traceback @@ -295,8 +321,16 @@ def main(prog: Union[str, None] = None) -> None: sys.stderr.write( "\n\nValidator encountered a Runtime Error. Please report this to the PySHACL issue tracker.\n" ) - sys.exit(2) - + exit_code = 2 + finally: + if data_file is not None: + try: + data_file.close() + except Exception as e: + sys.stderr.write("Error closing data file:\n") + sys.stderr.write(str(e)) + if exit_code is not None: + sys.exit(exit_code) if args.format == 'human': args.output.write(v_text) elif args.format == 'table': @@ -317,6 +351,9 @@ def col_widther(s, w): return '\n'.join(s2) if not is_conform: + from rdflib import Graph + from rdflib.namespace import SH + t2 = PrettyTable() t2.field_names = ['No.', 'Severity', 'Focus Node', 'Result Path', 'Message', 'Component', 'Shape', 'Value'] t2.align = "l" diff --git a/pyshacl/constraints/core/other_constraints.py b/pyshacl/constraints/core/other_constraints.py index e4c5db3..d96a81d 100644 --- a/pyshacl/constraints/core/other_constraints.py +++ b/pyshacl/constraints/core/other_constraints.py @@ -174,21 +174,86 @@ def evaluate( if p: working_paths.add(p) - for f, value_nodes in focus_value_nodes.items(): - for v in value_nodes: - pred_obs = target_graph.predicate_objects(v) - for p, o in pred_obs: - if (p, o) in self.ALWAYS_IGNORE: - continue - elif p in self.ignored_props: - continue - elif p in working_paths: - continue - non_conformant = True - o_node = cast(RDFNode, o) - p_node = cast(RDFNode, p) - rept = self.make_v_result(target_graph, f, value_node=o_node, result_path=p_node) - reports.append(rept) + if executor.sparql_mode: + select_vars_string = "" + filter_props_list = [] + bgp_list = [] + init_bindings = {} + if len(self.ignored_props) > 0: + filter_template = "(" + if len(self.ignored_props) == 1: + filter_template += f"{{P}} != {next(iter(self.ignored_props)).n3()}" + else: + this_filter_parts = [] + for ig in self.ignored_props: + this_filter_parts.append(f"({{P}} != {ig.n3()})") + filter_template += " && ".join(this_filter_parts) + filter_template += ")" + else: + filter_template = "" + for i, f in enumerate(focus_value_nodes.keys()): + for j, v in enumerate(focus_value_nodes[f]): + select_vars_string += f"?p{i}_{j} ?o{i}_{j} " + bgp_line = f"OPTIONAL {{ $v{i}_{j} ?p{i}_{j} ?o{i}_{j} . }}" + bgp_list.append(bgp_line) + if filter_template: + filter_props_line = filter_template.replace("{P}", f"?p{i}_{j}") + filter_props_list.append(filter_props_line) + init_bindings[f"v{i}_{j}"] = v + bgp_string = "\n".join(bgp_list) + if len(filter_props_list) > 1: + filter_props_string = "FILTER (" + " && ".join(filter_props_list) + ")" + elif len(filter_props_list) == 1: + filter_props_string = "FILTER " + filter_props_list[0] + else: + filter_props_string = "" + closed_query = f"SELECT DISTINCT {select_vars_string} {{\n\t{bgp_string}\n\t{filter_props_string}\n}}" + try: + results = target_graph.query(closed_query, initBindings=init_bindings) + except Exception as e: + print(e) + raise + found_fvpo = [] + if len(results) > 0: + for r in results: + for i, f in enumerate(focus_value_nodes.keys()): + for j, v in enumerate(focus_value_nodes[f]): + p = r[f"p{i}_{j}"] + o = r[f"o{i}_{j}"] + if p is None or o is None or p == "UNDEF" or o == "UNDEF": + continue + fvpo = (f, v, p, o) + if fvpo in found_fvpo: + continue + found_fvpo.append(fvpo) + # TODO: remove code duplication + if (p, o) in self.ALWAYS_IGNORE: + continue + elif p in self.ignored_props: + continue + elif p in working_paths: + continue + non_conformant = True + o_node = cast(RDFNode, o) + p_node = cast(RDFNode, p) + rept = self.make_v_result(target_graph, f, value_node=o_node, result_path=p_node) + reports.append(rept) + else: + for f, value_nodes in focus_value_nodes.items(): + for v in value_nodes: + pred_obs = target_graph.predicate_objects(v) + for p, o in pred_obs: + if (p, o) in self.ALWAYS_IGNORE: + continue + elif p in self.ignored_props: + continue + elif p in working_paths: + continue + non_conformant = True + o_node = cast(RDFNode, o) + p_node = cast(RDFNode, p) + rept = self.make_v_result(target_graph, f, value_node=o_node, result_path=p_node) + reports.append(rept) return (not non_conformant), reports diff --git a/pyshacl/constraints/core/property_pair_constraints.py b/pyshacl/constraints/core/property_pair_constraints.py index b96deed..29c3e5a 100644 --- a/pyshacl/constraints/core/property_pair_constraints.py +++ b/pyshacl/constraints/core/property_pair_constraints.py @@ -9,6 +9,7 @@ from pyshacl.constraints.constraint_component import ConstraintComponent from pyshacl.consts import SH from pyshacl.errors import ConstraintLoadError, ReportableRuntimeError +from pyshacl.helper.path_helper import shacl_path_to_sparql_path from pyshacl.pytypes import GraphLike, SHACLExecutor from pyshacl.rdfutil import stringify_node @@ -79,12 +80,57 @@ def evaluate( non_conformant = False for eq in iter(self.property_compare_set): - _nc, _r = self._evaluate_property_equals(eq, target_graph, focus_value_nodes) + if executor.sparql_mode: + _nc, _r = self._evaluate_property_equals_sparql(eq, target_graph, focus_value_nodes) + else: + _nc, _r = self._evaluate_property_equals_rdflib(eq, target_graph, focus_value_nodes) non_conformant = non_conformant or _nc reports.extend(_r) return (not non_conformant), reports - def _evaluate_property_equals(self, eq, target_graph, f_v_dict): + def _evaluate_property_equals_sparql(self, eq, target_graph, f_v_dict): + reports = [] + non_conformant = False + prefixes = dict(target_graph.namespaces()) + eq_path = shacl_path_to_sparql_path(self.shape.sg, eq, prefixes=prefixes) + eq_lookup_query = f"SELECT DISTINCT {' '.join(f'?v{i}' for i,_ in enumerate(f_v_dict))} WHERE {{\n" + init_bindings = {} + f_eq_results = {} + for i, f in enumerate(f_v_dict.keys()): + eq_lookup_query += f"OPTIONAL {{ $f{i} {eq_path} ?v{i} . }}\n" + init_bindings[f"f{i}"] = f + f_eq_results[f] = set() + eq_lookup_query += "}" + try: + results = target_graph.query(eq_lookup_query, initBindings=init_bindings) + except Exception as e: + print(e) + raise + if len(results) > 0: + for r in results: + for i, f in enumerate(f_v_dict.keys()): + val_i = r[i] + if val_i is None or val_i == "UNDEF": + continue + f_eq_results[f].add(val_i) + for i, f in enumerate(f_v_dict.keys()): + value_node_set = set(f_v_dict[f]) + compare_values = f_eq_results[f] + value_nodes_missing = value_node_set.difference(compare_values) + compare_values_missing = compare_values.difference(value_node_set) + if len(value_nodes_missing) > 0 or len(compare_values_missing) > 0: + non_conformant = True + else: + continue + for value_node in value_nodes_missing: + rept = self.make_v_result(target_graph, f, value_node=value_node) + reports.append(rept) + for compare_value in compare_values_missing: + rept = self.make_v_result(target_graph, f, value_node=compare_value) + reports.append(rept) + return non_conformant, reports + + def _evaluate_property_equals_rdflib(self, eq, target_graph, f_v_dict): reports = [] non_conformant = False for f, value_nodes in f_v_dict.items(): @@ -161,12 +207,53 @@ def evaluate( non_conformant = False for dj in iter(self.property_compare_set): - _nc, _r = self._evaluate_property_disjoint(dj, target_graph, focus_value_nodes) + if executor.sparql_mode: + _nc, _r = self._evaluate_property_disjoint_sparql(dj, target_graph, focus_value_nodes) + else: + _nc, _r = self._evaluate_property_disjoint_rdflib(dj, target_graph, focus_value_nodes) non_conformant = non_conformant or _nc reports.extend(_r) return (not non_conformant), reports - def _evaluate_property_disjoint(self, dj, target_graph, f_v_dict): + def _evaluate_property_disjoint_sparql(self, dj, target_graph, f_v_dict): + reports = [] + non_conformant = False + prefixes = dict(target_graph.namespaces()) + dj_path = shacl_path_to_sparql_path(self.shape.sg, dj, prefixes=prefixes) + dj_lookup_query = f"SELECT DISTINCT {' '.join(f'?v{i}' for i,_ in enumerate(f_v_dict))} WHERE {{\n" + init_bindings = {} + f_dj_results = {} + for i, f in enumerate(f_v_dict.keys()): + dj_lookup_query += f"OPTIONAL {{ $f{i} {dj_path} ?v{i} . }}\n" + init_bindings[f"f{i}"] = f + f_dj_results[f] = set() + dj_lookup_query += "}" + try: + results = target_graph.query(dj_lookup_query, initBindings=init_bindings) + except Exception as e: + print(e) + raise + if len(results) > 0: + for r in results: + for i, f in enumerate(f_v_dict.keys()): + val_i = r[i] + if val_i is None or val_i == "UNDEF": + continue + f_dj_results[f].add(val_i) + for i, f in enumerate(f_v_dict.keys()): + value_node_set = set(f_v_dict[f]) + compare_values = f_dj_results[f] + common_nodes = value_node_set.intersection(compare_values) + if len(common_nodes) > 0: + non_conformant = True + else: + continue + for common_node in common_nodes: + rept = self.make_v_result(target_graph, f, value_node=common_node) + reports.append(rept) + return non_conformant, reports + + def _evaluate_property_disjoint_rdflib(self, dj, target_graph, f_v_dict): reports = [] non_conformant = False for f, value_nodes in f_v_dict.items(): @@ -247,48 +334,91 @@ def evaluate( for lt in iter(self.property_compare_set): if isinstance(lt, rdflib.Literal) or isinstance(lt, rdflib.BNode): raise ReportableRuntimeError("Value of sh:lessThan MUST be a URI Identifier.") - _nc, _r = self._evaluate_less_than(lt, target_graph, focus_value_nodes) + if executor.sparql_mode: + _nc, _r = self._evaluate_less_than_sparql(lt, target_graph, focus_value_nodes) + else: + _nc, _r = self._evaluate_less_than_rdflib(lt, target_graph, focus_value_nodes) non_conformant = non_conformant or _nc reports.extend(_r) return (not non_conformant), reports - def _evaluate_less_than(self, lt, target_graph, f_v_dict): + def _compare_lt(self, value_node_set, compare_values, datagraph, f): + non_conformant = False + reports = [] + for value_node in iter(value_node_set): + if isinstance(value_node, rdflib.BNode): + raise ReportableRuntimeError("Cannot use sh:lessThan to compare a BlankNode.") + value_is_string = False + orig_value_node = value_node + if isinstance(value_node, rdflib.URIRef): + value_node = str(value_node) + value_is_string = True + elif isinstance(value_node, rdflib.Literal) and isinstance(value_node.value, str): + value_node = value_node.value + value_is_string = True + + for compare_value in compare_values: + if isinstance(compare_value, rdflib.BNode): + raise ReportableRuntimeError("Cannot use sh:lessThan to compare a BlankNode.") + compare_is_string = False + if isinstance(compare_value, rdflib.URIRef): + compare_value = str(compare_value) + compare_is_string = True + elif isinstance(compare_value, rdflib.Literal) and isinstance(compare_value.value, str): + compare_value = compare_value.value + compare_is_string = True + if (value_is_string and not compare_is_string) or (compare_is_string and not value_is_string): + non_conformant = True + elif not value_node < compare_value: + non_conformant = True + else: + continue + rept = self.make_v_result(datagraph, f, value_node=orig_value_node) + reports.append(rept) + return non_conformant, reports + + def _evaluate_less_than_sparql(self, lt, target_graph, f_v_dict): + reports = [] + non_conformant = False + prefixes = dict(target_graph.namespaces()) + lt_path = shacl_path_to_sparql_path(self.shape.sg, lt, prefixes=prefixes) + lt_lookup_query = f"SELECT DISTINCT {' '.join(f'?v{i}' for i,_ in enumerate(f_v_dict))} WHERE {{\n" + init_bindings = {} + f_lt_results = {} + for i, f in enumerate(f_v_dict.keys()): + lt_lookup_query += f"OPTIONAL {{ $f{i} {lt_path} ?v{i} . }}\n" + init_bindings[f"f{i}"] = f + f_lt_results[f] = set() + lt_lookup_query += "}" + try: + results = target_graph.query(lt_lookup_query, initBindings=init_bindings) + except Exception as e: + print(e) + raise + if len(results) > 0: + for r in results: + for i, f in enumerate(f_v_dict.keys()): + val_i = r[i] + if val_i is None or val_i == "UNDEF": + continue + f_lt_results[f].add(val_i) + for i, f in enumerate(f_v_dict.keys()): + value_node_set = set(f_v_dict[f]) + compare_values = f_lt_results[f] + _nc, _r = self._compare_lt(value_node_set, compare_values, target_graph, f) + non_conformant = non_conformant or _nc + reports.extend(_r) + return non_conformant, reports + + def _evaluate_less_than_rdflib(self, lt, target_graph, f_v_dict): reports = [] non_conformant = False for f, value_nodes in f_v_dict.items(): value_node_set = set(value_nodes) compare_values = set(target_graph.objects(f, lt)) - - for value_node in iter(value_node_set): - if isinstance(value_node, rdflib.BNode): - raise ReportableRuntimeError("Cannot use sh:lessThan to compare a BlankNode.") - value_is_string = False - orig_value_node = value_node - if isinstance(value_node, rdflib.URIRef): - value_node = str(value_node) - value_is_string = True - elif isinstance(value_node, rdflib.Literal) and isinstance(value_node.value, str): - value_node = value_node.value - value_is_string = True - - for compare_value in compare_values: - if isinstance(compare_value, rdflib.BNode): - raise ReportableRuntimeError("Cannot use sh:lessThan to compare a BlankNode.") - compare_is_string = False - if isinstance(compare_value, rdflib.URIRef): - compare_value = str(compare_value) - compare_is_string = True - elif isinstance(compare_value, rdflib.Literal) and isinstance(compare_value.value, str): - compare_value = compare_value.value - compare_is_string = True - if (value_is_string and not compare_is_string) or (compare_is_string and not value_is_string): - non_conformant = True - elif not value_node < compare_value: - non_conformant = True - else: - continue - rept = self.make_v_result(target_graph, f, value_node=orig_value_node) - reports.append(rept) + _nc, _r = self._compare_lt(value_node_set, compare_values, target_graph, f) + non_conformant = non_conformant or _nc + reports.extend(_r) return non_conformant, reports @@ -355,46 +485,89 @@ def evaluate( for lt in iter(self.property_compare_set): if isinstance(lt, rdflib.Literal) or isinstance(lt, rdflib.BNode): raise ReportableRuntimeError("Value of sh:lessThanOrEquals MUST be a URI Identifier.") - _nc, _r = self._evaluate_ltoe(lt, target_graph, focus_value_nodes) + if executor.sparql_mode: + _nc, _r = self._evaluate_ltoe_sparql(lt, target_graph, focus_value_nodes) + else: + _nc, _r = self._evaluate_ltoe_rdflib(lt, target_graph, focus_value_nodes) non_conformant = non_conformant or _nc reports.extend(_r) return (not non_conformant), reports - def _evaluate_ltoe(self, lt, target_graph, f_v_dict): + def _compare_ltoe(self, value_node_set, compare_values, datagraph, f): + non_conformant = False + reports = [] + for value_node in iter(value_node_set): + if isinstance(value_node, rdflib.BNode): + raise ReportableRuntimeError("Cannot use sh:lessThanOrEquals to compare a BlankNode.") + value_is_string = False + orig_value_node = value_node + if isinstance(value_node, rdflib.URIRef): + value_node = str(value_node) + value_is_string = True + elif isinstance(value_node, rdflib.Literal) and isinstance(value_node.value, str): + value_node = value_node.value + value_is_string = True + + for compare_value in compare_values: + if isinstance(compare_value, rdflib.BNode): + raise ReportableRuntimeError("Cannot use sh:lessThanOrEquals to compare a BlankNode.") + compare_is_string = False + if isinstance(compare_value, rdflib.URIRef): + compare_value = str(compare_value) + compare_is_string = True + elif isinstance(compare_value, rdflib.Literal) and isinstance(compare_value.value, str): + compare_value = compare_value.value + compare_is_string = True + if (value_is_string and not compare_is_string) or (compare_is_string and not value_is_string): + non_conformant = True + elif not value_node <= compare_value: + non_conformant = True + else: + continue + rept = self.make_v_result(datagraph, f, value_node=orig_value_node) + reports.append(rept) + return non_conformant, reports + + def _evaluate_ltoe_sparql(self, ltoe, target_graph, f_v_dict): + reports = [] + non_conformant = False + prefixes = dict(target_graph.namespaces()) + ltoe_path = shacl_path_to_sparql_path(self.shape.sg, ltoe, prefixes=prefixes) + ltoe_lookup_query = f"SELECT DISTINCT {' '.join(f'?v{i}' for i,_ in enumerate(f_v_dict))} WHERE {{\n" + init_bindings = {} + f_ltoe_results = {} + for i, f in enumerate(f_v_dict.keys()): + ltoe_lookup_query += f"OPTIONAL {{ $f{i} {ltoe_path} ?v{i} . }}\n" + init_bindings[f"f{i}"] = f + f_ltoe_results[f] = set() + ltoe_lookup_query += "}" + try: + results = target_graph.query(ltoe_lookup_query, initBindings=init_bindings) + except Exception as e: + print(e) + raise + if len(results) > 0: + for r in results: + for i, f in enumerate(f_v_dict.keys()): + val_i = r[i] + if val_i is None or val_i == "UNDEF": + continue + f_ltoe_results[f].add(val_i) + for i, f in enumerate(f_v_dict.keys()): + value_node_set = set(f_v_dict[f]) + compare_values = f_ltoe_results[f] + _nc, _r = self._compare_ltoe(value_node_set, compare_values, target_graph, f) + non_conformant = non_conformant or _nc + reports.extend(_r) + return non_conformant, reports + + def _evaluate_ltoe_rdflib(self, ltoe, target_graph, f_v_dict): reports = [] non_conformant = False for f, value_nodes in f_v_dict.items(): value_node_set = set(value_nodes) - compare_values = set(target_graph.objects(f, lt)) - - for value_node in iter(value_node_set): - if isinstance(value_node, rdflib.BNode): - raise ReportableRuntimeError("Cannot use sh:lessThanOrEquals to compare a BlankNode.") - value_is_string = False - orig_value_node = value_node - if isinstance(value_node, rdflib.URIRef): - value_node = str(value_node) - value_is_string = True - elif isinstance(value_node, rdflib.Literal) and isinstance(value_node.value, str): - value_node = value_node.value - value_is_string = True - - for compare_value in compare_values: - if isinstance(compare_value, rdflib.BNode): - raise ReportableRuntimeError("Cannot use sh:lessThanOrEquals to compare a BlankNode.") - compare_is_string = False - if isinstance(compare_value, rdflib.URIRef): - compare_value = str(compare_value) - compare_is_string = True - elif isinstance(compare_value, rdflib.Literal) and isinstance(compare_value.value, str): - compare_value = compare_value.value - compare_is_string = True - if (value_is_string and not compare_is_string) or (compare_is_string and not value_is_string): - non_conformant = True - elif not value_node <= compare_value: - non_conformant = True - else: - continue - rept = self.make_v_result(target_graph, f, value_node=orig_value_node) - reports.append(rept) + compare_values = set(target_graph.objects(f, ltoe)) + _nc, _r = self._compare_ltoe(value_node_set, compare_values, target_graph, f) + non_conformant = non_conformant or _nc + reports.extend(_r) return non_conformant, reports diff --git a/pyshacl/constraints/core/value_constraints.py b/pyshacl/constraints/core/value_constraints.py index 28ae7e9..ce83eb7 100644 --- a/pyshacl/constraints/core/value_constraints.py +++ b/pyshacl/constraints/core/value_constraints.py @@ -96,13 +96,40 @@ def evaluate( """ reports = [] non_conformant = False - for c in self.class_rules: - _n, _r = self._evaluate_class_rules(target_graph, focus_value_nodes, c) - non_conformant = non_conformant or _n - reports.extend(_r) + if executor.sparql_mode: + for c in self.class_rules: + _n, _r = self._evaluate_class_rules_sparql(target_graph, focus_value_nodes, c) + non_conformant = non_conformant or _n + reports.extend(_r) + else: + for c in self.class_rules: + _n, _r = self._evaluate_class_rules_rdflib(target_graph, focus_value_nodes, c) + non_conformant = non_conformant or _n + reports.extend(_r) return (not non_conformant), reports - def _evaluate_class_rules(self, target_graph, f_v_dict, class_rule): + def _evaluate_class_rules_sparql(self, target_graph, f_v_dict, class_rule): + reports = [] + non_conformant = False + sparql_ask = """ASK {$value rdf:type/rdfs:subClassOf* $class .}""" + for f, value_nodes in f_v_dict.items(): + for v in value_nodes: + found = False + if isinstance(v, Literal): + self.shape.logger.debug( + "Class Constraint won't work with Literals. " + "Attempting to match Literal node {} to class of {} will fail.".format(v, class_rule) + ) + else: + resp = target_graph.query(sparql_ask, initBindings={"value": v, "class": class_rule}) + found = resp.askAnswer + if not found: + non_conformant = True + rept = self.make_v_result(target_graph, f, value_node=v) + reports.append(rept) + return non_conformant, reports + + def _evaluate_class_rules_rdflib(self, target_graph, f_v_dict, class_rule): reports = [] non_conformant = False for f, value_nodes in f_v_dict.items(): @@ -305,9 +332,3 @@ def evaluate( rept = self.make_v_result(target_graph, f, value_node=v) reports.append(rept) return (not non_conformant), reports - - def _evaluate_nodekind_rules(self, target_graph, f_v_pairs, nodekind_rule): - reports = [] - non_conformant = False - - return non_conformant, reports diff --git a/pyshacl/pytypes.py b/pyshacl/pytypes.py index c746e55..3b28ccf 100644 --- a/pyshacl/pytypes.py +++ b/pyshacl/pytypes.py @@ -15,9 +15,11 @@ @dataclass class SHACLExecutor: validator: Optional[object] = None + advanced_mode: bool = False abort_on_first: bool = False allow_infos: bool = False allow_warnings: bool = False iterate_rules: bool = False debug: bool = False + sparql_mode: bool = False max_validation_depth: int = 15 diff --git a/pyshacl/shape.py b/pyshacl/shape.py index edbd9ba..c520b4c 100644 --- a/pyshacl/shape.py +++ b/pyshacl/shape.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # +import itertools import logging import sys from decimal import Decimal @@ -38,6 +39,7 @@ from .errors import ConstraintLoadError, ConstraintLoadWarning, ReportableRuntimeError, ShapeLoadError from .helper import get_query_helper_cls from .helper.expression_helper import value_nodes_from_path +from .helper.path_helper import shacl_path_to_sparql_path from .pytypes import GraphLike, SHACLExecutor if TYPE_CHECKING: @@ -386,23 +388,217 @@ def focus_nodes(self, data_graph, debug=False): self.logger.debug(f"Milliseconds to find focus nodes: {elapsed*1000.0:.3f}ms") return found_node_targets - def value_nodes(self, target_graph, focus): + @classmethod + def make_focus_nodes_sparql_values( + cls, target_classes_s: Set, implicit_classes_s: Set, target_objects_of_s: Set, target_subjects_of_s: Set + ): + init_bindings = {} + values_keys = [] + values_vals = [] + if len(target_classes_s) > 1: + values_keys.append("$targetClass") + values_vals.append(list(target_classes_s)) + else: + init_bindings["targetClass"] = next(iter(target_classes_s)) if len(target_classes_s) > 0 else "UNDEF" + if len(implicit_classes_s) > 1: + values_keys.append("$implicitClass") + values_vals.append(list(implicit_classes_s)) + else: + init_bindings["implicitClass"] = next(iter(implicit_classes_s)) if len(implicit_classes_s) > 0 else "UNDEF" + if len(target_subjects_of_s) > 1: + values_keys.append("$targetSubjectsOf") + values_vals.append(list(target_subjects_of_s)) + else: + init_bindings["targetSubjectsOf"] = ( + next(iter(target_subjects_of_s)) if len(target_subjects_of_s) > 0 else "UNDEF" + ) + if len(target_objects_of_s) > 1: + values_keys.append("$targetObjectsOf") + values_vals.append(list(target_objects_of_s)) + else: + init_bindings["targetObjectsOf"] = ( + next(iter(target_objects_of_s)) if len(target_objects_of_s) > 0 else "UNDEF" + ) + if len(values_keys) < 1: + return "", init_bindings + else: + values_clause = f"VALUES ({' '.join(values_keys)}) {{\n" + product = itertools.product(*values_vals) + for p in product: + values_clause += f"\t( {' '.join(p_x.n3() for p_x in p)} )\n" + values_clause += "}" + return values_clause, init_bindings + + def focus_nodes_sparql(self, data_graph, debug=False): + """ + The set of focus nodes for a shape may be identified as follows: + + specified in a shape using target declarations + specified in any constraint that references a shape in parameters of shape-expecting constraint parameters (e.g. sh:node) + specified as explicit input to the SHACL processor for validating a specific RDF term against a shape + :return: + """ + t1 = 0.0 + if debug: + t1 = perf_counter() + (target_nodes, target_classes, implicit_classes, target_objects_of, target_subjects_of) = self.target() + if self._advanced: + advanced_targets = self.advanced_target() + else: + advanced_targets = False + found_node_targets = set() + target_nodes = set(target_nodes) + target_classes = set(target_classes) + implicit_classes = set(implicit_classes) + target_objects_of = set(target_objects_of) + target_subjects_of = set(target_subjects_of) + if all( + ( + advanced_targets is False, + len(target_nodes) < 1, + len(target_classes) < 1, + len(implicit_classes) < 1, + len(target_objects_of) < 1, + len(target_subjects_of) < 1, + ) + ): + return found_node_targets + + found_node_targets.update(target_nodes) + if ( + advanced_targets is False + and len(target_classes) < 1 + and len(implicit_classes) < 1 + and len(target_objects_of) < 1 + and len(target_subjects_of) < 1 + ): + return found_node_targets + if ( + len(target_classes) > 0 + or len(implicit_classes) > 0 + or len(target_objects_of) > 0 + or len(target_subjects_of) > 0 + ): + focus_query = """\ + SELECT ?targetClass_F ?targetSubjectsOf_F ?targetObjectsOf_F WHERE { + {VALUES_CLAUSE} + OPTIONAL { { + ?targetClass_F rdf:type/rdfs:subClassOf* $targetClass . + } UNION { + ?targetClass_F rdf:type/rdfs:subClassOf* $implicitClass . + }. } + OPTIONAL { ?targetSubjectsOf_F $targetSubjectsOf ?anyA . } + OPTIONAL {?anyB $targetObjectsOf ?targetObjectsOf_F . } + } + """ + values_clause, init_bindings = self.make_focus_nodes_sparql_values( + target_classes, implicit_classes, target_objects_of, target_subjects_of + ) + new_query = focus_query.replace("{VALUES_CLAUSE}", values_clause) + try: + resp = data_graph.query(new_query, initBindings=init_bindings) + except Exception as e: + print(new_query) + raise e + if len(resp) > 0: + for result_set in resp: + target_class_f, target_subjects_of_f, target_objects_of_f = result_set + if target_class_f is not None and target_class_f != "UNDEF": + found_node_targets.add(target_class_f) + if target_subjects_of_f is not None and target_subjects_of_f != "UNDEF": + found_node_targets.add(target_subjects_of_f) + if target_objects_of_f is not None and target_objects_of_f != "UNDEF": + found_node_targets.add(target_objects_of_f) + if advanced_targets: + for at_node, at in advanced_targets.items(): + if at['type'] == SH_SPARQLTarget: + qh = at['qh'] + select = qh.apply_prefixes(qh.select_text) + results = data_graph.query(select, initBindings=None) + if not results or len(results.bindings) < 1: + continue + for r in results: + t = r['this'] + found_node_targets.add(t) + elif at['type'] in (SH_JSTarget, SH_JSTargetType): + raise ReportableRuntimeError( + "SHACL Advanced Targets with JSTargets are not yet implemented in SPARQL Remote Graph Mode." + ) + else: + results = at['qt'].find_targets(data_graph) + if not results or len(results.bindings) < 1: + continue + for r in results: + t = r['this'] + found_node_targets.add(t) + if debug: + t2 = perf_counter() + elapsed = t2 - t1 + self.logger.debug(f"Milliseconds to find focus nodes: {elapsed*1000.0:.3f}ms") + return found_node_targets + + def value_nodes(self, target_graph, focus, sparql_mode: bool = False, debug: bool = False): """ For each focus node, you can get a set of value nodes. For a Node Shape, each focus node has just one value node, which is just the focus_node :param target_graph: :param focus: + :param sparql_mode: + :type sparql_mode: bool + :param debug: + :type debug: bool :return: """ + t1 = 0.0 + if debug: + t1 = perf_counter() if not isinstance(focus, (tuple, list, set)): focus = [focus] if not self.is_property_shape: + if debug: + t2 = perf_counter() + elapsed = t2 - t1 + self.logger.debug(f"Milliseconds to find value nodes for focus nodes: {elapsed * 1000.0:.3f}ms") return {f: set((f,)) for f in focus} path_val = self.path() + focus_dict = {} - for f in focus: - focus_dict[f] = value_nodes_from_path(self.sg, f, path_val, target_graph) + if sparql_mode: + # Shortcut for simple URI path, path rewriting and everything else + if isinstance(path_val, URIRef): + sparql_path = path_val.n3(namespace_manager=target_graph.namespace_manager) + else: + prefixes = dict(target_graph.namespace_manager.namespaces()) + sparql_path = shacl_path_to_sparql_path(self.sg, path_val, prefixes=prefixes) + values_query = f"SELECT {' '.join(f'?v{i}' for i,_ in enumerate(focus))} WHERE {{\n" + init_bindings = {} + for i, f in enumerate(focus): + focus_dict[f] = set() + values_query += f"OPTIONAL {{ \t$f{i} {sparql_path} ?v{i} . }}\n" + init_bindings[f"f{i}"] = f + values_query += "}" + try: + results = target_graph.query(values_query, initBindings=init_bindings) + except Exception as e: + print(e) + raise + if len(results) > 0: + for r in results: + for i, f in enumerate(focus): + row_focus_result = r[i] + if row_focus_result is None or row_focus_result == "UNDEF": + continue + focus_dict[f].add(row_focus_result) + else: + pass + else: + for f in focus: + focus_dict[f] = value_nodes_from_path(self.sg, f, path_val, target_graph) + if debug: + t2 = perf_counter() + elapsed = t2 - t1 + self.logger.debug(f"Milliseconds to find value nodes for focus nodes: {elapsed*1000.0:.3f}ms") return focus_dict def find_custom_constraints(self): @@ -452,7 +648,10 @@ def validate( rh_shape = False self.logger.debug(f"Checking if Shape {str(self)} defines its own targets.") self.logger.debug("Identifying targets to find focus nodes.") - focus = self.focus_nodes(target_graph, debug=executor.debug) + if executor.sparql_mode: + focus = self.focus_nodes_sparql(target_graph, debug=executor.debug) + else: + focus = self.focus_nodes(target_graph, debug=executor.debug) self.logger.debug(f"Found {len(focus)} Focus Nodes to evaluate.") if len(focus) < 1: # It's possible for shapes to have _no_ focus nodes @@ -503,7 +702,9 @@ def validate( constraint_map = PARAMETER_MAP parameters = (p for p, v in self.sg.predicate_objects(self.node) if p in search_parameters) reports = [] - focus_value_nodes = self.value_nodes(target_graph, focus) + focus_value_nodes = self.value_nodes( + target_graph, focus, sparql_mode=executor.sparql_mode, debug=executor.debug + ) filter_reports: bool = False allow_conform: bool = False allowed_severities: Set[URIRef] = set() diff --git a/pyshacl/validate.py b/pyshacl/validate.py index 59f0b55..67d9555 100644 --- a/pyshacl/validate.py +++ b/pyshacl/validate.py @@ -1,27 +1,21 @@ # -*- coding: utf-8 -*- # import logging +import os import sys from functools import wraps from io import BufferedIOBase, TextIOBase from os import getenv, path from sys import stderr -from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Set, Tuple, Union, cast +from typing import Dict, List, Optional, Tuple, Union import rdflib from rdflib import BNode, Literal, URIRef -from rdflib.util import from_n3 from .consts import ( - RDF_object, - RDF_predicate, - RDF_subject, RDF_type, - RDFS_Resource, SH_conforms, - SH_detail, SH_result, - SH_resultMessage, SH_ValidationReport, env_truths, ) @@ -34,14 +28,11 @@ add_baked_in, clone_blank_node, clone_graph, - compare_blank_node, - compare_node, inoculate, inoculate_dataset, load_from_source, mix_datasets, mix_graphs, - order_graph_literal, ) from .rules import apply_rules, gather_rules from .shapes_graph import ShapesGraph @@ -71,6 +62,7 @@ def _load_default_options(cls, options_dict: dict): options_dict.setdefault('abort_on_first', False) options_dict.setdefault('allow_infos', False) options_dict.setdefault('allow_warnings', False) + options_dict.setdefault('sparql_mode', False) options_dict.setdefault('max_validation_depth', 15) if 'logger' not in options_dict: options_dict['logger'] = logging.getLogger(__name__) @@ -193,13 +185,20 @@ def __init__( self.data_graph_is_multigraph = isinstance(self.data_graph, (rdflib.Dataset, rdflib.ConjunctiveGraph)) if self.ont_graph is not None and isinstance(self.ont_graph, (rdflib.Dataset, rdflib.ConjunctiveGraph)): self.ont_graph.default_union = True - + if self.ont_graph is not None and options['sparql_mode']: + raise ReportableRuntimeError("Cannot use SPARQL Remote Graph Mode with extra Ontology Graph inoculation.") if shacl_graph is None: + if options['sparql_mode']: + raise ReportableRuntimeError( + "SHACL Shapes Graph must be a separate local graph or file when in SPARQL Remote Graph Mode." + ) shacl_graph = clone_graph(data_graph, identifier='shacl') assert isinstance(shacl_graph, rdflib.Graph), "shacl_graph must be a rdflib Graph object" self.shacl_graph = ShapesGraph(shacl_graph, self.debug, self.logger) # type: ShapesGraph if options['use_js']: + if options['sparql_mode']: + raise ReportableRuntimeError("Cannot use SHACL-JS in SPARQL Remote Graph Mode.") is_js_installed = check_extra_installed('js') if is_js_installed: self.shacl_graph.enable_js() @@ -224,10 +223,12 @@ def mix_in_ontology(self): def make_executor(self) -> SHACLExecutor: return SHACLExecutor( validator=self, + advanced_mode=bool(self.options.get('advanced', False)), abort_on_first=bool(self.options.get("abort_on_first", False)), allow_infos=bool(self.options.get("allow_infos", False)), allow_warnings=bool(self.options.get("allow_warnings", False)), iterate_rules=bool(self.options.get("iterate_rules", False)), + sparql_mode=bool(self.options.get("sparql_mode", False)), max_validation_depth=self.options.get("max_validation_depth", 15), debug=self.debug, ) @@ -249,8 +250,10 @@ def run(self): the_target_graph = self.data_graph inference_option = self.options.get('inference', 'none') if self.inplace and self.debug: - self.logger.debug("Skipping DataGraph clone because inplace option is passed.") + self.logger.debug("Skipping DataGraph clone because PySHACL is operating in inplace mode.") if inference_option and not self.pre_inferenced and str(inference_option) != "none": + if self.options.get('sparql_mode', False): + raise ReportableRuntimeError("Cannot use any pre-inference option in SPARQL Remote Graph Mode.") if not has_cloned and not self.inplace: self.logger.debug("Cloning DataGraph to temporary memory graph before pre-inferencing.") the_target_graph = clone_graph(the_target_graph) @@ -259,6 +262,8 @@ def run(self): self._run_pre_inference(the_target_graph, inference_option, logger=self.logger) self.pre_inferenced = True if not has_cloned and not self.inplace and self.options['advanced']: + if self.options.get('sparql_mode', False): + raise ReportableRuntimeError("Cannot clone DataGraph in SPARQL Remote Graph Mode.") # We still need to clone in advanced mode, because of triple rules self.logger.debug("Forcing clone of DataGraph because advanced mode is enabled.") the_target_graph = clone_graph(the_target_graph) @@ -271,7 +276,7 @@ def run(self): shapes = self.shacl_graph.shapes # This property getter triggers shapes harvest. executor = self.make_executor() - if self.options['advanced']: + if executor.advanced_mode: self.logger.debug("Activating SHACL-AF Features.") target_types = gather_target_types(self.shacl_graph) advanced = { @@ -308,8 +313,13 @@ def run(self): if self.debug: self.logger.debug(f"Validating DataGraph named {g.identifier}") if advanced: - apply_functions(executor, advanced['functions'], g) - apply_rules(executor, advanced['rules'], g) + if advanced['functions']: + apply_functions(executor, advanced['functions'], g) + if advanced['rules']: + if executor.sparql_mode: + self.logger.warning("Skipping SHACL Rules because operating in SPARQL Remote Graph Mode.") + else: + apply_rules(executor, advanced['rules'], g) try: for s in shapes: _is_conform, _reports = s.validate(executor, g) @@ -395,6 +405,7 @@ def validate( allow_infos: Optional[bool] = False, allow_warnings: Optional[bool] = False, max_validation_depth: Optional[int] = None, + sparql_mode: Optional[bool] = False, **kwargs, ): """ @@ -411,7 +422,7 @@ def validate( :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 + :param inplace: If this is enabled, do not clone the datagraph, manipulate it in-place :type inplace: bool :param abort_on_first: Stop evaluating constraints after first violation is found :type abort_on_first: bool | None @@ -421,6 +432,8 @@ def validate( :type allow_warnings: bool | None :param max_validation_depth: The maximum number of SHACL shapes "deep" that the validator can go before reaching an "endpoint" constraint. :type max_validation_depth: int | None + :param sparql_mode: Treat the DataGraph as a SPARQL endpoint, validate the graph at the SPARQL endpoint. + :type sparql_mode: bool | None :param kwargs: :return: """ @@ -446,29 +459,45 @@ def validate( # DataGraph is passed in as Text. It is not an rdflib.Graph # That means we load it into an ephemeral graph at runtime # that means we don't need to make a copy to prevent polluting it. - if isinstance(data_graph, str) and ( - data_graph.startswith("http:/") - or data_graph.startswith("https:/") - or data_graph.startswith("file:/") - or data_graph.startswith("urn:") - ): - ephemeral = False - elif isinstance(data_graph, bytes) and ( - data_graph.startswith(b"http:/") - or data_graph.startswith(b"https:/") - or data_graph.startswith(b"file:/") - or data_graph.startswith(b"urn:") - ): - ephemeral = False - else: - ephemeral = True + ephemeral = True else: ephemeral = False - - # force no owl imports on data_graph - loaded_dg = load_from_source( - data_graph, rdf_format=data_graph_format, multigraph=True, do_owl_imports=False, logger=log - ) + use_js = kwargs.pop('js', None) + if sparql_mode: + if use_js: + raise ReportableRuntimeError("Cannot use SHACL-JS in SPARQL Remote Graph Mode.") + if inplace: + raise ReportableRuntimeError("Cannot use inplace mode in SPARQL Remote Graph Mode.") + if ont_graph is not None: + raise ReportableRuntimeError("Cannot use SPARQL Remote Graph Mode with extra Ontology Graph inoculation.") + if isinstance(data_graph, bytes): + data_graph: str = data_graph.decode('utf-8') + else: + data_graph = data_graph + ephemeral = False + inplace = True + if ( + sparql_mode + and isinstance(data_graph, str) + and (data_graph.lower().startswith("http:") or data_graph.lower().startswith("https:")) + ): + from rdflib.plugins.stores.sparqlstore import SPARQLStore + + query_endpoint: str = data_graph + username = os.getenv("PYSHACL_SPARQL_USERNAME", "") + method = os.getenv("PYSHACL_SPARQL_METHOD", "GET") + if username: + password = os.getenv("PYSHACL_SPARQL_PASSWORD", "") + auth = (username, None if not password else password) + else: + auth = None + store = SPARQLStore(query_endpoint=query_endpoint, auth=auth, method=method) + loaded_dg = rdflib.Dataset(store=store, default_union=True) + else: + # force no owl imports on data_graph + loaded_dg = load_from_source( + data_graph, rdf_format=data_graph_format, multigraph=True, do_owl_imports=False, logger=log + ) ont_graph_format = kwargs.pop('ont_graph_format', None) if ont_graph is not None: loaded_og = load_from_source( @@ -485,7 +514,6 @@ def validate( rdflib_bool_unpatch() else: loaded_sg = None - use_js = kwargs.pop('js', None) iterate_rules = kwargs.pop('iterate_rules', False) if "abort_on_error" in kwargs: log.warning("Usage of abort_on_error is deprecated. Use abort_on_first instead.") @@ -501,6 +529,7 @@ def validate( 'advanced': advanced, 'iterate_rules': iterate_rules, 'use_js': use_js, + 'sparql_mode': sparql_mode, 'logger': log, } if max_validation_depth is not None: diff --git a/test/test_dash_validate.py b/test/test_dash_validate.py index 8a6764d..fda25bd 100644 --- a/test/test_dash_validate.py +++ b/test/test_dash_validate.py @@ -28,6 +28,7 @@ for y in glob.glob(path.join(x[0], '*.test.ttl')): dash_core_files.append((y, None)) + @pytest.mark.parametrize('target_file, shacl_file', dash_core_files) def test_dash_validate_all_core(target_file, shacl_file): try: @@ -41,6 +42,21 @@ def test_dash_validate_all_core(target_file, shacl_file): print(v_text) +@pytest.mark.parametrize('target_file, shacl_file', dash_core_files) +def test_dash_validate_all_core_sparql_mode(target_file, shacl_file): + try: + if shacl_file is None: + # shacl_file cannot be None in SPARQL Remote Graph Mode + shacl_file = target_file + val, _, v_text = pyshacl.validate( + target_file, shacl_graph=shacl_file, inference='none', check_dash_result=True, debug=True, sparql_mode=True, meta_shacl=False) + except (NotImplementedError, ReportableRuntimeError) as e: + print(e) + val = False + v_text = "" + assert val + print(v_text) + for x in walk(path.join(dash_files_dir, 'sparql')): for y in glob.glob(path.join(x[0], '*.test.ttl')): @@ -58,6 +74,20 @@ def test_dash_validate_all_sparql(target_file, shacl_file): assert val print(v_text) +@pytest.mark.parametrize('target_file, shacl_file', dash_sparql_files) +def test_dash_validate_all_sparql_sparql_mode(target_file, shacl_file): + try: + if shacl_file is None: + # shacl_file cannot be None in SPARQL Remote Graph Mode + shacl_file = target_file + val, _, v_text = pyshacl.validate( + target_file, shacl_graph=shacl_file, inference='none', check_dash_result=True, debug=True, sparql_mode=True, meta_shacl=False) + except (NotImplementedError, ReportableRuntimeError) as e: + print(e) + val = False + v_text = "" + assert val + print(v_text) # Tests for SHACL Advanced Features: https://www.w3.org/TR/shacl-af @@ -96,6 +126,7 @@ def test_dash_validate_all_sparql_rules(target_file, shacl_file): assert val print(v_text) + # Get all triple-rules tests. for x in walk(path.join(dash_files_dir, 'rules', 'triple')): for y in glob.glob(path.join(x[0], '*.test.ttl')): @@ -136,6 +167,7 @@ def test_dash_validate_all_triple_rules(target_file, shacl_file): print(v_text) + # Get all SHACL-AF sh:target tests. for x in walk(path.join(dash_files_dir, 'target')): for y in glob.glob(path.join(x[0], '*.test.ttl')): @@ -176,6 +208,46 @@ def test_dash_validate_target(target_file, shacl_file): print(v_text) + +@pytest.mark.parametrize('target_file, shacl_file', dash_target_files) +def test_dash_validate_target_sparql_mode(target_file, shacl_file): + test_name = shacl_file or target_file + try: + if shacl_file is None: + # shacl_file cannot be None in SPARQL Remote Graph Mode + shacl_file = target_file + val, _, v_text = pyshacl.validate( + target_file, shacl_graph=shacl_file, advanced=True, inference='none', check_dash_result=True, debug=True, sparql_mode=True, meta_shacl=False) + except NotImplementedError as ne: + for ani in ALLOWABLE_NOT_IMPLEMENTED: + if test_name.endswith(ani): + v_text = "Skipping not implemented feature in test: {}".format(test_name) + print(v_text) + val = True + break + else: + print(ne) + val = False + v_text = "" + except ReportableRuntimeError as e: + import traceback + print(e) + traceback.print_tb(e.__traceback__) + val = False + v_text = "" + try: + assert val + except AssertionError as ae: + for af in ALLOWABLE_FAILURES: + if test_name.endswith(af): + v_text = "Allowing failure in test: {}".format(test_name) + print(v_text) + break + else: + raise ae + + print(v_text) + # Get all SHACL-AF sh:expression tests. for x in walk(path.join(dash_files_dir, 'expression')): for y in glob.glob(path.join(x[0], '*.test.ttl')): diff --git a/test/test_sht_validate.py b/test/test_sht_validate.py index 6873b77..df6c499 100644 --- a/test/test_sht_validate.py +++ b/test/test_sht_validate.py @@ -52,6 +52,18 @@ def test_sht_all(base, index, caplog) -> None: }) +@pytest.mark.parametrize("base, index", test_index_map) +def test_sht_all_sparql_mode(base, index, caplog) -> None: + caplog.set_level(logging.DEBUG) + tests = tests_found_in_manifests[base] + test = tests[index] + run_sht_test(test, { + "inference": 'none', + "debug": True, + "sparql_mode": True, + "meta_shacl": False + }) + def run_sht_test(sht_test, validate_args: dict) -> None: logger = logging.getLogger() # pytest uses the root logger with a capturing handler if platform.system() == "Windows": @@ -61,6 +73,10 @@ def run_sht_test(sht_test, validate_args: dict) -> None: label = sht_test.label data_file = sht_test.data_graph shacl_file = sht_test.shapes_graph + sparql_mode = validate_args.get('sparql_mode', False) + if sparql_mode and shacl_file is None: + # shacl_file cannot be None in SPARQL Remote Graph Mode + shacl_file = data_file if label: logger.info("testing: ".format(label)) try: From f01154b8d5eba56fb8c59c95d166c7eb70ce853f Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Mon, 3 Jun 2024 20:49:25 +1000 Subject: [PATCH 2/3] README file typo, and link to section on Validation Failures --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 02076aa..18707b8 100644 --- a/README.md +++ b/README.md @@ -186,15 +186,15 @@ $ python3 -m pyshacl ``` ## Errors -Under certain circumstances pySHACL can produce a `Validation Failure`. This is a formal error defined by the SHACL specification and is required to be produced as a result of specific conditions within the SHACL graph. -If the validator produces a `Validation Failure`, the `results_graph` variable returned by the `validate()` function will be an instance of `ValidationFailure`. +Under certain circumstances pySHACL can produce a [`Validation Failure`](https://www.w3.org/TR/shacl/#failures). This is a formal error [defined by the SHACL specification](https://www.w3.org/TR/shacl/#failures) and is required to be produced as a result of specific conditions within the SHACL graph that leads to the inability to complete the validation. +If the validator produces a [`Validation Failure`](https://www.w3.org/TR/shacl/#failures), the `results_graph` variable returned by the `validate()` function will be an instance of `ValidationFailure`. See the `message` attribute on that instance to get more information about the validation failure. Other errors the validator can generate: - `ShapeLoadError`: This error is thrown when a SHACL Shape in the SHACL graph is in an invalid state and cannot be loaded into the validation engine. - `ConstraintLoadError`: This error is thrown when a SHACL Constraint Component is in an invalid state and cannot be loaded into the validation engine. - `ReportableRuntimeError`: An error occurred for a different reason, and the reason should be communicated back to the user of the validator. -- `RuntimeError`: The validator encountered a situation that caused it to throw an error, but the reason does concern the user. +- `RuntimeError`: The validator encountered a situation that caused it to throw an error, but the reason does not concern the user. Unlike `ValidationFailure`, these errors are not passed back as a result by the `validate()` function, but thrown as exceptions by the validation engine and must be caught in a `try ... except` block. From 34a7b01a5e69299b8357f1e7ffc3d68fb3567caa Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Fri, 21 Jun 2024 19:52:00 +1000 Subject: [PATCH 3/3] Change misleading Validation Error wording to Input Error. --- pyshacl/cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyshacl/cli.py b/pyshacl/cli.py index 37a75ac..2c59f21 100644 --- a/pyshacl/cli.py +++ b/pyshacl/cli.py @@ -220,7 +220,7 @@ def main(prog: Union[str, None] = None) -> None: http_main() if not args.data: # No datafile give, and not starting in server mode. - sys.stderr.write('Validation Error. No DataGraph file or endpoint supplied.\n') + sys.stderr.write('Input Error. No DataGraph file or endpoint supplied.\n') parser.print_usage(sys.stderr) sys.exit(1) validator_kwargs = {'debug': args.debug} @@ -228,7 +228,7 @@ def main(prog: Union[str, None] = None) -> None: if args.sparql_mode is not None and args.sparql_mode is True: endpoint = str(args.data).strip() if not endpoint.lower().startswith("http:") and not endpoint.lower().startswith("https:"): - sys.stderr.write("Validation Error. SPARQL Endpoint must start with http:// or https://.\n") + sys.stderr.write("Input Error. SPARQL Endpoint must start with http:// or https://.\n") sys.exit(1) data_graph = endpoint validator_kwargs['sparql_mode'] = True @@ -236,10 +236,10 @@ def main(prog: Union[str, None] = None) -> None: try: data_file = open(args.data, 'rb') except FileNotFoundError: - sys.stderr.write('Validation Error. DataGraph file not found.\n') + sys.stderr.write('Input Error. DataGraph file not found.\n') sys.exit(1) except PermissionError: - sys.stderr.write('Validation Error. DataGraph file not readable.\n') + sys.stderr.write('Input Error. DataGraph file not readable.\n') sys.exit(1) else: data_graph = data_file