diff --git a/poetry.lock b/poetry.lock
index c6d4a47..297d54c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]]
name = "aiofiles"
@@ -474,6 +474,27 @@ wcwidth = "*"
[package.extras]
tests = ["pytest", "pytest-cov", "pytest-lazy-fixtures"]
+[[package]]
+name = "protobuf"
+version = "6.33.2"
+description = ""
+optional = true
+python-versions = ">=3.9"
+groups = ["main"]
+markers = "extra == \"jelly\""
+files = [
+ {file = "protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d"},
+ {file = "protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4"},
+ {file = "protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43"},
+ {file = "protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e"},
+ {file = "protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872"},
+ {file = "protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f"},
+ {file = "protobuf-6.33.2-cp39-cp39-win32.whl", hash = "sha256:7109dcc38a680d033ffb8bf896727423528db9163be1b6a02d6a49606dcadbfe"},
+ {file = "protobuf-6.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:2981c58f582f44b6b13173e12bb8656711189c2a70250845f264b877f00b1913"},
+ {file = "protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c"},
+ {file = "protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4"},
+]
+
[[package]]
name = "pyduktape2"
version = "0.4.6"
@@ -551,6 +572,26 @@ files = [
{file = "pyduktape2-0.4.6.tar.gz", hash = "sha256:c84674e202ef4901bca8f6ea8b40197259bf44656167a1106ef076a491421bec"},
]
+[[package]]
+name = "pyjelly"
+version = "0.6.2"
+description = "Jelly-RDF implementation for Python"
+optional = true
+python-versions = ">=3.9"
+groups = ["main"]
+markers = "extra == \"jelly\""
+files = [
+ {file = "pyjelly-0.6.2-py3-none-any.whl", hash = "sha256:ba22053fced114541dc45a1c7ffcca57d02a47caba9ecd156430614e55cda9d5"},
+ {file = "pyjelly-0.6.2.tar.gz", hash = "sha256:ad7254cc2f0b9d3b9a18059d7096669d86f9510f7bf658f0011ca1ea43bc5f7f"},
+]
+
+[package.dependencies]
+protobuf = ">=6.30.0"
+typing-extensions = ">=4.12.2"
+
+[package.extras]
+rdflib = ["rdflib (>=7.1.4)"]
+
[[package]]
name = "pyparsing"
version = "3.2.1"
@@ -1168,9 +1209,10 @@ dev-coverage = ["coverage", "platformdirs", "pytest-cov"]
dev-lint = ["platformdirs", "ruff"]
dev-type-checking = ["mypy", "platformdirs", "types-setuptools"]
http = ["sanic", "sanic-cors", "sanic-ext"]
+jelly = ["pyjelly"]
js = ["pyduktape2"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.9,<4"
-content-hash = "963662cab3640d90efe7a54ea856e095ef47ee8c25a8333c8fb0ade73e162a59"
+content-hash = "cba69d0ea51b06c182f37ac7b5edd104855521390ce832496e50f6352c159195"
diff --git a/pyproject.toml b/pyproject.toml
index cef0921..02da460 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -71,6 +71,9 @@ http = [
"sanic-ext<23.6,>=23.3",
"sanic-cors==2.2.0"
]
+jelly = [
+ "pyjelly>=0.6.2"
+]
dev-lint = [
"ruff<0.10,>=0.9.3",
"platformdirs"
diff --git a/pyshacl/cli.py b/pyshacl/cli.py
index 98e7166..5ca0aa1 100644
--- a/pyshacl/cli.py
+++ b/pyshacl/cli.py
@@ -177,7 +177,7 @@ def str_is_true(s_var: str):
action='store',
help='Choose an output format. Default is "human".',
default='human',
- choices=('human', 'table', 'turtle', 'xml', 'json-ld', 'nt', 'n3'),
+ choices=('human', 'table', 'turtle', 'xml', 'json-ld', 'nt', 'n3', 'jelly'),
)
parser.add_argument(
'-df',
@@ -186,7 +186,7 @@ def str_is_true(s_var: str):
action='store',
help='Explicitly state the RDF File format of the input DataGraph file. Default="auto".',
default='auto',
- choices=('auto', 'turtle', 'xml', 'json-ld', 'nt', 'n3'),
+ choices=('auto', 'turtle', 'xml', 'json-ld', 'nt', 'n3', 'jelly'),
)
parser.add_argument(
'-sf',
@@ -195,7 +195,7 @@ def str_is_true(s_var: str):
action='store',
help='Explicitly state the RDF File format of the input SHACL file. Default="auto".',
default='auto',
- choices=('auto', 'turtle', 'xml', 'json-ld', 'nt', 'n3'),
+ choices=('auto', 'turtle', 'xml', 'json-ld', 'nt', 'n3', 'jelly'),
)
parser.add_argument(
'-ef',
@@ -204,7 +204,7 @@ def str_is_true(s_var: str):
action='store',
help='Explicitly state the RDF File format of the extra ontology file. Default="auto".',
default='auto',
- choices=('auto', 'turtle', 'xml', 'json-ld', 'nt', 'n3'),
+ choices=('auto', 'turtle', 'xml', 'json-ld', 'nt', 'n3', 'jelly'),
)
parser.add_argument('-V', '--version', action=ShowVersion, help='Show PySHACL version and exit.')
parser.add_argument(
@@ -422,10 +422,14 @@ def col_widther(s, w):
args.output.write(str(t2))
else:
if isinstance(v_graph, bytes):
- v_graph = v_graph.decode('utf-8')
+ if args.output is not None and args.output != sys.stdout:
+ args.output.close()
+ with open(args.output.name, "wb") as f:
+ f.write(v_graph)
+ else:
+ sys.stdout.buffer.write(v_graph)
+ sys.exit(0 if is_conform else 1)
args.output.write(v_graph)
- args.output.close()
- sys.exit(0 if is_conform else 1)
if __name__ == "__main__":
diff --git a/pyshacl/cli_rules.py b/pyshacl/cli_rules.py
index dfbbb2a..320f145 100644
--- a/pyshacl/cli_rules.py
+++ b/pyshacl/cli_rules.py
@@ -108,7 +108,7 @@
action='store',
help='Choose an output format. Default is "trig" for Datasets and "turtle" for Graphs.',
default='auto',
- choices=('auto', 'turtle', 'xml', 'trig', 'json-ld', 'nt', 'n3', 'nquads'),
+ choices=('auto', 'turtle', 'xml', 'trig', 'json-ld', 'nt', 'n3', 'nquads', 'jelly'),
)
parser.add_argument(
'-df',
@@ -117,7 +117,7 @@
action='store',
help='Explicitly state the RDF File format of the input DataGraph file. Default="auto".',
default='auto',
- choices=('auto', 'turtle', 'xml', 'trig', 'json-ld', 'nt', 'n3', 'nquads'),
+ choices=('auto', 'turtle', 'xml', 'trig', 'json-ld', 'nt', 'n3', 'nquads', 'jelly'),
)
parser.add_argument(
'-sf',
@@ -126,7 +126,7 @@
action='store',
help='Explicitly state the RDF File format of the input SHACL file. Default="auto".',
default='auto',
- choices=('auto', 'turtle', 'xml', 'trig', 'json-ld', 'nt', 'n3', 'nquads'),
+ choices=('auto', 'turtle', 'xml', 'trig', 'json-ld', 'nt', 'n3', 'nquads', 'jelly'),
)
parser.add_argument(
'-ef',
@@ -135,7 +135,7 @@
action='store',
help='Explicitly state the RDF File format of the extra ontology file. Default="auto".',
default='auto',
- choices=('auto', 'turtle', 'xml', 'trig', 'json-ld', 'nt', 'n3', 'nquads'),
+ choices=('auto', 'turtle', 'xml', 'trig', 'json-ld', 'nt', 'n3', 'nquads', 'jelly'),
)
parser.add_argument('-V', '--version', action=ShowVersion, help='Show PySHACL version and exit.')
parser.add_argument(
diff --git a/pyshacl/sh_http.py b/pyshacl/sh_http.py
index 06abe00..b11ba52 100644
--- a/pyshacl/sh_http.py
+++ b/pyshacl/sh_http.py
@@ -41,6 +41,7 @@ class InputRDFFormat(Enum):
JSONLD = "json-ld"
NT = "nt"
N3 = "n3"
+ JELLY = "jelly"
@dataclass
diff --git a/test/issues/test_jelly_support.py b/test/issues/test_jelly_support.py
new file mode 100644
index 0000000..82b9f00
--- /dev/null
+++ b/test/issues/test_jelly_support.py
@@ -0,0 +1,135 @@
+import subprocess
+from rdflib import Graph
+from pyshacl import validate
+
+def test_jelly_api_valid(tmp_path):
+ data_ttl = """
+ @prefix ex: .
+ ex:Alice ex:name "Alice" .
+ """
+
+ shapes_ttl = """
+ @prefix sh: .
+ @prefix ex: .
+ @prefix xsd: .
+
+ ex:PersonShape a sh:NodeShape ;
+ sh:targetClass ex:Alice ;
+ sh:property [
+ sh:path ex:name ;
+ sh:datatype xsd:string ;
+ ] .
+ """
+
+ # save ttl
+ data_ttl_path = tmp_path / "data.ttl"
+ shapes_ttl_path = tmp_path / "shapes.ttl"
+ data_ttl_path.write_text(data_ttl)
+ shapes_ttl_path.write_text(shapes_ttl)
+
+ # convert to jelly
+ g_data = Graph().parse(data_ttl_path)
+ g_shapes = Graph().parse(shapes_ttl_path)
+ data_jelly_path = tmp_path / "data.jelly"
+ shapes_jelly_path = tmp_path / "shapes.jelly"
+ g_data.serialize(data_jelly_path, format="jelly")
+ g_shapes.serialize(shapes_jelly_path, format="jelly")
+
+ # API validation
+ conforms, report_graph, _ = validate(
+ data_graph=g_data,
+ shacl_graph=g_shapes,
+ data_graph_format="jelly",
+ shacl_graph_format="jelly",
+ )
+
+ assert conforms
+ assert len(report_graph) > 0
+
+
+def test_jelly_api_invalid(tmp_path):
+ data_ttl = """
+ @prefix ex: .
+ ex:Bob ex:age "notANumber" .
+ """
+
+ shapes_ttl = """
+ @prefix sh: .
+ @prefix ex: .
+ @prefix xsd: .
+
+ ex:PersonShape a sh:NodeShape ;
+ sh:targetNode ex:Bob ;
+ sh:property [
+ sh:path ex:age ;
+ sh:datatype xsd:integer ;
+ ] .
+ """
+
+ data_ttl_path = tmp_path / "data.ttl"
+ shapes_ttl_path = tmp_path / "shapes.ttl"
+ data_ttl_path.write_text(data_ttl)
+ shapes_ttl_path.write_text(shapes_ttl)
+
+ g_data = Graph().parse(data_ttl_path)
+ g_shapes = Graph().parse(shapes_ttl_path)
+ data_jelly_path = tmp_path / "data.jelly"
+ shapes_jelly_path = tmp_path / "shapes.jelly"
+ g_data.serialize(data_jelly_path, format="jelly")
+ g_shapes.serialize(shapes_jelly_path, format="jelly")
+
+ conforms, report_graph, _ = validate(
+ data_graph=g_data,
+ shacl_graph=g_shapes,
+ data_graph_format="jelly",
+ shacl_graph_format="jelly",
+ )
+
+ assert not conforms
+ assert len(report_graph) > 0
+
+
+def test_jelly_cli_valid(tmp_path):
+ data_ttl = "@prefix ex: . ex:X ex:name \"X\" ."
+ shapes_ttl = """
+ @prefix sh: .
+ @prefix ex: .
+ @prefix xsd: .
+
+ ex:Shape a sh:NodeShape ;
+ sh:targetClass ex:X ;
+ sh:property [
+ sh:path ex:name ;
+ sh:datatype xsd:string ;
+ ] .
+ """
+
+ data_ttl_path = tmp_path / "d.ttl"
+ shapes_ttl_path = tmp_path / "s.ttl"
+ data_ttl_path.write_text(data_ttl)
+ shapes_ttl_path.write_text(shapes_ttl)
+
+ g_data = Graph().parse(data_ttl_path)
+ g_shapes = Graph().parse(shapes_ttl_path)
+
+ data_jelly = tmp_path / "d.jelly"
+ shapes_jelly = tmp_path / "s.jelly"
+ g_data.serialize(data_jelly, format="jelly")
+ g_shapes.serialize(shapes_jelly, format="jelly")
+
+ # run CLI
+ result = subprocess.run(
+ [
+ "pyshacl",
+ str(data_jelly),
+ "-s", str(shapes_jelly),
+ "-df", "jelly",
+ "-sf", "jelly"
+ ],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True
+ )
+
+ assert result.returncode == 0
+ assert "Conforms: True" in result.stdout