shacl2code 0.0.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shacl2code/__init__.py +4 -0
- shacl2code/__main__.py +12 -0
- shacl2code/context.py +167 -0
- shacl2code/lang/__init__.py +11 -0
- shacl2code/lang/common.py +144 -0
- shacl2code/lang/jinja.py +29 -0
- shacl2code/lang/jsonschema.py +51 -0
- shacl2code/lang/lang.py +19 -0
- shacl2code/lang/python.py +54 -0
- shacl2code/lang/templates/jsonschema.j2 +277 -0
- shacl2code/lang/templates/python.j2 +2054 -0
- shacl2code/main.py +139 -0
- shacl2code/model.py +293 -0
- shacl2code/urlcontext.py +23 -0
- shacl2code/version.py +1 -0
- shacl2code-0.0.11.dist-info/METADATA +205 -0
- shacl2code-0.0.11.dist-info/RECORD +20 -0
- shacl2code-0.0.11.dist-info/WHEEL +4 -0
- shacl2code-0.0.11.dist-info/entry_points.txt +2 -0
- shacl2code-0.0.11.dist-info/licenses/LICENSE +21 -0
shacl2code/main.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2024 Joshua Watt
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: MIT
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import urllib.request
|
|
11
|
+
import rdflib
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from . import Model, UrlContext, ContextData
|
|
15
|
+
from .version import VERSION
|
|
16
|
+
from .lang import LANGUAGES
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main(args=None):
|
|
20
|
+
def handle_generate(parser, args):
|
|
21
|
+
graph = rdflib.Graph()
|
|
22
|
+
for inmodel in args.input:
|
|
23
|
+
if inmodel == "-":
|
|
24
|
+
if args.input_format == "auto":
|
|
25
|
+
print("ERROR: Input format must be specified with stdin")
|
|
26
|
+
parser.print_help()
|
|
27
|
+
return 1
|
|
28
|
+
|
|
29
|
+
graph.parse(sys.stdin, format=args.input_format)
|
|
30
|
+
else:
|
|
31
|
+
if args.input_format == "auto":
|
|
32
|
+
graph.parse(inmodel)
|
|
33
|
+
else:
|
|
34
|
+
graph.parse(inmodel, format=args.input_format)
|
|
35
|
+
|
|
36
|
+
contexts = []
|
|
37
|
+
for c in args.context:
|
|
38
|
+
with urllib.request.urlopen(c) as f:
|
|
39
|
+
data = json.load(f)
|
|
40
|
+
contexts.append(ContextData(data, c))
|
|
41
|
+
|
|
42
|
+
for location, url in args.context_url:
|
|
43
|
+
if "://" in location:
|
|
44
|
+
with urllib.request.urlopen(location) as f:
|
|
45
|
+
data = json.load(f)
|
|
46
|
+
else:
|
|
47
|
+
with Path(location).open("r") as f:
|
|
48
|
+
data = json.load(f)
|
|
49
|
+
contexts.append(ContextData(data, url))
|
|
50
|
+
|
|
51
|
+
m = Model(graph, UrlContext(contexts))
|
|
52
|
+
|
|
53
|
+
render = args.lang(args)
|
|
54
|
+
render.output(m)
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
def handle_list(parser, args):
|
|
58
|
+
width = max(len(lang) for lang in LANGUAGES)
|
|
59
|
+
for k, v in LANGUAGES.items():
|
|
60
|
+
if args.short:
|
|
61
|
+
print(k)
|
|
62
|
+
else:
|
|
63
|
+
print(f"{k:{width}} - {v.HELP}")
|
|
64
|
+
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
def handle_version(parser, args):
|
|
68
|
+
print(VERSION)
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
parser = argparse.ArgumentParser(
|
|
72
|
+
description=f"Convert JSON-LD model to python. Version {VERSION}"
|
|
73
|
+
)
|
|
74
|
+
command_subparser = parser.add_subparsers(
|
|
75
|
+
title="command",
|
|
76
|
+
description="Command to execute",
|
|
77
|
+
required=True,
|
|
78
|
+
)
|
|
79
|
+
generate_parser = command_subparser.add_parser(
|
|
80
|
+
"generate",
|
|
81
|
+
help="Generate language bindings",
|
|
82
|
+
)
|
|
83
|
+
generate_parser.add_argument(
|
|
84
|
+
"--input",
|
|
85
|
+
"-i",
|
|
86
|
+
help="Input model (path, URL, or '-')",
|
|
87
|
+
action="append",
|
|
88
|
+
default=[],
|
|
89
|
+
required=True,
|
|
90
|
+
)
|
|
91
|
+
generate_parser.add_argument(
|
|
92
|
+
"--input-format",
|
|
93
|
+
"-t",
|
|
94
|
+
help="Input file format, or 'auto' to attempt to determine automatically. Default is %(default)s",
|
|
95
|
+
default="auto",
|
|
96
|
+
)
|
|
97
|
+
generate_parser.add_argument(
|
|
98
|
+
"--context",
|
|
99
|
+
"-x",
|
|
100
|
+
help="Require context for output (URL)",
|
|
101
|
+
action="append",
|
|
102
|
+
default=[],
|
|
103
|
+
)
|
|
104
|
+
generate_parser.add_argument(
|
|
105
|
+
"--context-url",
|
|
106
|
+
"-u",
|
|
107
|
+
help="Require context from LOCATION (path or URL), but report as URL in generated code",
|
|
108
|
+
nargs=2,
|
|
109
|
+
metavar=("LOCATION", "URL"),
|
|
110
|
+
action="append",
|
|
111
|
+
default=[],
|
|
112
|
+
)
|
|
113
|
+
generate_parser.set_defaults(func=handle_generate)
|
|
114
|
+
|
|
115
|
+
lang_subparser = generate_parser.add_subparsers(
|
|
116
|
+
title="language",
|
|
117
|
+
description="Language to generate",
|
|
118
|
+
required=True,
|
|
119
|
+
)
|
|
120
|
+
for k, v in LANGUAGES.items():
|
|
121
|
+
p = lang_subparser.add_parser(k, help=v.HELP)
|
|
122
|
+
v.get_arguments(p)
|
|
123
|
+
p.set_defaults(lang=v)
|
|
124
|
+
|
|
125
|
+
list_parser = command_subparser.add_parser("list", help="List languages")
|
|
126
|
+
list_parser.add_argument(
|
|
127
|
+
"--short",
|
|
128
|
+
"-s",
|
|
129
|
+
action="store_true",
|
|
130
|
+
help="Only list languages without descriptions",
|
|
131
|
+
)
|
|
132
|
+
list_parser.set_defaults(func=handle_list)
|
|
133
|
+
|
|
134
|
+
version_parser = command_subparser.add_parser("version", help="Show version")
|
|
135
|
+
version_parser.set_defaults(func=handle_version)
|
|
136
|
+
|
|
137
|
+
parsed_args = parser.parse_args(args)
|
|
138
|
+
|
|
139
|
+
return parsed_args.func(parser, parsed_args)
|
shacl2code/model.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2024 Joshua Watt
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: MIT
|
|
6
|
+
|
|
7
|
+
import typing
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from rdflib import URIRef
|
|
11
|
+
from rdflib.namespace import RDF, RDFS, OWL, SH, XSD, DefinedNamespace, Namespace
|
|
12
|
+
|
|
13
|
+
PATTERN_DATATYPES = [
|
|
14
|
+
str(XSD.string),
|
|
15
|
+
str(XSD.dateTime),
|
|
16
|
+
str(XSD.dateTimeStamp),
|
|
17
|
+
str(XSD.anyURI),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SHACL2CODE(DefinedNamespace):
|
|
22
|
+
idPropertyName: URIRef
|
|
23
|
+
isExtensible: URIRef
|
|
24
|
+
isAbstract: URIRef
|
|
25
|
+
|
|
26
|
+
_NS = Namespace("https://jpewdev.github.io/shacl2code/schema#")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ModelException(Exception):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def common_prefix(*s):
|
|
34
|
+
if not s:
|
|
35
|
+
return ""
|
|
36
|
+
|
|
37
|
+
if len(s) == 1:
|
|
38
|
+
return s[0]
|
|
39
|
+
|
|
40
|
+
p1 = common_prefix(*s[: len(s) // 2])
|
|
41
|
+
p2 = common_prefix(*s[len(s) // 2 :])
|
|
42
|
+
for idx in range(len(p1)):
|
|
43
|
+
if idx >= len(p2):
|
|
44
|
+
return p2
|
|
45
|
+
|
|
46
|
+
if p1[idx] != p2[idx]:
|
|
47
|
+
return p2[:idx]
|
|
48
|
+
|
|
49
|
+
return p1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def remove_common_prefix(val, *cmp):
|
|
53
|
+
prefix = common_prefix(val, *cmp)
|
|
54
|
+
return val[len(prefix) :]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Individual:
|
|
59
|
+
_id: str
|
|
60
|
+
varname: str
|
|
61
|
+
comment: str = ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class Property:
|
|
66
|
+
path: str
|
|
67
|
+
varname: str
|
|
68
|
+
comment: str = ""
|
|
69
|
+
max_count: int = None
|
|
70
|
+
min_count: int = None
|
|
71
|
+
enum_values: list = None
|
|
72
|
+
class_id: str = ""
|
|
73
|
+
datatype: str = ""
|
|
74
|
+
pattern: str = ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class Class:
|
|
79
|
+
_id: str
|
|
80
|
+
clsname: str
|
|
81
|
+
parent_ids: typing.List[str]
|
|
82
|
+
derived_ids: list
|
|
83
|
+
properties: typing.List[Property]
|
|
84
|
+
comment: str = ""
|
|
85
|
+
id_property: str = ""
|
|
86
|
+
node_kind: str = None
|
|
87
|
+
is_extensible: bool = False
|
|
88
|
+
is_abstract: bool = False
|
|
89
|
+
named_individuals: list = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Model(object):
|
|
93
|
+
def __init__(self, graph, context=None):
|
|
94
|
+
self.model = graph
|
|
95
|
+
self.context = context
|
|
96
|
+
self.compact_ids = {}
|
|
97
|
+
self.objects = {}
|
|
98
|
+
self.enums = []
|
|
99
|
+
self.classes = []
|
|
100
|
+
class_iris = set()
|
|
101
|
+
classes_by_iri = {}
|
|
102
|
+
|
|
103
|
+
def int_val(v):
|
|
104
|
+
if not v:
|
|
105
|
+
return None
|
|
106
|
+
return int(v)
|
|
107
|
+
|
|
108
|
+
def str_val(v):
|
|
109
|
+
if v is None:
|
|
110
|
+
return v
|
|
111
|
+
return str(v)
|
|
112
|
+
|
|
113
|
+
def get_inherited_value(subject, predicate, default=None):
|
|
114
|
+
def get_value(subject, predicate):
|
|
115
|
+
value = self.model.value(subject, predicate)
|
|
116
|
+
if value is not None:
|
|
117
|
+
return value
|
|
118
|
+
|
|
119
|
+
for parent in self.model.objects(subject, RDFS.subClassOf):
|
|
120
|
+
value = get_value(parent, predicate)
|
|
121
|
+
if value is not None:
|
|
122
|
+
return value
|
|
123
|
+
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
value = get_value(subject, predicate)
|
|
127
|
+
if value is not None:
|
|
128
|
+
return value
|
|
129
|
+
return default
|
|
130
|
+
|
|
131
|
+
def set_prop_range(p, range_id):
|
|
132
|
+
nonlocal class_iris
|
|
133
|
+
|
|
134
|
+
if range_id in class_iris:
|
|
135
|
+
p.class_id = str(range_id)
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
def get_named_individuals(cls_iri):
|
|
141
|
+
members = []
|
|
142
|
+
for member_iri in self.model.subjects(RDF.type, cls_iri):
|
|
143
|
+
if (member_iri, RDF.type, OWL.NamedIndividual) not in self.model:
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
members.append(
|
|
147
|
+
Individual(
|
|
148
|
+
_id=str(member_iri),
|
|
149
|
+
varname=remove_common_prefix(member_iri, cls_iri).lstrip("/"),
|
|
150
|
+
comment=str(
|
|
151
|
+
self.model.value(member_iri, RDFS.comment, default="")
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
return members
|
|
156
|
+
|
|
157
|
+
def is_abstract(s):
|
|
158
|
+
if (
|
|
159
|
+
s,
|
|
160
|
+
RDF.type,
|
|
161
|
+
URIRef("http://spdx.invalid./AbstractClass"),
|
|
162
|
+
) in self.model:
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
return bool(self.model.value(s, SHACL2CODE.isAbstract, default=False))
|
|
166
|
+
|
|
167
|
+
class_iris = set(self.model.subjects(RDF.type, OWL.Class))
|
|
168
|
+
for cls_iri in class_iris:
|
|
169
|
+
c = Class(
|
|
170
|
+
_id=str(cls_iri),
|
|
171
|
+
parent_ids=[
|
|
172
|
+
str(parent_iri)
|
|
173
|
+
for parent_iri in self.model.objects(cls_iri, RDFS.subClassOf)
|
|
174
|
+
if parent_iri in class_iris
|
|
175
|
+
],
|
|
176
|
+
derived_ids=[],
|
|
177
|
+
clsname=self.get_class_name(cls_iri),
|
|
178
|
+
comment=str(self.model.value(cls_iri, RDFS.comment, default="")),
|
|
179
|
+
properties=[],
|
|
180
|
+
id_property=str_val(
|
|
181
|
+
get_inherited_value(cls_iri, SHACL2CODE.idPropertyName)
|
|
182
|
+
),
|
|
183
|
+
node_kind=get_inherited_value(cls_iri, SH.nodeKind, SH.BlankNodeOrIRI),
|
|
184
|
+
is_extensible=bool(self.model.value(cls_iri, SHACL2CODE.isExtensible)),
|
|
185
|
+
is_abstract=is_abstract(cls_iri),
|
|
186
|
+
named_individuals=get_named_individuals(cls_iri),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if c.node_kind not in (SH.IRI, SH.BlankNode, SH.BlankNodeOrIRI):
|
|
190
|
+
raise ModelException(
|
|
191
|
+
f"Class {c._id} has unsupported '{SH.nodeKind}' value '{c.node_kind}'"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
for obj_prop in self.model.objects(cls_iri, SH.property):
|
|
195
|
+
prop = self.model.value(obj_prop, SH.path)
|
|
196
|
+
|
|
197
|
+
p = Property(
|
|
198
|
+
varname=self.model.value(
|
|
199
|
+
obj_prop,
|
|
200
|
+
SH.name,
|
|
201
|
+
default=self.get_compact_id(
|
|
202
|
+
prop,
|
|
203
|
+
fallback=remove_common_prefix(prop, cls_iri).lstrip("/"),
|
|
204
|
+
),
|
|
205
|
+
),
|
|
206
|
+
path=str(prop),
|
|
207
|
+
comment=str(self.model.value(prop, RDFS.comment, default="")),
|
|
208
|
+
max_count=int_val(self.model.value(obj_prop, SH.maxCount)),
|
|
209
|
+
min_count=int_val(self.model.value(obj_prop, SH.minCount)),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if in_list := self.model.value(obj_prop, SH["in"]):
|
|
213
|
+
p.enum_values = sorted(tuple(self.model.items(in_list)))
|
|
214
|
+
|
|
215
|
+
if range_id := self.model.value(obj_prop, SH["class"]):
|
|
216
|
+
if not set_prop_range(p, range_id):
|
|
217
|
+
raise ModelException(
|
|
218
|
+
f"Prop {prop} has unknown class restriction {range_id}"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
elif range_id := self.model.value(obj_prop, SH.datatype):
|
|
222
|
+
p.datatype = str(range_id)
|
|
223
|
+
|
|
224
|
+
elif range_id := self.model.value(prop, RDFS.range):
|
|
225
|
+
if not set_prop_range(p, range_id):
|
|
226
|
+
p.datatype = str(range_id)
|
|
227
|
+
|
|
228
|
+
else:
|
|
229
|
+
raise ModelException(f"Prop '{prop}' is missing range")
|
|
230
|
+
|
|
231
|
+
if pattern := self.model.value(obj_prop, SH.pattern):
|
|
232
|
+
if not p.datatype:
|
|
233
|
+
raise ModelException(
|
|
234
|
+
f"Property '{prop}' is not a datatype and may not have a pattern"
|
|
235
|
+
)
|
|
236
|
+
if p.datatype not in PATTERN_DATATYPES:
|
|
237
|
+
raise ModelException(
|
|
238
|
+
f"Property '{prop}' of type '{p.datatype}' cannot have a pattern. Must be one of type {' '.join(PATTERN_DATATYPES)}"
|
|
239
|
+
)
|
|
240
|
+
p.pattern = str(pattern)
|
|
241
|
+
|
|
242
|
+
c.properties.append(p)
|
|
243
|
+
|
|
244
|
+
c.properties.sort(key=lambda p: p.path)
|
|
245
|
+
|
|
246
|
+
self.classes.append(c)
|
|
247
|
+
classes_by_iri[str(cls_iri)] = c
|
|
248
|
+
|
|
249
|
+
for c in self.classes:
|
|
250
|
+
for p in c.parent_ids:
|
|
251
|
+
classes_by_iri[p].derived_ids.append(c._id)
|
|
252
|
+
|
|
253
|
+
for c in self.classes:
|
|
254
|
+
c.derived_ids.sort()
|
|
255
|
+
|
|
256
|
+
self.enums.sort(key=lambda e: e._id)
|
|
257
|
+
self.classes.sort(key=lambda c: c._id)
|
|
258
|
+
|
|
259
|
+
tmp_classes = self.classes
|
|
260
|
+
done_ids = set()
|
|
261
|
+
self.classes = []
|
|
262
|
+
|
|
263
|
+
while tmp_classes:
|
|
264
|
+
c = tmp_classes.pop(0)
|
|
265
|
+
|
|
266
|
+
# If any parent classes of this class are outstanding, then push it
|
|
267
|
+
# back on the end of the class list and try again. This ensures that
|
|
268
|
+
# derived classes are always written after any parent classes
|
|
269
|
+
if not all(p in done_ids for p in c.parent_ids):
|
|
270
|
+
tmp_classes.append(c)
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
self.classes.append(c)
|
|
274
|
+
done_ids.add(c._id)
|
|
275
|
+
|
|
276
|
+
def get_compact_id(self, _id, *, fallback=None):
|
|
277
|
+
"""
|
|
278
|
+
Returns the "compacted" name of an object, that is the name of the
|
|
279
|
+
object with the context applied
|
|
280
|
+
"""
|
|
281
|
+
_id = str(_id)
|
|
282
|
+
if _id not in self.compact_ids:
|
|
283
|
+
self.compact_ids[_id] = self.context.compact(_id)
|
|
284
|
+
|
|
285
|
+
if self.compact_ids[_id] == _id and fallback is not None:
|
|
286
|
+
return fallback
|
|
287
|
+
return self.compact_ids[_id]
|
|
288
|
+
|
|
289
|
+
def get_class_name(self, c):
|
|
290
|
+
"""
|
|
291
|
+
Returns the name for a class that should be used in Code
|
|
292
|
+
"""
|
|
293
|
+
return self.get_compact_id(c).split(":")
|
shacl2code/urlcontext.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2024 Joshua Watt
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: MIT
|
|
6
|
+
|
|
7
|
+
import typing
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from .context import Context
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ContextData:
|
|
14
|
+
context: typing.Dict
|
|
15
|
+
url: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UrlContext(Context):
|
|
19
|
+
def __init__(self, contexts=[]):
|
|
20
|
+
super().__init__([c.context.get("@context", {}) for c in contexts])
|
|
21
|
+
self.urls = []
|
|
22
|
+
for ctx in contexts:
|
|
23
|
+
self.urls.append(ctx.url)
|
shacl2code/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.0.11"
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: shacl2code
|
|
3
|
+
Version: 0.0.11
|
|
4
|
+
Summary: Convert SHACL model file to code bindings
|
|
5
|
+
Project-URL: Homepage, https://github.com/JPEWdev/shacl2code
|
|
6
|
+
Project-URL: Repository, https://github.com/JPEWdev/shacl2code.git
|
|
7
|
+
Project-URL: Issues, https://github.com/JPEWdev/shacl2code/issues
|
|
8
|
+
Author-email: Joshua Watt <JPEWhacker@gmail.com>
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Requires-Dist: jinja2>=3.1.2
|
|
22
|
+
Requires-Dist: rdflib>=7.0.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: flake8>=7.0.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: jsonschema>=4.21.1; extra == 'dev'
|
|
26
|
+
Requires-Dist: pyshacl>=0.25.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-server-fixtures>=1.7; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# Convert SHACL Model to code bindings
|
|
33
|
+
[](https://htmlpreview.github.io/?https://github.com/JPEWdev/shacl2code/blob/python-coverage-comment-action-data/htmlcov/index.html)
|
|
34
|
+
|
|
35
|
+
This tool can be used to convert a SHACL model into various code bindings
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
`shacl2code` can be installed using pip:
|
|
40
|
+
|
|
41
|
+
```shell
|
|
42
|
+
python3 -m pip install shacl2code
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
`shacl2code` can generate bindings from either a local file:
|
|
48
|
+
```shell
|
|
49
|
+
shacl2code generate -i model.jsonld python -o out.py
|
|
50
|
+
```
|
|
51
|
+
Or from a URL:
|
|
52
|
+
```shell
|
|
53
|
+
shacl2code generate -i https://example.com/rdf/model.jsonld python -o out.py
|
|
54
|
+
```
|
|
55
|
+
Or from stdin:
|
|
56
|
+
```shell
|
|
57
|
+
cat model.jsonld | shacl2code generate -i - python -o - > out.py
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For more information, run:
|
|
61
|
+
```shell
|
|
62
|
+
shacl2code --help
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The available language bindings can be viewed by running:
|
|
66
|
+
```shell
|
|
67
|
+
shacl2code list
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Developing
|
|
71
|
+
|
|
72
|
+
Developing on `shacl2code` is best done using a virtual environment. You can
|
|
73
|
+
configure one and install shacl2code in editable mode with all necessary
|
|
74
|
+
development dependencies by running:
|
|
75
|
+
|
|
76
|
+
```shell
|
|
77
|
+
python3 -m venv .venv
|
|
78
|
+
. .venv/bin/activate
|
|
79
|
+
pip install -e ".[dev]"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Testing
|
|
83
|
+
|
|
84
|
+
`shacl2code` has a test suite written in [pytest][pytest]. To run it, setup a
|
|
85
|
+
virtual environment as shown above, then run:
|
|
86
|
+
```shell
|
|
87
|
+
pytest
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
In addition to the test results, a test coverage report will also be generated
|
|
91
|
+
using [pytest-cov][pytest-cov]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## Custom Annotations
|
|
95
|
+
|
|
96
|
+
`shacl2code` supports a number of custom annotations that can be specified in a
|
|
97
|
+
SHACL model to give hints about the generated code. All of these annotations
|
|
98
|
+
live in the `https://jpewdev.github.io/shacl2code/schema#` namespace, and
|
|
99
|
+
commonly are given the `sh-to-code` prefix to make it easier to reference them.
|
|
100
|
+
For example, in Turtle one would add the prefix mapping:
|
|
101
|
+
|
|
102
|
+
```ttl
|
|
103
|
+
@prefix sh-to-code: <https://jpewdev.github.io/shacl2code/schema#> .
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### ID Property Name
|
|
107
|
+
|
|
108
|
+
The `idPropertyName` annotation allows a class to specify what the name of the
|
|
109
|
+
"property" that specifies the RDF subject of an object is for serializations
|
|
110
|
+
that support it. For example, in JSON-LD, the `@id` property indicates the
|
|
111
|
+
subject in RDF. If you wanted to alias the `@id` property to another name, the
|
|
112
|
+
`idPropertyName` annotation will let you do this. For example, the following
|
|
113
|
+
turtle will use `MyId` instead of `@id` when writing JSON-LD bindings:
|
|
114
|
+
|
|
115
|
+
```ttl
|
|
116
|
+
<MyClass> a owl:Class, sh:NodeShape ;
|
|
117
|
+
sh-to-code:idPropertyName "MyId"
|
|
118
|
+
.
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
When doing this, the class would then look like this in JSON-LD:
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"@type": "MyClass",
|
|
125
|
+
"MyId": "http://example.com/id"
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The `idProperyName` annotation is inherited by derived classes, so for example
|
|
130
|
+
any class that derived from `MyClass` would also use `MyId` as the subject
|
|
131
|
+
property.
|
|
132
|
+
|
|
133
|
+
**Note:** This only specifies what the name of the field should be in generated
|
|
134
|
+
bindings and has no bearing on how an RDF parser would interpret the property.
|
|
135
|
+
In order to still be parsed by RDF, you would also need context file that maps
|
|
136
|
+
`MyId` to `@id`, for example:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"@context": {
|
|
141
|
+
"MyId": "@id"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`shacl2code` doesn't do this for you, nor does it validate that you have done
|
|
147
|
+
it.
|
|
148
|
+
|
|
149
|
+
### Extensible Classes
|
|
150
|
+
|
|
151
|
+
Most bindings generated from `shacl2code` are "closed" in that they do not
|
|
152
|
+
allow extra properties to be added to object outside of what is specified in
|
|
153
|
+
model. This ensures that field name typos and other unintended properties are
|
|
154
|
+
not added to an object. However, in some cases a class may be specifically
|
|
155
|
+
intended to be extended such that arbitrary fields can be added to it, which
|
|
156
|
+
can be done using the `isExtensible` property. This is a boolean property that
|
|
157
|
+
indicates if a class can be extended, and defaults to `false`. For example, the
|
|
158
|
+
following turtle will declare a class as extensible:
|
|
159
|
+
|
|
160
|
+
```ttl
|
|
161
|
+
|
|
162
|
+
<MyClass> a owl:Class, sh:NodeShape ;
|
|
163
|
+
sh-to-code:isExtensible true
|
|
164
|
+
.
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The `isExtensible` property is _not_ inherited by derived classes, meaning it
|
|
168
|
+
is possible to have a class derived from `MyClass` which is itself not
|
|
169
|
+
extensible.
|
|
170
|
+
|
|
171
|
+
The mechanism for dealing with extensible classes will vary between the
|
|
172
|
+
different bindings, but in general it means that they will not be very picky
|
|
173
|
+
about object types and properties in any location where an extensible class is
|
|
174
|
+
allowed.
|
|
175
|
+
|
|
176
|
+
**Note**: You may want to be careful about where and how many extensible
|
|
177
|
+
classes are allowed in your model. If there are too many and they are allowed
|
|
178
|
+
anywhere, it may mean that typos in object types (e.g. `@type` in JSON-LD) are
|
|
179
|
+
not caught by validation as they will have to be assumed to be a derived class
|
|
180
|
+
from an extensible type.
|
|
181
|
+
|
|
182
|
+
### Abstract Classes
|
|
183
|
+
|
|
184
|
+
By default, classes generated by `shacl2code` are all instantiable (i.e. they
|
|
185
|
+
can be created). In some instances, it may be desirable to declare a class as
|
|
186
|
+
abstract (meaning that it cannot be instantiated, but non-abstract derived
|
|
187
|
+
classes can). This can be done with the boolean `isAbstract` property. For
|
|
188
|
+
example, the following turtle will declare a class as abstract:
|
|
189
|
+
|
|
190
|
+
```ttl
|
|
191
|
+
|
|
192
|
+
<MyClass> a owl:Class, sh:NodeShape ;
|
|
193
|
+
sh-to-code:isAbstract true
|
|
194
|
+
.
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The `isAbstract` property is _not_ inherited by derived classes, so any derived
|
|
198
|
+
classes are automatically concrete unless they indicate otherwise.
|
|
199
|
+
|
|
200
|
+
Note that for compatibility reasons, it is also possible to define a class as
|
|
201
|
+
abstract by declaring it to be of type: `http://spdx.invalid./AbstractClass`,
|
|
202
|
+
but this is not preferred.
|
|
203
|
+
|
|
204
|
+
[pytest]: https://www.pytest.org
|
|
205
|
+
[pytest-cov]: https://pytest-cov.readthedocs.io/en/latest/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
shacl2code/__init__.py,sha256=aq5je2i1vkTFCIhUEEt1RsbJlsXpg2rzPLEK_PJh3ac,197
|
|
2
|
+
shacl2code/__main__.py,sha256=xWmYOo4S8cDiXgIQFhBg1fdHi1ANIbviMSrbx4LXOAw,173
|
|
3
|
+
shacl2code/context.py,sha256=pzu_GseOZVz4gR0JC5LjuKigOn5LrlS9rlwyu9wfgDA,5368
|
|
4
|
+
shacl2code/main.py,sha256=eQ5bWu7pGKWb2DAEOc4brMapzPlghtuW6hOZ9M85lMo,4005
|
|
5
|
+
shacl2code/model.py,sha256=ESABSh4xHh5e5qfmeqQqLqdI1G1EWVsoLQMJ-sTBqgY,9077
|
|
6
|
+
shacl2code/urlcontext.py,sha256=cOlNXX1gdbhtmV4hnPTwEdMOPNn8iP0l3sH6pEr3pwQ,472
|
|
7
|
+
shacl2code/version.py,sha256=wvhEP9_tqxjTVjTcNW0lZOeHOeP3yZH_m-zQShg9NXI,19
|
|
8
|
+
shacl2code/lang/__init__.py,sha256=yy-nNrvlRM_O--MpBZ55OO2HoU6W9ICN6H_CE75JJnA,314
|
|
9
|
+
shacl2code/lang/common.py,sha256=pGftHigCCq_MEYny6EqtBnvLxnDEOfrljT0sIKw2YNs,3932
|
|
10
|
+
shacl2code/lang/jinja.py,sha256=MlxZUMoBSaD76ahwBSmGTBJilGiecdvA__WxUqfRZUI,607
|
|
11
|
+
shacl2code/lang/jsonschema.py,sha256=PfffswQ-QY0rV0aGR9qzVzJ-P78UKFvMh8yiuCA2RDo,1292
|
|
12
|
+
shacl2code/lang/lang.py,sha256=4TbEsktsHCrMwLlT4bkhh2fhMWkIxFdwhT1vwML8Rx8,296
|
|
13
|
+
shacl2code/lang/python.py,sha256=umiZx0p2J_vBaoTnPx3ldx4e5yKmQ0hGd7qN6lTVsy8,1414
|
|
14
|
+
shacl2code/lang/templates/jsonschema.j2,sha256=cuTGYvQ1pLj6aMnMwtwTVRxyQMRWdY7YOt-6kzJqCaw,11340
|
|
15
|
+
shacl2code/lang/templates/python.j2,sha256=oXQqL2KoRsf974l-jk4ImOCXYn49iyy5h1Svevkbalg,57340
|
|
16
|
+
shacl2code-0.0.11.dist-info/METADATA,sha256=XjDPB8Wn1q928vJardZ4P0JTRD-ACuRIdvrdP339eYA,6927
|
|
17
|
+
shacl2code-0.0.11.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
|
|
18
|
+
shacl2code-0.0.11.dist-info/entry_points.txt,sha256=xMo3DqLqOpZfEwDXLL3lqQZoXUXKUUhgn-ABzH4nOEg,47
|
|
19
|
+
shacl2code-0.0.11.dist-info/licenses/LICENSE,sha256=npUT32k_O0lU4UHJ8ZZ2_Ti0GR1oSbNGRzID9lSWkxs,1068
|
|
20
|
+
shacl2code-0.0.11.dist-info/RECORD,,
|