shacl2code 0.0.11__tar.gz → 0.0.13__tar.gz
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-0.0.11 → shacl2code-0.0.13}/.github/workflows/test.yaml +5 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/PKG-INFO +1 -2
- {shacl2code-0.0.11 → shacl2code-0.0.13}/pyproject.toml +0 -1
- shacl2code-0.0.13/src/shacl2code/context.py +277 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/common.py +9 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/templates/jsonschema.j2 +21 -5
- {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/templates/python.j2 +74 -26
- {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/model.py +1 -1
- shacl2code-0.0.13/src/shacl2code/version.py +1 -0
- shacl2code-0.0.13/tests/__init__.py +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/conftest.py +4 -5
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/context.j2 +2 -2
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/model/test-context.json +65 -7
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/model/test.ttl +24 -1
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/python/roundtrip.json +14 -2
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/expect/jsonschema/test-context.json +105 -12
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/expect/jsonschema/test.json +88 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/expect/python/test-context.py +107 -35
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/expect/python/test.py +85 -20
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/expect/raw/test-context.txt +4 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/expect/raw/test.txt +3 -1
- shacl2code-0.0.13/tests/http.py +74 -0
- shacl2code-0.0.13/tests/test_context.py +136 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/test_jsonschema.py +41 -5
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/test_model_source.py +60 -7
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/test_python.py +60 -5
- shacl2code-0.0.11/src/shacl2code/context.py +0 -167
- shacl2code-0.0.11/src/shacl2code/version.py +0 -1
- shacl2code-0.0.11/tests/test_context.py +0 -144
- {shacl2code-0.0.11 → shacl2code-0.0.13}/.flake8 +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/.github/workflows/coverage-generate.yaml +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/.github/workflows/coverage-report.yaml +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/.github/workflows/publish.yaml +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/.gitignore +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/LICENSE +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/README.md +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/__init__.py +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/__main__.py +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/__init__.py +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/jinja.py +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/jsonschema.py +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/lang.py +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/python.py +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/main.py +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/urlcontext.py +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/abort.j2 +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/bad-id.j2 +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/bad-node-kind.ttl +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/bad-pattern-class.ttl +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/bad-pattern-integer.ttl +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/bad-reference.jsonld +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/context-url.j2 +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/missing-range.ttl +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/python/bad-object-type-inline.json +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/python/bad-object-type-ref-after.json +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/python/bad-object-type-ref-before.json +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/python/links.json +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/raw.j2 +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/expect/make_expect.py +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/test_cli.py +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/test_common_jinja.py +0 -0
- {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/test_common_prefix.py +0 -0
|
@@ -22,10 +22,15 @@ jobs:
|
|
|
22
22
|
uses: actions/setup-python@v4
|
|
23
23
|
with:
|
|
24
24
|
python-version: ${{ matrix.python-version }}
|
|
25
|
+
- name: Setup Node
|
|
26
|
+
uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: 20
|
|
25
29
|
- name: Install dependencies
|
|
26
30
|
run: |
|
|
27
31
|
python -m pip install --upgrade pip
|
|
28
32
|
pip install build
|
|
33
|
+
npm install -g jsonld-cli
|
|
29
34
|
- name: Build package
|
|
30
35
|
run: |
|
|
31
36
|
python -m build
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: shacl2code
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.13
|
|
4
4
|
Summary: Convert SHACL model file to code bindings
|
|
5
5
|
Project-URL: Homepage, https://github.com/JPEWdev/shacl2code
|
|
6
6
|
Project-URL: Repository, https://github.com/JPEWdev/shacl2code.git
|
|
@@ -25,7 +25,6 @@ Requires-Dist: flake8>=7.0.0; extra == 'dev'
|
|
|
25
25
|
Requires-Dist: jsonschema>=4.21.1; extra == 'dev'
|
|
26
26
|
Requires-Dist: pyshacl>=0.25.0; extra == 'dev'
|
|
27
27
|
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
|
|
28
|
-
Requires-Dist: pytest-server-fixtures>=1.7; extra == 'dev'
|
|
29
28
|
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
30
29
|
Description-Content-Type: text/markdown
|
|
31
30
|
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Copyright (c) 2024 Joshua Watt
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def foreach_context(contexts):
|
|
10
|
+
for ctx in contexts:
|
|
11
|
+
for name, value in ctx.items():
|
|
12
|
+
yield name, value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Context(object):
|
|
16
|
+
def __init__(self, contexts=[]):
|
|
17
|
+
self.contexts = [c for c in contexts if c]
|
|
18
|
+
self.__vocabs = []
|
|
19
|
+
self.__expanded_iris = {}
|
|
20
|
+
self.__expanded_ids = {}
|
|
21
|
+
self.__expanded_vocabs = {}
|
|
22
|
+
self.__compacted_iris = {}
|
|
23
|
+
self.__compacted_ids = {}
|
|
24
|
+
self.__compacted_vocabs = {}
|
|
25
|
+
|
|
26
|
+
@contextmanager
|
|
27
|
+
def vocab_push(self, vocab):
|
|
28
|
+
if not vocab:
|
|
29
|
+
yield self
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
self.__vocabs.append(vocab)
|
|
33
|
+
try:
|
|
34
|
+
yield self
|
|
35
|
+
finally:
|
|
36
|
+
self.__vocabs.pop()
|
|
37
|
+
|
|
38
|
+
def __vocab_key(self):
|
|
39
|
+
if not self.__vocabs:
|
|
40
|
+
return ""
|
|
41
|
+
|
|
42
|
+
return self.__vocabs[-1]
|
|
43
|
+
|
|
44
|
+
def __get_vocab_contexts(self):
|
|
45
|
+
contexts = []
|
|
46
|
+
|
|
47
|
+
for v in self.__vocabs:
|
|
48
|
+
for name, value in foreach_context(self.contexts):
|
|
49
|
+
if (
|
|
50
|
+
isinstance(value, dict)
|
|
51
|
+
and value["@type"] == "@vocab"
|
|
52
|
+
and v == self.expand_iri(value["@id"])
|
|
53
|
+
):
|
|
54
|
+
if "@context" in value:
|
|
55
|
+
contexts.insert(0, value["@context"])
|
|
56
|
+
|
|
57
|
+
contexts.extend(self.contexts)
|
|
58
|
+
|
|
59
|
+
return contexts
|
|
60
|
+
|
|
61
|
+
def __choose_possible(
|
|
62
|
+
self,
|
|
63
|
+
term,
|
|
64
|
+
default,
|
|
65
|
+
contexts,
|
|
66
|
+
*,
|
|
67
|
+
vocab=False,
|
|
68
|
+
base=False,
|
|
69
|
+
exact=False,
|
|
70
|
+
prefix=False,
|
|
71
|
+
):
|
|
72
|
+
def remove_prefix(_id, value):
|
|
73
|
+
expanded_id = self.expand_iri(_id)
|
|
74
|
+
expanded_value = self.expand_iri(value)
|
|
75
|
+
|
|
76
|
+
possible = set()
|
|
77
|
+
if expanded_id.startswith(expanded_value):
|
|
78
|
+
tmp_id = _id[len(expanded_value) :]
|
|
79
|
+
possible.add(tmp_id)
|
|
80
|
+
return possible
|
|
81
|
+
|
|
82
|
+
def helper(term):
|
|
83
|
+
possible = set()
|
|
84
|
+
for name, value in foreach_context(contexts):
|
|
85
|
+
if name == "@vocab":
|
|
86
|
+
if vocab:
|
|
87
|
+
possible |= remove_prefix(term, value)
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
if name == "@base":
|
|
91
|
+
if base:
|
|
92
|
+
possible |= remove_prefix(term, value)
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
if isinstance(value, dict):
|
|
96
|
+
value = value["@id"]
|
|
97
|
+
|
|
98
|
+
if term == self.expand_iri(value):
|
|
99
|
+
if exact and name not in possible:
|
|
100
|
+
possible.add(name)
|
|
101
|
+
possible |= helper(name)
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
if not prefix:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
if term.startswith(value) and value.endswith("/"):
|
|
108
|
+
tmp_id = name + ":" + term[len(value) :].lstrip("/")
|
|
109
|
+
if tmp_id not in possible:
|
|
110
|
+
possible.add(tmp_id)
|
|
111
|
+
possible |= helper(tmp_id)
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
if term.startswith(value + ":") and self.expand_iri(value).endswith(
|
|
115
|
+
"/"
|
|
116
|
+
):
|
|
117
|
+
tmp_id = name + term[len(value) :]
|
|
118
|
+
if tmp_id not in possible:
|
|
119
|
+
possible.add(tmp_id)
|
|
120
|
+
possible |= helper(tmp_id)
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
return possible
|
|
124
|
+
|
|
125
|
+
possible = helper(term)
|
|
126
|
+
|
|
127
|
+
if not possible:
|
|
128
|
+
return default
|
|
129
|
+
|
|
130
|
+
# To select from the possible identifiers, choose the one that has the
|
|
131
|
+
# least context (fewest ":"), then the shortest, and finally
|
|
132
|
+
# alphabetically
|
|
133
|
+
possible = list(possible)
|
|
134
|
+
possible.sort(key=lambda p: (p.count(":"), len(p), p))
|
|
135
|
+
return possible[0]
|
|
136
|
+
|
|
137
|
+
def compact_iri(self, iri):
|
|
138
|
+
if iri not in self.__compacted_iris:
|
|
139
|
+
self.__compacted_iris[iri] = self.__choose_possible(
|
|
140
|
+
iri,
|
|
141
|
+
iri,
|
|
142
|
+
self.contexts,
|
|
143
|
+
exact=True,
|
|
144
|
+
prefix=True,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return self.__compacted_iris[iri]
|
|
148
|
+
|
|
149
|
+
def compact_id(self, _id):
|
|
150
|
+
if ":" not in _id:
|
|
151
|
+
return _id
|
|
152
|
+
|
|
153
|
+
if _id not in self.__compacted_ids:
|
|
154
|
+
self.__compacted_ids[_id] = self.__choose_possible(
|
|
155
|
+
_id,
|
|
156
|
+
_id,
|
|
157
|
+
self.contexts,
|
|
158
|
+
base=True,
|
|
159
|
+
prefix=True,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return self.__compacted_ids[_id]
|
|
163
|
+
|
|
164
|
+
def compact_vocab(self, term, vocab=None):
|
|
165
|
+
with self.vocab_push(vocab):
|
|
166
|
+
v = self.__vocab_key()
|
|
167
|
+
if v in self.__compacted_vocabs and term in self.__compacted_vocabs[v]:
|
|
168
|
+
return self.__compacted_vocabs[v][term]
|
|
169
|
+
|
|
170
|
+
compact = self.__choose_possible(
|
|
171
|
+
term,
|
|
172
|
+
None,
|
|
173
|
+
self.__get_vocab_contexts(),
|
|
174
|
+
vocab=True,
|
|
175
|
+
exact=True,
|
|
176
|
+
)
|
|
177
|
+
if compact is not None:
|
|
178
|
+
self.__compacted_vocabs.setdefault(v, {})[term] = self.compact_id(
|
|
179
|
+
compact
|
|
180
|
+
)
|
|
181
|
+
return compact
|
|
182
|
+
|
|
183
|
+
# If unable to compact with a vocabulary, compact as an ID
|
|
184
|
+
return self.compact_id(term)
|
|
185
|
+
|
|
186
|
+
def expand_iri(self, iri):
|
|
187
|
+
if iri not in self.__expanded_iris:
|
|
188
|
+
self.__expanded_iris[iri] = self.__expand(
|
|
189
|
+
iri,
|
|
190
|
+
self.contexts,
|
|
191
|
+
exact=True,
|
|
192
|
+
prefix=True,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return self.__expanded_iris[iri]
|
|
196
|
+
|
|
197
|
+
def expand_id(self, _id):
|
|
198
|
+
if _id not in self.__expanded_ids:
|
|
199
|
+
self.__expanded_ids[_id] = self.__expand(
|
|
200
|
+
_id,
|
|
201
|
+
self.contexts,
|
|
202
|
+
base=True,
|
|
203
|
+
prefix=True,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return self.__expanded_ids[_id]
|
|
207
|
+
|
|
208
|
+
def expand_vocab(self, term, vocab=None):
|
|
209
|
+
with self.vocab_push(vocab):
|
|
210
|
+
v = self.__vocab_key()
|
|
211
|
+
if v not in self.__expanded_vocabs or term not in self.__expanded_vocabs[v]:
|
|
212
|
+
value = self.__expand(
|
|
213
|
+
term,
|
|
214
|
+
self.__get_vocab_contexts(),
|
|
215
|
+
vocab=True,
|
|
216
|
+
exact=True,
|
|
217
|
+
)
|
|
218
|
+
self.__expanded_vocabs.setdefault(v, {})[term] = self.expand_id(value)
|
|
219
|
+
|
|
220
|
+
return self.__expanded_vocabs[v][term]
|
|
221
|
+
|
|
222
|
+
def __expand(
|
|
223
|
+
self,
|
|
224
|
+
term,
|
|
225
|
+
contexts,
|
|
226
|
+
*,
|
|
227
|
+
base=False,
|
|
228
|
+
exact=False,
|
|
229
|
+
prefix=False,
|
|
230
|
+
vocab=False,
|
|
231
|
+
):
|
|
232
|
+
def helper(term):
|
|
233
|
+
vocabs = []
|
|
234
|
+
bases = []
|
|
235
|
+
prefixes = []
|
|
236
|
+
exacts = []
|
|
237
|
+
is_short = not re.match(r"[^:]+:", term)
|
|
238
|
+
|
|
239
|
+
for name, value in foreach_context(contexts):
|
|
240
|
+
if name == "@vocab":
|
|
241
|
+
if vocab and is_short:
|
|
242
|
+
vocabs.append(helper(value))
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
if name == "@base":
|
|
246
|
+
if base and is_short:
|
|
247
|
+
bases.append(value)
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
if isinstance(value, dict):
|
|
251
|
+
value = value["@id"]
|
|
252
|
+
|
|
253
|
+
if term == name:
|
|
254
|
+
if exact:
|
|
255
|
+
exacts.append(helper(value))
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
if prefix:
|
|
259
|
+
prefixes.append(name)
|
|
260
|
+
|
|
261
|
+
for e in exacts:
|
|
262
|
+
return e
|
|
263
|
+
|
|
264
|
+
if ":" in term:
|
|
265
|
+
p, suffix = term.split(":", 1)
|
|
266
|
+
for name in prefixes:
|
|
267
|
+
if p == name:
|
|
268
|
+
p = self.expand_iri(p)
|
|
269
|
+
if p.endswith("/"):
|
|
270
|
+
return p + suffix
|
|
271
|
+
|
|
272
|
+
for value in vocabs + bases:
|
|
273
|
+
return value + term
|
|
274
|
+
|
|
275
|
+
return term
|
|
276
|
+
|
|
277
|
+
return helper(term)
|
|
@@ -114,6 +114,14 @@ class BasicJinjaRender(object):
|
|
|
114
114
|
d.sort()
|
|
115
115
|
return d
|
|
116
116
|
|
|
117
|
+
def get_all_named_individuals(cls):
|
|
118
|
+
ni = set(i._id for i in cls.named_individuals)
|
|
119
|
+
|
|
120
|
+
for d in get_all_derived(cls):
|
|
121
|
+
ni |= set(i._id for i in classes.get(d).named_individuals)
|
|
122
|
+
|
|
123
|
+
return ni
|
|
124
|
+
|
|
117
125
|
classes = ObjectList(model.classes)
|
|
118
126
|
concrete_classes = ObjectList(
|
|
119
127
|
list(c for c in model.classes if not c.is_abstract)
|
|
@@ -132,6 +140,7 @@ class BasicJinjaRender(object):
|
|
|
132
140
|
|
|
133
141
|
env = {
|
|
134
142
|
"get_all_derived": get_all_derived,
|
|
143
|
+
"get_all_named_individuals": get_all_named_individuals,
|
|
135
144
|
**self.get_extra_env(),
|
|
136
145
|
}
|
|
137
146
|
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
{#- Classes are divided into 2 parts. The properties are separated into a separate object #}
|
|
60
60
|
{#- so that a object can references the properties of its parent without needing the const #}
|
|
61
61
|
{#- @type tag #}
|
|
62
|
-
{%- if not class.is_abstract %}
|
|
62
|
+
{%- if not class.is_abstract or class.is_extensible %}
|
|
63
63
|
"{{ varname(*class.clsname) }}": {
|
|
64
64
|
"allOf": [
|
|
65
65
|
{
|
|
@@ -69,13 +69,21 @@
|
|
|
69
69
|
{%- endif %}
|
|
70
70
|
"properties": {
|
|
71
71
|
"{{ class.id_property or "@id" }}": { "$ref": "#/$defs/{{ class.node_kind.split("#")[-1] }}" },
|
|
72
|
-
"{{ context.
|
|
72
|
+
"{{ context.compact_iri("@type") }}": {
|
|
73
|
+
{#- Abstract Extensible classes are weird; any type _except_ the specific class type is allowed #}
|
|
74
|
+
{%- if class.is_abstract and class.is_extensible %}
|
|
75
|
+
"allOf": [
|
|
76
|
+
{ "$ref": "#/$defs/IRI" },
|
|
77
|
+
{ "not": { "const": "{{ context.compact_vocab(class._id) }}" } }
|
|
78
|
+
]
|
|
79
|
+
{%- else %}
|
|
73
80
|
"oneOf": [
|
|
74
81
|
{%- if class.is_extensible %}
|
|
75
82
|
{ "$ref": "#/$defs/IRI" },
|
|
76
83
|
{%- endif %}
|
|
77
84
|
{ "const": "{{ context.compact_vocab(class._id) }}" }
|
|
78
85
|
]
|
|
86
|
+
{%- endif %}
|
|
79
87
|
}
|
|
80
88
|
}{%- if class.node_kind == SH.IRI %},
|
|
81
89
|
"required": ["{{ class.id_property or "@id" }}"]
|
|
@@ -86,9 +94,14 @@
|
|
|
86
94
|
},
|
|
87
95
|
{%- endif %}
|
|
88
96
|
"{{ varname(*class.clsname) }}_derived": {
|
|
89
|
-
{%- set ns = namespace(
|
|
97
|
+
{%- set ns = namespace(json_refs=[], named_individuals=[]) %}
|
|
90
98
|
{%- for d in get_all_derived(class) + [class._id] %}
|
|
91
99
|
{%- set ns.json_refs = ns.json_refs + ["#/$defs/" + varname(*classes.get(d).clsname)] %}
|
|
100
|
+
{%- for n in classes.get(d).named_individuals %}
|
|
101
|
+
{%- if context.compact_iri(n._id) != n._id %}
|
|
102
|
+
{%- set ns.named_individuals = ns.named_individuals + [context.compact_iri(n._id)] %}
|
|
103
|
+
{%- endif %}
|
|
104
|
+
{%- endfor %}
|
|
92
105
|
{%- endfor %}
|
|
93
106
|
"anyOf": [
|
|
94
107
|
{%- if ns.json_refs %}
|
|
@@ -104,6 +117,9 @@
|
|
|
104
117
|
]
|
|
105
118
|
},
|
|
106
119
|
{%- endif %}
|
|
120
|
+
{%- for n in ns.named_individuals %}
|
|
121
|
+
{ "const": "{{ n }}" },
|
|
122
|
+
{%- endfor %}
|
|
107
123
|
{ "$ref": "#/$defs/BlankNodeOrIRI" }
|
|
108
124
|
]
|
|
109
125
|
},
|
|
@@ -251,7 +267,7 @@
|
|
|
251
267
|
"SHACLClass": {
|
|
252
268
|
"type": "object",
|
|
253
269
|
"properties": {
|
|
254
|
-
"{{ context.
|
|
270
|
+
"{{ context.compact_iri("@type") }}": {
|
|
255
271
|
"oneOf": [
|
|
256
272
|
{ "$ref": "#/$defs/IRI" },
|
|
257
273
|
{
|
|
@@ -264,7 +280,7 @@
|
|
|
264
280
|
]
|
|
265
281
|
}
|
|
266
282
|
},
|
|
267
|
-
"required": ["{{ context.
|
|
283
|
+
"required": ["{{ context.compact_iri("@type") }}"]
|
|
268
284
|
},
|
|
269
285
|
"AnyClass": {
|
|
270
286
|
"anyOf": [
|
|
@@ -220,13 +220,34 @@ class FloatProp(Property):
|
|
|
220
220
|
return decoder.read_float()
|
|
221
221
|
|
|
222
222
|
|
|
223
|
-
class
|
|
223
|
+
class IRIProp(Property):
|
|
224
|
+
def __init__(self, context=[], *, pattern=None):
|
|
225
|
+
super().__init__(pattern=pattern)
|
|
226
|
+
self.context = context
|
|
227
|
+
|
|
228
|
+
def compact(self, value):
|
|
229
|
+
for iri, compact in self.context:
|
|
230
|
+
if value == iri:
|
|
231
|
+
return compact
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
def expand(self, value):
|
|
235
|
+
for iri, compact in self.context:
|
|
236
|
+
if value == compact:
|
|
237
|
+
return iri
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
def iri_values(self):
|
|
241
|
+
return (iri for iri, _ in self.context)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class ObjectProp(IRIProp):
|
|
224
245
|
"""
|
|
225
246
|
A scalar SHACL object property of a SHACL object
|
|
226
247
|
"""
|
|
227
248
|
|
|
228
|
-
def __init__(self, cls, required):
|
|
229
|
-
super().__init__()
|
|
249
|
+
def __init__(self, cls, required, context=[]):
|
|
250
|
+
super().__init__(context)
|
|
230
251
|
self.cls = cls
|
|
231
252
|
self.required = required
|
|
232
253
|
|
|
@@ -264,7 +285,7 @@ class ObjectProp(Property):
|
|
|
264
285
|
raise ValueError("Object cannot be None")
|
|
265
286
|
|
|
266
287
|
if isinstance(value, str):
|
|
267
|
-
encoder.write_iri(value)
|
|
288
|
+
encoder.write_iri(value, self.compact(value))
|
|
268
289
|
return
|
|
269
290
|
|
|
270
291
|
return value.encode(encoder, state)
|
|
@@ -274,6 +295,8 @@ class ObjectProp(Property):
|
|
|
274
295
|
if iri is None:
|
|
275
296
|
return self.cls.decode(decoder, objectset=objectset)
|
|
276
297
|
|
|
298
|
+
iri = self.expand(iri) or iri
|
|
299
|
+
|
|
277
300
|
if objectset is None:
|
|
278
301
|
return iri
|
|
279
302
|
|
|
@@ -442,36 +465,27 @@ class ListProp(Property):
|
|
|
442
465
|
return ListProxy(self.prop, data=data)
|
|
443
466
|
|
|
444
467
|
|
|
445
|
-
class EnumProp(
|
|
468
|
+
class EnumProp(IRIProp):
|
|
446
469
|
VALID_TYPES = str
|
|
447
470
|
|
|
448
471
|
def __init__(self, values, *, pattern=None):
|
|
449
|
-
super().__init__(pattern=pattern)
|
|
450
|
-
self.values = values
|
|
472
|
+
super().__init__(values, pattern=pattern)
|
|
451
473
|
|
|
452
474
|
def validate(self, value):
|
|
453
475
|
super().validate(value)
|
|
454
476
|
|
|
455
|
-
valid_values =
|
|
477
|
+
valid_values = self.iri_values()
|
|
456
478
|
if value not in valid_values:
|
|
457
479
|
raise ValueError(
|
|
458
480
|
f"'{value}' is not a valid value. Choose one of {' '.join(valid_values)}"
|
|
459
481
|
)
|
|
460
482
|
|
|
461
483
|
def encode(self, encoder, value, state):
|
|
462
|
-
|
|
463
|
-
if iri == value:
|
|
464
|
-
encoder.write_enum(value, self, compact)
|
|
465
|
-
return
|
|
466
|
-
|
|
467
|
-
encoder.write_enum(value, self)
|
|
484
|
+
encoder.write_enum(value, self, self.compact(value))
|
|
468
485
|
|
|
469
486
|
def decode(self, decoder, *, objectset=None):
|
|
470
487
|
v = decoder.read_enum(self)
|
|
471
|
-
|
|
472
|
-
if v == compact:
|
|
473
|
-
return iri
|
|
474
|
-
return v
|
|
488
|
+
return self.expand(v) or v
|
|
475
489
|
|
|
476
490
|
|
|
477
491
|
class NodeKind(Enum):
|
|
@@ -506,6 +520,8 @@ def register(type_iri, *, compact_type=None, abstract=False):
|
|
|
506
520
|
SHACLObject.CLASSES[key] = c
|
|
507
521
|
|
|
508
522
|
def decorator(c):
|
|
523
|
+
global NAMED_INDIVIDUALS
|
|
524
|
+
|
|
509
525
|
assert issubclass(
|
|
510
526
|
c, SHACLObject
|
|
511
527
|
), f"{c.__name__} is not derived from SHACLObject"
|
|
@@ -518,6 +534,8 @@ def register(type_iri, *, compact_type=None, abstract=False):
|
|
|
518
534
|
if compact_type:
|
|
519
535
|
add_class(compact_type, c)
|
|
520
536
|
|
|
537
|
+
NAMED_INDIVIDUALS |= set(c.NAMED_INDIVIDUALS.values())
|
|
538
|
+
|
|
521
539
|
# Registration is deferred until the first instance of class is created
|
|
522
540
|
# so that it has access to any other defined class
|
|
523
541
|
c._NEEDS_REG = True
|
|
@@ -527,6 +545,7 @@ def register(type_iri, *, compact_type=None, abstract=False):
|
|
|
527
545
|
|
|
528
546
|
|
|
529
547
|
register_lock = threading.Lock()
|
|
548
|
+
NAMED_INDIVIDUALS = set()
|
|
530
549
|
|
|
531
550
|
|
|
532
551
|
@functools.total_ordering
|
|
@@ -537,7 +556,7 @@ class SHACLObject(object):
|
|
|
537
556
|
IS_ABSTRACT = True
|
|
538
557
|
|
|
539
558
|
def __init__(self, **kwargs):
|
|
540
|
-
if self.
|
|
559
|
+
if self._is_abstract():
|
|
541
560
|
raise NotImplementedError(
|
|
542
561
|
f"{self.__class__.__name__} is abstract and cannot be implemented"
|
|
543
562
|
)
|
|
@@ -559,6 +578,9 @@ class SHACLObject(object):
|
|
|
559
578
|
for k, v in kwargs.items():
|
|
560
579
|
setattr(self, k, v)
|
|
561
580
|
|
|
581
|
+
def _is_abstract(self):
|
|
582
|
+
return self.__class__.IS_ABSTRACT
|
|
583
|
+
|
|
562
584
|
@classmethod
|
|
563
585
|
def _register_props(cls):
|
|
564
586
|
cls._add_property("_id", StringProp(), iri="@id")
|
|
@@ -875,11 +897,20 @@ class SHACLExtensibleObject(object):
|
|
|
875
897
|
CLOSED = False
|
|
876
898
|
|
|
877
899
|
def __init__(self, typ=None, **kwargs):
|
|
878
|
-
super().__init__(**kwargs)
|
|
879
900
|
if typ:
|
|
880
901
|
self.__dict__["_obj_TYPE"] = (typ, None)
|
|
881
902
|
else:
|
|
882
903
|
self.__dict__["_obj_TYPE"] = (self._OBJ_TYPE, self._OBJ_COMPACT_TYPE)
|
|
904
|
+
super().__init__(**kwargs)
|
|
905
|
+
|
|
906
|
+
def _is_abstract(self):
|
|
907
|
+
# Unknown classes are assumed to not be abstract so that they can be
|
|
908
|
+
# deserialized
|
|
909
|
+
typ = self.__dict__["_obj_TYPE"][0]
|
|
910
|
+
if typ in self.__class__.CLASSES:
|
|
911
|
+
return self.__class__.CLASSES[typ].IS_ABSTRACT
|
|
912
|
+
|
|
913
|
+
return False
|
|
883
914
|
|
|
884
915
|
@classmethod
|
|
885
916
|
def _make_object(cls, typ):
|
|
@@ -1079,6 +1110,8 @@ class SHACLObjectSet(object):
|
|
|
1079
1110
|
return self._link()
|
|
1080
1111
|
|
|
1081
1112
|
def _link(self):
|
|
1113
|
+
global NAMED_INDIVIDUALS
|
|
1114
|
+
|
|
1082
1115
|
self.missing_ids = set()
|
|
1083
1116
|
visited = set()
|
|
1084
1117
|
|
|
@@ -1101,6 +1134,9 @@ class SHACLObjectSet(object):
|
|
|
1101
1134
|
obj_by_id[_id] = obj
|
|
1102
1135
|
self.obj_by_id = obj_by_id
|
|
1103
1136
|
|
|
1137
|
+
# Named individuals aren't considered missing
|
|
1138
|
+
self.missing_ids -= NAMED_INDIVIDUALS
|
|
1139
|
+
|
|
1104
1140
|
return self.missing_ids
|
|
1105
1141
|
|
|
1106
1142
|
def find_by_id(self, _id, default=None):
|
|
@@ -1493,14 +1529,14 @@ class JSONLDDecoder(Decoder):
|
|
|
1493
1529
|
|
|
1494
1530
|
def object_keys(self):
|
|
1495
1531
|
for key in self.data.keys():
|
|
1496
|
-
if key in ("@type", "{{ context.
|
|
1532
|
+
if key in ("@type", "{{ context.compact_iri('@type') }}"):
|
|
1497
1533
|
continue
|
|
1498
1534
|
if self.root and key == "@context":
|
|
1499
1535
|
continue
|
|
1500
1536
|
yield key
|
|
1501
1537
|
|
|
1502
1538
|
def read_object(self):
|
|
1503
|
-
typ = self.__get_value("@type", "{{ context.
|
|
1539
|
+
typ = self.__get_value("@type", "{{ context.compact_iri('@type') }}")
|
|
1504
1540
|
if typ is not None:
|
|
1505
1541
|
return typ, self
|
|
1506
1542
|
|
|
@@ -1692,7 +1728,7 @@ class JSONLDEncoder(Encoder):
|
|
|
1692
1728
|
@contextmanager
|
|
1693
1729
|
def write_object(self, o, _id, needs_id):
|
|
1694
1730
|
self.data = {
|
|
1695
|
-
"{{ context.
|
|
1731
|
+
"{{ context.compact_iri('@type') }}": o.COMPACT_TYPE or o.TYPE,
|
|
1696
1732
|
}
|
|
1697
1733
|
if needs_id:
|
|
1698
1734
|
self.data[o.ID_ALIAS or "@id"] = _id
|
|
@@ -1816,7 +1852,7 @@ class JSONLDInlineEncoder(Encoder):
|
|
|
1816
1852
|
self._write_comma()
|
|
1817
1853
|
|
|
1818
1854
|
self.write("{")
|
|
1819
|
-
self.write_string("{{ context.
|
|
1855
|
+
self.write_string("{{ context.compact_iri('@type') }}")
|
|
1820
1856
|
self.write(":")
|
|
1821
1857
|
self.write_string(o.COMPACT_TYPE or o.TYPE)
|
|
1822
1858
|
self.comma = True
|
|
@@ -1946,7 +1982,7 @@ CONTEXT_URLS = [
|
|
|
1946
1982
|
#{{ (" " + l).rstrip() }}
|
|
1947
1983
|
{%- endfor %}
|
|
1948
1984
|
{%- endif %}
|
|
1949
|
-
@register("{{ class._id }}"{%- if context.
|
|
1985
|
+
@register("{{ class._id }}"{%- if context.compact_iri(class._id) != class._id %}, compact_type="{{ context.compact_iri(class._id) }}"{%- endif %}, abstract={{ class.is_abstract }})
|
|
1950
1986
|
class {{ varname(*class.clsname) }}(
|
|
1951
1987
|
{%- if class.is_extensible -%}
|
|
1952
1988
|
SHACLExtensibleObject{{", "}}
|
|
@@ -1997,7 +2033,19 @@ class {{ varname(*class.clsname) }}(
|
|
|
1997
2033
|
{%- endfor %}
|
|
1998
2034
|
])
|
|
1999
2035
|
{%- elif prop.class_id -%}
|
|
2000
|
-
|
|
2036
|
+
{%- set ctx = [] %}
|
|
2037
|
+
{%- for value in get_all_named_individuals(classes.get(prop.class_id)) %}
|
|
2038
|
+
{%- if context.compact_vocab(value, prop.path) != value %}
|
|
2039
|
+
{{- ctx.append((value, context.compact_vocab(value, prop.path))) or "" }}
|
|
2040
|
+
{%- endif %}
|
|
2041
|
+
{%- endfor -%}
|
|
2042
|
+
ObjectProp({{ varname(*classes.get(prop.class_id).clsname) }}, {% if prop.min_count and not is_list %}True{% else %}False{% endif %}{% if ctx %}, context=[
|
|
2043
|
+
{%- for value, compact in ctx %}
|
|
2044
|
+
("{{ value }}", "{{ compact }}"),
|
|
2045
|
+
{%- endfor %}
|
|
2046
|
+
],
|
|
2047
|
+
{%- endif -%}
|
|
2048
|
+
)
|
|
2001
2049
|
{%- else -%}
|
|
2002
2050
|
{% if not prop.datatype in DATATYPE_CLASSES -%}
|
|
2003
2051
|
{{ abort("Unknown data type " + prop.datatype) -}}
|
|
@@ -280,7 +280,7 @@ class Model(object):
|
|
|
280
280
|
"""
|
|
281
281
|
_id = str(_id)
|
|
282
282
|
if _id not in self.compact_ids:
|
|
283
|
-
self.compact_ids[_id] = self.context.
|
|
283
|
+
self.compact_ids[_id] = self.context.compact_iri(_id)
|
|
284
284
|
|
|
285
285
|
if self.compact_ids[_id] == _id and fallback is not None:
|
|
286
286
|
return fallback
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.0.13"
|
|
File without changes
|