Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
18 changes: 11 additions & 7 deletions pyshacl/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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(
Expand Down Expand Up @@ -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__":
Expand Down
8 changes: 4 additions & 4 deletions pyshacl/cli_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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(
Expand Down
1 change: 1 addition & 0 deletions pyshacl/sh_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class InputRDFFormat(Enum):
JSONLD = "json-ld"
NT = "nt"
N3 = "n3"
JELLY = "jelly"


@dataclass
Expand Down
135 changes: 135 additions & 0 deletions test/issues/test_jelly_support.py
Original file line number Diff line number Diff line change
@@ -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: <http://example.org/> .
ex:Alice ex:name "Alice" .
"""

shapes_ttl = """
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

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: <http://example.org/> .
ex:Bob ex:age "notANumber" .
"""

shapes_ttl = """
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

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: <http://example.org/> . ex:X ex:name \"X\" ."
shapes_ttl = """
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

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