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
|
@@ -0,0 +1,2054 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Generated Python bindings from a SHACL model
|
|
4
|
+
#
|
|
5
|
+
# {{ disclaimer }}
|
|
6
|
+
#
|
|
7
|
+
# SPDX-License-Identifier: MIT
|
|
8
|
+
|
|
9
|
+
import functools
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from contextlib import contextmanager
|
|
17
|
+
from datetime import datetime, timezone, timedelta
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from abc import ABC, abstractmethod
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_type(obj, types):
|
|
23
|
+
if not isinstance(obj, types):
|
|
24
|
+
if isinstance(types, (list, tuple)):
|
|
25
|
+
raise TypeError(
|
|
26
|
+
f"Value must be one of type: {', '.join(t.__name__ for t in types)}. Got {type(obj)}"
|
|
27
|
+
)
|
|
28
|
+
raise TypeError(f"Value must be of type {types.__name__}. Got {type(obj)}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Property(ABC):
|
|
32
|
+
"""
|
|
33
|
+
A generic SHACL object property. The different types will derive from this
|
|
34
|
+
class
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, *, pattern=None):
|
|
38
|
+
self.pattern = pattern
|
|
39
|
+
|
|
40
|
+
def init(self):
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
def validate(self, value):
|
|
44
|
+
check_type(value, self.VALID_TYPES)
|
|
45
|
+
if self.pattern is not None and not re.search(
|
|
46
|
+
self.pattern, self.to_string(value)
|
|
47
|
+
):
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Value is not correctly formatted. Got '{self.to_string(value)}'"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def set(self, value):
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
def check_min_count(self, value, min_count):
|
|
56
|
+
return min_count == 1
|
|
57
|
+
|
|
58
|
+
def check_max_count(self, value, max_count):
|
|
59
|
+
return max_count == 1
|
|
60
|
+
|
|
61
|
+
def elide(self, value):
|
|
62
|
+
return value is None
|
|
63
|
+
|
|
64
|
+
def walk(self, value, callback, path):
|
|
65
|
+
callback(value, path)
|
|
66
|
+
|
|
67
|
+
def iter_objects(self, value, recursive, visited):
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
def link_prop(self, value, objectset, missing, visited):
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
def to_string(self, value):
|
|
74
|
+
return str(value)
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def encode(self, encoder, value, state):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def decode(self, decoder, *, objectset=None):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class StringProp(Property):
|
|
86
|
+
"""
|
|
87
|
+
A scalar string property for an SHACL object
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
VALID_TYPES = str
|
|
91
|
+
|
|
92
|
+
def set(self, value):
|
|
93
|
+
return str(value)
|
|
94
|
+
|
|
95
|
+
def encode(self, encoder, value, state):
|
|
96
|
+
encoder.write_string(value)
|
|
97
|
+
|
|
98
|
+
def decode(self, decoder, *, objectset=None):
|
|
99
|
+
return decoder.read_string()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class AnyURIProp(StringProp):
|
|
103
|
+
def encode(self, encoder, value, state):
|
|
104
|
+
encoder.write_iri(value)
|
|
105
|
+
|
|
106
|
+
def decode(self, decoder, *, objectset=None):
|
|
107
|
+
return decoder.read_iri()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class DateTimeProp(Property):
|
|
111
|
+
"""
|
|
112
|
+
A Date/Time Object with optional timezone
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
VALID_TYPES = datetime
|
|
116
|
+
UTC_FORMAT_STR = "%Y-%m-%dT%H:%M:%SZ"
|
|
117
|
+
REGEX = r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})?$"
|
|
118
|
+
|
|
119
|
+
def set(self, value):
|
|
120
|
+
return self._normalize(value)
|
|
121
|
+
|
|
122
|
+
def encode(self, encoder, value, state):
|
|
123
|
+
encoder.write_datetime(self.to_string(value))
|
|
124
|
+
|
|
125
|
+
def decode(self, decoder, *, objectset=None):
|
|
126
|
+
s = decoder.read_datetime()
|
|
127
|
+
if s is None:
|
|
128
|
+
return None
|
|
129
|
+
v = self.from_string(s)
|
|
130
|
+
return self._normalize(v)
|
|
131
|
+
|
|
132
|
+
def _normalize(self, value):
|
|
133
|
+
if value.utcoffset() is None:
|
|
134
|
+
value = value.astimezone()
|
|
135
|
+
offset = value.utcoffset()
|
|
136
|
+
if offset % timedelta(minutes=1):
|
|
137
|
+
offset = offset - (offset % timedelta(minutes=1))
|
|
138
|
+
value = value.replace(tzinfo=timezone(offset))
|
|
139
|
+
value = value.replace(microsecond=0)
|
|
140
|
+
return value
|
|
141
|
+
|
|
142
|
+
def to_string(self, value):
|
|
143
|
+
value = self._normalize(value)
|
|
144
|
+
if value.tzinfo == timezone.utc:
|
|
145
|
+
return value.strftime(self.UTC_FORMAT_STR)
|
|
146
|
+
return value.isoformat()
|
|
147
|
+
|
|
148
|
+
def from_string(self, value):
|
|
149
|
+
if not re.match(self.REGEX, value):
|
|
150
|
+
raise ValueError(f"'{value}' is not a correctly formatted datetime")
|
|
151
|
+
if "Z" in value:
|
|
152
|
+
d = datetime(
|
|
153
|
+
*(time.strptime(value, self.UTC_FORMAT_STR)[0:6]),
|
|
154
|
+
tzinfo=timezone.utc,
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
d = datetime.fromisoformat(value)
|
|
158
|
+
|
|
159
|
+
return self._normalize(d)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class DateTimeStampProp(DateTimeProp):
|
|
163
|
+
"""
|
|
164
|
+
A Date/Time Object with required timestamp
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
REGEX = r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class IntegerProp(Property):
|
|
171
|
+
VALID_TYPES = int
|
|
172
|
+
|
|
173
|
+
def set(self, value):
|
|
174
|
+
return int(value)
|
|
175
|
+
|
|
176
|
+
def encode(self, encoder, value, state):
|
|
177
|
+
encoder.write_integer(value)
|
|
178
|
+
|
|
179
|
+
def decode(self, decoder, *, objectset=None):
|
|
180
|
+
return decoder.read_integer()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class PositiveIntegerProp(IntegerProp):
|
|
184
|
+
def validate(self, value):
|
|
185
|
+
super().validate(value)
|
|
186
|
+
if value < 1:
|
|
187
|
+
raise ValueError(f"Value must be >=1. Got {value}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class NonNegativeIntegerProp(IntegerProp):
|
|
191
|
+
def validate(self, value):
|
|
192
|
+
super().validate(value)
|
|
193
|
+
if value < 0:
|
|
194
|
+
raise ValueError(f"Value must be >= 0. Got {value}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class BooleanProp(Property):
|
|
198
|
+
VALID_TYPES = bool
|
|
199
|
+
|
|
200
|
+
def set(self, value):
|
|
201
|
+
return bool(value)
|
|
202
|
+
|
|
203
|
+
def encode(self, encoder, value, state):
|
|
204
|
+
encoder.write_bool(value)
|
|
205
|
+
|
|
206
|
+
def decode(self, decoder, *, objectset=None):
|
|
207
|
+
return decoder.read_bool()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class FloatProp(Property):
|
|
211
|
+
VALID_TYPES = (float, int)
|
|
212
|
+
|
|
213
|
+
def set(self, value):
|
|
214
|
+
return float(value)
|
|
215
|
+
|
|
216
|
+
def encode(self, encoder, value, state):
|
|
217
|
+
encoder.write_float(value)
|
|
218
|
+
|
|
219
|
+
def decode(self, decoder, *, objectset=None):
|
|
220
|
+
return decoder.read_float()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class ObjectProp(Property):
|
|
224
|
+
"""
|
|
225
|
+
A scalar SHACL object property of a SHACL object
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
def __init__(self, cls, required):
|
|
229
|
+
super().__init__()
|
|
230
|
+
self.cls = cls
|
|
231
|
+
self.required = required
|
|
232
|
+
|
|
233
|
+
def init(self):
|
|
234
|
+
if self.required and not self.cls.IS_ABSTRACT:
|
|
235
|
+
return self.cls()
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
def validate(self, value):
|
|
239
|
+
check_type(value, (self.cls, str))
|
|
240
|
+
|
|
241
|
+
def walk(self, value, callback, path):
|
|
242
|
+
if value is None:
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
if not isinstance(value, str):
|
|
246
|
+
value.walk(callback, path)
|
|
247
|
+
else:
|
|
248
|
+
callback(value, path)
|
|
249
|
+
|
|
250
|
+
def iter_objects(self, value, recursive, visited):
|
|
251
|
+
if value is None or isinstance(value, str):
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
if value not in visited:
|
|
255
|
+
visited.add(value)
|
|
256
|
+
yield value
|
|
257
|
+
|
|
258
|
+
if recursive:
|
|
259
|
+
for c in value.iter_objects(recursive=True, visited=visited):
|
|
260
|
+
yield c
|
|
261
|
+
|
|
262
|
+
def encode(self, encoder, value, state):
|
|
263
|
+
if value is None:
|
|
264
|
+
raise ValueError("Object cannot be None")
|
|
265
|
+
|
|
266
|
+
if isinstance(value, str):
|
|
267
|
+
encoder.write_iri(value)
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
return value.encode(encoder, state)
|
|
271
|
+
|
|
272
|
+
def decode(self, decoder, *, objectset=None):
|
|
273
|
+
iri = decoder.read_iri()
|
|
274
|
+
if iri is None:
|
|
275
|
+
return self.cls.decode(decoder, objectset=objectset)
|
|
276
|
+
|
|
277
|
+
if objectset is None:
|
|
278
|
+
return iri
|
|
279
|
+
|
|
280
|
+
obj = objectset.find_by_id(iri)
|
|
281
|
+
if obj is None:
|
|
282
|
+
return iri
|
|
283
|
+
|
|
284
|
+
self.validate(obj)
|
|
285
|
+
return obj
|
|
286
|
+
|
|
287
|
+
def link_prop(self, value, objectset, missing, visited):
|
|
288
|
+
if value is None:
|
|
289
|
+
return value
|
|
290
|
+
|
|
291
|
+
if isinstance(value, str):
|
|
292
|
+
o = objectset.find_by_id(value)
|
|
293
|
+
if o is not None:
|
|
294
|
+
self.validate(o)
|
|
295
|
+
return o
|
|
296
|
+
|
|
297
|
+
if missing is not None:
|
|
298
|
+
missing.add(value)
|
|
299
|
+
|
|
300
|
+
return value
|
|
301
|
+
|
|
302
|
+
# De-duplicate IDs
|
|
303
|
+
if value._id:
|
|
304
|
+
value = objectset.find_by_id(value._id, value)
|
|
305
|
+
self.validate(value)
|
|
306
|
+
|
|
307
|
+
value.link_helper(objectset, missing, visited)
|
|
308
|
+
return value
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class ListProxy(object):
|
|
312
|
+
def __init__(self, prop, data=None):
|
|
313
|
+
if data is None:
|
|
314
|
+
self.__data = []
|
|
315
|
+
else:
|
|
316
|
+
self.__data = data
|
|
317
|
+
self.__prop = prop
|
|
318
|
+
|
|
319
|
+
def append(self, value):
|
|
320
|
+
self.__prop.validate(value)
|
|
321
|
+
self.__data.append(self.__prop.set(value))
|
|
322
|
+
|
|
323
|
+
def insert(self, idx, value):
|
|
324
|
+
self.__prop.validate(value)
|
|
325
|
+
self.__data.insert(idx, self.__prop.set(value))
|
|
326
|
+
|
|
327
|
+
def extend(self, items):
|
|
328
|
+
for i in items:
|
|
329
|
+
self.append(i)
|
|
330
|
+
|
|
331
|
+
def sort(self, *args, **kwargs):
|
|
332
|
+
self.__data.sort(*args, **kwargs)
|
|
333
|
+
|
|
334
|
+
def __getitem__(self, key):
|
|
335
|
+
return self.__data[key]
|
|
336
|
+
|
|
337
|
+
def __setitem__(self, key, value):
|
|
338
|
+
if isinstance(key, slice):
|
|
339
|
+
for v in value:
|
|
340
|
+
self.__prop.validate(v)
|
|
341
|
+
self.__data[key] = [self.__prop.set(v) for v in value]
|
|
342
|
+
else:
|
|
343
|
+
self.__prop.validate(value)
|
|
344
|
+
self.__data[key] = self.__prop.set(value)
|
|
345
|
+
|
|
346
|
+
def __delitem__(self, key):
|
|
347
|
+
del self.__data[key]
|
|
348
|
+
|
|
349
|
+
def __contains__(self, item):
|
|
350
|
+
return item in self.__data
|
|
351
|
+
|
|
352
|
+
def __iter__(self):
|
|
353
|
+
return iter(self.__data)
|
|
354
|
+
|
|
355
|
+
def __len__(self):
|
|
356
|
+
return len(self.__data)
|
|
357
|
+
|
|
358
|
+
def __str__(self):
|
|
359
|
+
return str(self.__data)
|
|
360
|
+
|
|
361
|
+
def __repr__(self):
|
|
362
|
+
return repr(self.__data)
|
|
363
|
+
|
|
364
|
+
def __eq__(self, other):
|
|
365
|
+
if isinstance(other, ListProxy):
|
|
366
|
+
return self.__data == other.__data
|
|
367
|
+
|
|
368
|
+
return self.__data == other
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class ListProp(Property):
|
|
372
|
+
"""
|
|
373
|
+
A list of SHACL properties
|
|
374
|
+
"""
|
|
375
|
+
|
|
376
|
+
VALID_TYPES = (list, ListProxy)
|
|
377
|
+
|
|
378
|
+
def __init__(self, prop):
|
|
379
|
+
super().__init__()
|
|
380
|
+
self.prop = prop
|
|
381
|
+
|
|
382
|
+
def init(self):
|
|
383
|
+
return ListProxy(self.prop)
|
|
384
|
+
|
|
385
|
+
def validate(self, value):
|
|
386
|
+
super().validate(value)
|
|
387
|
+
|
|
388
|
+
for i in value:
|
|
389
|
+
self.prop.validate(i)
|
|
390
|
+
|
|
391
|
+
def set(self, value):
|
|
392
|
+
if isinstance(value, ListProxy):
|
|
393
|
+
return value
|
|
394
|
+
|
|
395
|
+
return ListProxy(self.prop, [self.prop.set(d) for d in value])
|
|
396
|
+
|
|
397
|
+
def check_min_count(self, value, min_count):
|
|
398
|
+
check_type(value, ListProxy)
|
|
399
|
+
return len(value) >= min_count
|
|
400
|
+
|
|
401
|
+
def check_max_count(self, value, max_count):
|
|
402
|
+
check_type(value, ListProxy)
|
|
403
|
+
return len(value) <= max_count
|
|
404
|
+
|
|
405
|
+
def elide(self, value):
|
|
406
|
+
check_type(value, ListProxy)
|
|
407
|
+
return len(value) == 0
|
|
408
|
+
|
|
409
|
+
def walk(self, value, callback, path):
|
|
410
|
+
callback(value, path)
|
|
411
|
+
for idx, v in enumerate(value):
|
|
412
|
+
self.prop.walk(v, callback, path + [f"[{idx}]"])
|
|
413
|
+
|
|
414
|
+
def iter_objects(self, value, recursive, visited):
|
|
415
|
+
for v in value:
|
|
416
|
+
for c in self.prop.iter_objects(v, recursive, visited):
|
|
417
|
+
yield c
|
|
418
|
+
|
|
419
|
+
def link_prop(self, value, objectset, missing, visited):
|
|
420
|
+
if isinstance(value, ListProxy):
|
|
421
|
+
data = [self.prop.link_prop(v, objectset, missing, visited) for v in value]
|
|
422
|
+
else:
|
|
423
|
+
data = [self.prop.link_prop(v, objectset, missing, visited) for v in value]
|
|
424
|
+
|
|
425
|
+
return ListProxy(self.prop, data=data)
|
|
426
|
+
|
|
427
|
+
def encode(self, encoder, value, state):
|
|
428
|
+
check_type(value, ListProxy)
|
|
429
|
+
|
|
430
|
+
with encoder.write_list() as list_s:
|
|
431
|
+
for v in value:
|
|
432
|
+
with list_s.write_list_item() as item_s:
|
|
433
|
+
self.prop.encode(item_s, v, state)
|
|
434
|
+
|
|
435
|
+
def decode(self, decoder, *, objectset=None):
|
|
436
|
+
data = []
|
|
437
|
+
for val_d in decoder.read_list():
|
|
438
|
+
v = self.prop.decode(val_d, objectset=objectset)
|
|
439
|
+
self.prop.validate(v)
|
|
440
|
+
data.append(v)
|
|
441
|
+
|
|
442
|
+
return ListProxy(self.prop, data=data)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class EnumProp(Property):
|
|
446
|
+
VALID_TYPES = str
|
|
447
|
+
|
|
448
|
+
def __init__(self, values, *, pattern=None):
|
|
449
|
+
super().__init__(pattern=pattern)
|
|
450
|
+
self.values = values
|
|
451
|
+
|
|
452
|
+
def validate(self, value):
|
|
453
|
+
super().validate(value)
|
|
454
|
+
|
|
455
|
+
valid_values = (iri for iri, _ in self.values)
|
|
456
|
+
if value not in valid_values:
|
|
457
|
+
raise ValueError(
|
|
458
|
+
f"'{value}' is not a valid value. Choose one of {' '.join(valid_values)}"
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
def encode(self, encoder, value, state):
|
|
462
|
+
for iri, compact in self.values:
|
|
463
|
+
if iri == value:
|
|
464
|
+
encoder.write_enum(value, self, compact)
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
encoder.write_enum(value, self)
|
|
468
|
+
|
|
469
|
+
def decode(self, decoder, *, objectset=None):
|
|
470
|
+
v = decoder.read_enum(self)
|
|
471
|
+
for iri, compact in self.values:
|
|
472
|
+
if v == compact:
|
|
473
|
+
return iri
|
|
474
|
+
return v
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class NodeKind(Enum):
|
|
478
|
+
BlankNode = 1
|
|
479
|
+
IRI = 2
|
|
480
|
+
BlankNodeOrIRI = 3
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def is_IRI(s):
|
|
484
|
+
if not isinstance(s, str):
|
|
485
|
+
return False
|
|
486
|
+
if s.startswith("_:"):
|
|
487
|
+
return False
|
|
488
|
+
if ":" not in s:
|
|
489
|
+
return False
|
|
490
|
+
return True
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def is_blank_node(s):
|
|
494
|
+
if not isinstance(s, str):
|
|
495
|
+
return False
|
|
496
|
+
if not s.startswith("_:"):
|
|
497
|
+
return False
|
|
498
|
+
return True
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def register(type_iri, *, compact_type=None, abstract=False):
|
|
502
|
+
def add_class(key, c):
|
|
503
|
+
assert (
|
|
504
|
+
key not in SHACLObject.CLASSES
|
|
505
|
+
), f"{key} already registered to {SHACLObject.CLASSES[key].__name__}"
|
|
506
|
+
SHACLObject.CLASSES[key] = c
|
|
507
|
+
|
|
508
|
+
def decorator(c):
|
|
509
|
+
assert issubclass(
|
|
510
|
+
c, SHACLObject
|
|
511
|
+
), f"{c.__name__} is not derived from SHACLObject"
|
|
512
|
+
|
|
513
|
+
c._OBJ_TYPE = type_iri
|
|
514
|
+
c.IS_ABSTRACT = abstract
|
|
515
|
+
add_class(type_iri, c)
|
|
516
|
+
|
|
517
|
+
c._OBJ_COMPACT_TYPE = compact_type
|
|
518
|
+
if compact_type:
|
|
519
|
+
add_class(compact_type, c)
|
|
520
|
+
|
|
521
|
+
# Registration is deferred until the first instance of class is created
|
|
522
|
+
# so that it has access to any other defined class
|
|
523
|
+
c._NEEDS_REG = True
|
|
524
|
+
return c
|
|
525
|
+
|
|
526
|
+
return decorator
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
register_lock = threading.Lock()
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@functools.total_ordering
|
|
533
|
+
class SHACLObject(object):
|
|
534
|
+
CLASSES = {}
|
|
535
|
+
NODE_KIND = NodeKind.BlankNodeOrIRI
|
|
536
|
+
ID_ALIAS = None
|
|
537
|
+
IS_ABSTRACT = True
|
|
538
|
+
|
|
539
|
+
def __init__(self, **kwargs):
|
|
540
|
+
if self.__class__.IS_ABSTRACT:
|
|
541
|
+
raise NotImplementedError(
|
|
542
|
+
f"{self.__class__.__name__} is abstract and cannot be implemented"
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
with register_lock:
|
|
546
|
+
cls = self.__class__
|
|
547
|
+
if cls._NEEDS_REG:
|
|
548
|
+
cls._OBJ_PROPERTIES = {}
|
|
549
|
+
cls._OBJ_IRIS = {}
|
|
550
|
+
cls._register_props()
|
|
551
|
+
cls._NEEDS_REG = False
|
|
552
|
+
|
|
553
|
+
self.__dict__["_obj_data"] = {}
|
|
554
|
+
self.__dict__["_obj_metadata"] = {}
|
|
555
|
+
|
|
556
|
+
for iri, prop, _, _, _, _ in self.__iter_props():
|
|
557
|
+
self.__dict__["_obj_data"][iri] = prop.init()
|
|
558
|
+
|
|
559
|
+
for k, v in kwargs.items():
|
|
560
|
+
setattr(self, k, v)
|
|
561
|
+
|
|
562
|
+
@classmethod
|
|
563
|
+
def _register_props(cls):
|
|
564
|
+
cls._add_property("_id", StringProp(), iri="@id")
|
|
565
|
+
|
|
566
|
+
@classmethod
|
|
567
|
+
def _add_property(
|
|
568
|
+
cls,
|
|
569
|
+
pyname,
|
|
570
|
+
prop,
|
|
571
|
+
iri,
|
|
572
|
+
min_count=None,
|
|
573
|
+
max_count=None,
|
|
574
|
+
compact=None,
|
|
575
|
+
):
|
|
576
|
+
if pyname in cls._OBJ_IRIS:
|
|
577
|
+
raise KeyError(f"'{pyname}' is already defined for '{cls.__name__}'")
|
|
578
|
+
if iri in cls._OBJ_PROPERTIES:
|
|
579
|
+
raise KeyError(f"'{iri}' is already defined for '{cls.__name__}'")
|
|
580
|
+
|
|
581
|
+
while hasattr(cls, pyname):
|
|
582
|
+
pyname = pyname + "_"
|
|
583
|
+
|
|
584
|
+
pyname = sys.intern(pyname)
|
|
585
|
+
iri = sys.intern(iri)
|
|
586
|
+
|
|
587
|
+
cls._OBJ_IRIS[pyname] = iri
|
|
588
|
+
cls._OBJ_PROPERTIES[iri] = (prop, min_count, max_count, pyname, compact)
|
|
589
|
+
|
|
590
|
+
def __setattr__(self, name, value):
|
|
591
|
+
if name == self.ID_ALIAS:
|
|
592
|
+
self["@id"] = value
|
|
593
|
+
return
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
iri = self._OBJ_IRIS[name]
|
|
597
|
+
self[iri] = value
|
|
598
|
+
except KeyError:
|
|
599
|
+
raise AttributeError(
|
|
600
|
+
f"'{name}' is not a valid property of {self.__class__.__name__}"
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
def __getattr__(self, name):
|
|
604
|
+
if name in self._OBJ_IRIS:
|
|
605
|
+
return self.__dict__["_obj_data"][self._OBJ_IRIS[name]]
|
|
606
|
+
|
|
607
|
+
if name == self.ID_ALIAS:
|
|
608
|
+
return self.__dict__["_obj_data"]["@id"]
|
|
609
|
+
|
|
610
|
+
if name == "_metadata":
|
|
611
|
+
return self.__dict__["_obj_metadata"]
|
|
612
|
+
|
|
613
|
+
if name == "_IRI":
|
|
614
|
+
return self._OBJ_IRIS
|
|
615
|
+
|
|
616
|
+
if name == "TYPE":
|
|
617
|
+
return self.__class__._OBJ_TYPE
|
|
618
|
+
|
|
619
|
+
if name == "COMPACT_TYPE":
|
|
620
|
+
return self.__class__._OBJ_COMPACT_TYPE
|
|
621
|
+
|
|
622
|
+
raise AttributeError(
|
|
623
|
+
f"'{name}' is not a valid property of {self.__class__.__name__}"
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
def __delattr__(self, name):
|
|
627
|
+
if name == self.ID_ALIAS:
|
|
628
|
+
del self["@id"]
|
|
629
|
+
return
|
|
630
|
+
|
|
631
|
+
try:
|
|
632
|
+
iri = self._OBJ_IRIS[name]
|
|
633
|
+
del self[iri]
|
|
634
|
+
except KeyError:
|
|
635
|
+
raise AttributeError(
|
|
636
|
+
f"'{name}' is not a valid property of {self.__class__.__name__}"
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
def __get_prop(self, iri):
|
|
640
|
+
if iri not in self._OBJ_PROPERTIES:
|
|
641
|
+
raise KeyError(
|
|
642
|
+
f"'{iri}' is not a valid property of {self.__class__.__name__}"
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
return self._OBJ_PROPERTIES[iri]
|
|
646
|
+
|
|
647
|
+
def __iter_props(self):
|
|
648
|
+
for iri, v in self._OBJ_PROPERTIES.items():
|
|
649
|
+
yield iri, *v
|
|
650
|
+
|
|
651
|
+
def __getitem__(self, iri):
|
|
652
|
+
return self.__dict__["_obj_data"][iri]
|
|
653
|
+
|
|
654
|
+
def __setitem__(self, iri, value):
|
|
655
|
+
if iri == "@id":
|
|
656
|
+
if self.NODE_KIND == NodeKind.BlankNode:
|
|
657
|
+
if not is_blank_node(value):
|
|
658
|
+
raise ValueError(
|
|
659
|
+
f"{self.__class__.__name__} ({id(self)}) can only have local reference. Property '{iri}' cannot be set to '{value}' and must start with '_:'"
|
|
660
|
+
)
|
|
661
|
+
elif self.NODE_KIND == NodeKind.IRI:
|
|
662
|
+
if not is_IRI(value):
|
|
663
|
+
raise ValueError(
|
|
664
|
+
f"{self.__class__.__name__} ({id(self)}) can only have an IRI value. Property '{iri}' cannot be set to '{value}'"
|
|
665
|
+
)
|
|
666
|
+
else:
|
|
667
|
+
if not is_blank_node(value) and not is_IRI(value):
|
|
668
|
+
raise ValueError(
|
|
669
|
+
f"{self.__class__.__name__} ({id(self)}) Has invalid Property '{iri}' '{value}'. Must be a blank node or IRI"
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
prop, _, _, _, _ = self.__get_prop(iri)
|
|
673
|
+
prop.validate(value)
|
|
674
|
+
self.__dict__["_obj_data"][iri] = prop.set(value)
|
|
675
|
+
|
|
676
|
+
def __delitem__(self, iri):
|
|
677
|
+
prop, _, _, _, _ = self.__get_prop(iri)
|
|
678
|
+
self.__dict__["_obj_data"][iri] = prop.init()
|
|
679
|
+
|
|
680
|
+
def __iter__(self):
|
|
681
|
+
return self._OBJ_PROPERTIES.keys()
|
|
682
|
+
|
|
683
|
+
def walk(self, callback, path=None):
|
|
684
|
+
"""
|
|
685
|
+
Walk object tree, invoking the callback for each item
|
|
686
|
+
|
|
687
|
+
Callback has the form:
|
|
688
|
+
|
|
689
|
+
def callback(object, path):
|
|
690
|
+
"""
|
|
691
|
+
if path is None:
|
|
692
|
+
path = ["."]
|
|
693
|
+
|
|
694
|
+
if callback(self, path):
|
|
695
|
+
for iri, prop, _, _, _, _ in self.__iter_props():
|
|
696
|
+
prop.walk(self.__dict__["_obj_data"][iri], callback, path + [f".{iri}"])
|
|
697
|
+
|
|
698
|
+
def property_keys(self):
|
|
699
|
+
for iri, _, _, _, pyname, compact in self.__iter_props():
|
|
700
|
+
if iri == "@id":
|
|
701
|
+
compact = self.ID_ALIAS
|
|
702
|
+
yield pyname, iri, compact
|
|
703
|
+
|
|
704
|
+
def iter_objects(self, *, recursive=False, visited=None):
|
|
705
|
+
"""
|
|
706
|
+
Iterate of all objects that are a child of this one
|
|
707
|
+
"""
|
|
708
|
+
if visited is None:
|
|
709
|
+
visited = set()
|
|
710
|
+
|
|
711
|
+
for iri, prop, _, _, _, _ in self.__iter_props():
|
|
712
|
+
for c in prop.iter_objects(
|
|
713
|
+
self.__dict__["_obj_data"][iri], recursive=recursive, visited=visited
|
|
714
|
+
):
|
|
715
|
+
yield c
|
|
716
|
+
|
|
717
|
+
def encode(self, encoder, state):
|
|
718
|
+
idname = self.ID_ALIAS or self._OBJ_IRIS["_id"]
|
|
719
|
+
if not self._id and self.NODE_KIND == NodeKind.IRI:
|
|
720
|
+
raise ValueError(
|
|
721
|
+
f"{self.__class__.__name__} ({id(self)}) must have a IRI for property '{idname}'"
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
if state.is_written(self):
|
|
725
|
+
encoder.write_iri(state.get_object_id(self))
|
|
726
|
+
return
|
|
727
|
+
|
|
728
|
+
state.add_written(self)
|
|
729
|
+
|
|
730
|
+
with encoder.write_object(
|
|
731
|
+
self,
|
|
732
|
+
state.get_object_id(self),
|
|
733
|
+
bool(self._id) or state.is_refed(self),
|
|
734
|
+
) as obj_s:
|
|
735
|
+
self._encode_properties(obj_s, state)
|
|
736
|
+
|
|
737
|
+
def _encode_properties(self, encoder, state):
|
|
738
|
+
for iri, prop, min_count, max_count, pyname, compact in self.__iter_props():
|
|
739
|
+
value = self.__dict__["_obj_data"][iri]
|
|
740
|
+
if prop.elide(value):
|
|
741
|
+
if min_count:
|
|
742
|
+
raise ValueError(
|
|
743
|
+
f"Property '{pyname}' in {self.__class__.__name__} ({id(self)}) is required (currently {value!r})"
|
|
744
|
+
)
|
|
745
|
+
continue
|
|
746
|
+
|
|
747
|
+
if min_count is not None:
|
|
748
|
+
if not prop.check_min_count(value, min_count):
|
|
749
|
+
raise ValueError(
|
|
750
|
+
f"Property '{pyname}' in {self.__class__.__name__} ({id(self)}) requires a minimum of {min_count} elements"
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
if max_count is not None:
|
|
754
|
+
if not prop.check_max_count(value, max_count):
|
|
755
|
+
raise ValueError(
|
|
756
|
+
f"Property '{pyname}' in {self.__class__.__name__} ({id(self)}) requires a maximum of {max_count} elements"
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
if iri == self._OBJ_IRIS["_id"]:
|
|
760
|
+
continue
|
|
761
|
+
|
|
762
|
+
with encoder.write_property(iri, compact) as prop_s:
|
|
763
|
+
prop.encode(prop_s, value, state)
|
|
764
|
+
|
|
765
|
+
@classmethod
|
|
766
|
+
def _make_object(cls, typ):
|
|
767
|
+
if typ not in cls.CLASSES:
|
|
768
|
+
raise TypeError(f"Unknown type {typ}")
|
|
769
|
+
|
|
770
|
+
return cls.CLASSES[typ]()
|
|
771
|
+
|
|
772
|
+
@classmethod
|
|
773
|
+
def decode(cls, decoder, *, objectset=None):
|
|
774
|
+
typ, obj_d = decoder.read_object()
|
|
775
|
+
if typ is None:
|
|
776
|
+
raise TypeError("Unable to determine type for object")
|
|
777
|
+
|
|
778
|
+
obj = cls._make_object(typ)
|
|
779
|
+
for key in (obj.ID_ALIAS, obj._OBJ_IRIS["_id"]):
|
|
780
|
+
with obj_d.read_property(key) as prop_d:
|
|
781
|
+
if prop_d is None:
|
|
782
|
+
continue
|
|
783
|
+
|
|
784
|
+
_id = prop_d.read_iri()
|
|
785
|
+
if _id is None:
|
|
786
|
+
raise TypeError(f"Object key '{key}' is the wrong type")
|
|
787
|
+
|
|
788
|
+
obj._id = _id
|
|
789
|
+
break
|
|
790
|
+
|
|
791
|
+
if obj.NODE_KIND == NodeKind.IRI and not obj._id:
|
|
792
|
+
raise ValueError("Object is missing required IRI")
|
|
793
|
+
|
|
794
|
+
if objectset is not None:
|
|
795
|
+
if obj._id:
|
|
796
|
+
v = objectset.find_by_id(_id)
|
|
797
|
+
if v is not None:
|
|
798
|
+
return v
|
|
799
|
+
|
|
800
|
+
obj._decode_properties(obj_d, objectset=objectset)
|
|
801
|
+
|
|
802
|
+
if objectset is not None:
|
|
803
|
+
objectset.add_index(obj)
|
|
804
|
+
return obj
|
|
805
|
+
|
|
806
|
+
def _decode_properties(self, decoder, objectset=None):
|
|
807
|
+
for key in decoder.object_keys():
|
|
808
|
+
if not self._decode_prop(decoder, key, objectset=objectset):
|
|
809
|
+
raise KeyError(f"Unknown property '{key}'")
|
|
810
|
+
|
|
811
|
+
def _decode_prop(self, decoder, key, objectset=None):
|
|
812
|
+
if key in (self._OBJ_IRIS["_id"], self.ID_ALIAS):
|
|
813
|
+
return True
|
|
814
|
+
|
|
815
|
+
for iri, prop, _, _, _, compact in self.__iter_props():
|
|
816
|
+
if compact == key:
|
|
817
|
+
read_key = compact
|
|
818
|
+
elif iri == key:
|
|
819
|
+
read_key = iri
|
|
820
|
+
else:
|
|
821
|
+
continue
|
|
822
|
+
|
|
823
|
+
with decoder.read_property(read_key) as prop_d:
|
|
824
|
+
v = prop.decode(prop_d, objectset=objectset)
|
|
825
|
+
prop.validate(v)
|
|
826
|
+
self.__dict__["_obj_data"][iri] = v
|
|
827
|
+
return True
|
|
828
|
+
|
|
829
|
+
return False
|
|
830
|
+
|
|
831
|
+
def link_helper(self, objectset, missing, visited):
|
|
832
|
+
if self in visited:
|
|
833
|
+
return
|
|
834
|
+
|
|
835
|
+
visited.add(self)
|
|
836
|
+
|
|
837
|
+
for iri, prop, _, _, _, _ in self.__iter_props():
|
|
838
|
+
self.__dict__["_obj_data"][iri] = prop.link_prop(
|
|
839
|
+
self.__dict__["_obj_data"][iri],
|
|
840
|
+
objectset,
|
|
841
|
+
missing,
|
|
842
|
+
visited,
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
def __str__(self):
|
|
846
|
+
parts = [
|
|
847
|
+
f"{self.__class__.__name__}(",
|
|
848
|
+
]
|
|
849
|
+
if self._id:
|
|
850
|
+
parts.append(f"@id='{self._id}'")
|
|
851
|
+
parts.append(")")
|
|
852
|
+
return "".join(parts)
|
|
853
|
+
|
|
854
|
+
def __hash__(self):
|
|
855
|
+
return super().__hash__()
|
|
856
|
+
|
|
857
|
+
def __eq__(self, other):
|
|
858
|
+
return super().__eq__(other)
|
|
859
|
+
|
|
860
|
+
def __lt__(self, other):
|
|
861
|
+
def sort_key(obj):
|
|
862
|
+
if isinstance(obj, str):
|
|
863
|
+
return (obj, "", "", "")
|
|
864
|
+
return (
|
|
865
|
+
obj._id or "",
|
|
866
|
+
obj.TYPE,
|
|
867
|
+
getattr(obj, "name", None) or "",
|
|
868
|
+
id(obj),
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
return sort_key(self) < sort_key(other)
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
class SHACLExtensibleObject(object):
|
|
875
|
+
CLOSED = False
|
|
876
|
+
|
|
877
|
+
def __init__(self, typ=None, **kwargs):
|
|
878
|
+
super().__init__(**kwargs)
|
|
879
|
+
if typ:
|
|
880
|
+
self.__dict__["_obj_TYPE"] = (typ, None)
|
|
881
|
+
else:
|
|
882
|
+
self.__dict__["_obj_TYPE"] = (self._OBJ_TYPE, self._OBJ_COMPACT_TYPE)
|
|
883
|
+
|
|
884
|
+
@classmethod
|
|
885
|
+
def _make_object(cls, typ):
|
|
886
|
+
# Check for a known type, and if so, deserialize as that instead
|
|
887
|
+
if typ in cls.CLASSES:
|
|
888
|
+
return cls.CLASSES[typ]()
|
|
889
|
+
|
|
890
|
+
obj = cls(typ)
|
|
891
|
+
return obj
|
|
892
|
+
|
|
893
|
+
def _decode_properties(self, decoder, objectset=None):
|
|
894
|
+
if self.CLOSED:
|
|
895
|
+
super()._decode_properties(decoder, objectset=objectset)
|
|
896
|
+
return
|
|
897
|
+
|
|
898
|
+
for key in decoder.object_keys():
|
|
899
|
+
if self._decode_prop(decoder, key, objectset=objectset):
|
|
900
|
+
continue
|
|
901
|
+
|
|
902
|
+
if not is_IRI(key):
|
|
903
|
+
raise KeyError(
|
|
904
|
+
f"Extensible object properties must be IRIs. Got '{key}'"
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
with decoder.read_property(key) as prop_d:
|
|
908
|
+
self.__dict__["_obj_data"][key] = prop_d.read_value()
|
|
909
|
+
|
|
910
|
+
def _encode_properties(self, encoder, state):
|
|
911
|
+
def encode_value(encoder, v):
|
|
912
|
+
if isinstance(v, bool):
|
|
913
|
+
encoder.write_bool(v)
|
|
914
|
+
elif isinstance(v, str):
|
|
915
|
+
encoder.write_string(v)
|
|
916
|
+
elif isinstance(v, int):
|
|
917
|
+
encoder.write_integer(v)
|
|
918
|
+
elif isinstance(v, float):
|
|
919
|
+
encoder.write_float(v)
|
|
920
|
+
else:
|
|
921
|
+
raise TypeError(
|
|
922
|
+
f"Unsupported serialized type {type(v)} with value '{v}'"
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
super()._encode_properties(encoder, state)
|
|
926
|
+
if self.CLOSED:
|
|
927
|
+
return
|
|
928
|
+
|
|
929
|
+
for iri, value in self.__dict__["_obj_data"].items():
|
|
930
|
+
if iri in self._OBJ_PROPERTIES:
|
|
931
|
+
continue
|
|
932
|
+
|
|
933
|
+
with encoder.write_property(iri) as prop_s:
|
|
934
|
+
encode_value(prop_s, value)
|
|
935
|
+
|
|
936
|
+
def __setitem__(self, iri, value):
|
|
937
|
+
try:
|
|
938
|
+
super().__setitem__(iri, value)
|
|
939
|
+
except KeyError:
|
|
940
|
+
if self.CLOSED:
|
|
941
|
+
raise
|
|
942
|
+
|
|
943
|
+
if not is_IRI(iri):
|
|
944
|
+
raise KeyError(f"Key '{iri}' must be an IRI")
|
|
945
|
+
self.__dict__["_obj_data"][iri] = value
|
|
946
|
+
|
|
947
|
+
def __delitem__(self, iri):
|
|
948
|
+
try:
|
|
949
|
+
super().__delitem__(iri)
|
|
950
|
+
except KeyError:
|
|
951
|
+
if self.CLOSED:
|
|
952
|
+
raise
|
|
953
|
+
|
|
954
|
+
if not is_IRI(iri):
|
|
955
|
+
raise KeyError(f"Key '{iri}' must be an IRI")
|
|
956
|
+
del self.__dict__["_obj_data"][iri]
|
|
957
|
+
|
|
958
|
+
def __getattr__(self, name):
|
|
959
|
+
if name == "TYPE":
|
|
960
|
+
return self.__dict__["_obj_TYPE"][0]
|
|
961
|
+
if name == "COMPACT_TYPE":
|
|
962
|
+
return self.__dict__["_obj_TYPE"][1]
|
|
963
|
+
return super().__getattr__(name)
|
|
964
|
+
|
|
965
|
+
def property_keys(self):
|
|
966
|
+
iris = set()
|
|
967
|
+
for pyname, iri, compact in super().property_keys():
|
|
968
|
+
iris.add(iri)
|
|
969
|
+
yield pyname, iri, compact
|
|
970
|
+
|
|
971
|
+
if self.CLOSED:
|
|
972
|
+
return
|
|
973
|
+
|
|
974
|
+
for iri in self.__dict__["_obj_data"].keys():
|
|
975
|
+
if iri not in iris:
|
|
976
|
+
yield None, iri, None
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
class SHACLObjectSet(object):
|
|
980
|
+
def __init__(self, objects=[], *, link=False):
|
|
981
|
+
self.objects = set()
|
|
982
|
+
self.missing_ids = set()
|
|
983
|
+
for o in objects:
|
|
984
|
+
self.objects.add(o)
|
|
985
|
+
self.create_index()
|
|
986
|
+
if link:
|
|
987
|
+
self._link()
|
|
988
|
+
|
|
989
|
+
def create_index(self):
|
|
990
|
+
"""
|
|
991
|
+
(re)Create object index
|
|
992
|
+
|
|
993
|
+
Creates or recreates the indices for the object set to enable fast
|
|
994
|
+
lookup. All objects and their children are walked and indexed
|
|
995
|
+
"""
|
|
996
|
+
self.obj_by_id = {}
|
|
997
|
+
self.obj_by_type = {}
|
|
998
|
+
for o in self.foreach():
|
|
999
|
+
self.add_index(o)
|
|
1000
|
+
|
|
1001
|
+
def add_index(self, obj):
|
|
1002
|
+
"""
|
|
1003
|
+
Add object to index
|
|
1004
|
+
|
|
1005
|
+
Adds the object to all appropriate indices
|
|
1006
|
+
"""
|
|
1007
|
+
|
|
1008
|
+
def reg_type(typ, compact, o, exact):
|
|
1009
|
+
self.obj_by_type.setdefault(typ, set()).add((exact, o))
|
|
1010
|
+
if compact:
|
|
1011
|
+
self.obj_by_type.setdefault(compact, set()).add((exact, o))
|
|
1012
|
+
|
|
1013
|
+
if not isinstance(obj, SHACLObject):
|
|
1014
|
+
raise TypeError("Object is not of type SHACLObject")
|
|
1015
|
+
|
|
1016
|
+
for typ in SHACLObject.CLASSES.values():
|
|
1017
|
+
if isinstance(obj, typ):
|
|
1018
|
+
reg_type(
|
|
1019
|
+
typ._OBJ_TYPE, typ._OBJ_COMPACT_TYPE, obj, obj.__class__ is typ
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
# This covers custom extensions
|
|
1023
|
+
reg_type(obj.TYPE, obj.COMPACT_TYPE, obj, True)
|
|
1024
|
+
|
|
1025
|
+
if not obj._id:
|
|
1026
|
+
return
|
|
1027
|
+
|
|
1028
|
+
self.missing_ids.discard(obj._id)
|
|
1029
|
+
|
|
1030
|
+
if obj._id in self.obj_by_id:
|
|
1031
|
+
return
|
|
1032
|
+
|
|
1033
|
+
self.obj_by_id[obj._id] = obj
|
|
1034
|
+
|
|
1035
|
+
def add(self, obj):
|
|
1036
|
+
"""
|
|
1037
|
+
Add object to object set
|
|
1038
|
+
|
|
1039
|
+
Adds a SHACLObject to the object set and index it.
|
|
1040
|
+
|
|
1041
|
+
NOTE: Child objects of the attached object are not indexes
|
|
1042
|
+
"""
|
|
1043
|
+
if not isinstance(obj, SHACLObject):
|
|
1044
|
+
raise TypeError("Object is not of type SHACLObject")
|
|
1045
|
+
|
|
1046
|
+
if obj not in self.objects:
|
|
1047
|
+
self.objects.add(obj)
|
|
1048
|
+
self.add_index(obj)
|
|
1049
|
+
return obj
|
|
1050
|
+
|
|
1051
|
+
def update(self, *others):
|
|
1052
|
+
"""
|
|
1053
|
+
Update object set adding all objects in each other iterable
|
|
1054
|
+
"""
|
|
1055
|
+
for o in others:
|
|
1056
|
+
for obj in o:
|
|
1057
|
+
self.add(obj)
|
|
1058
|
+
|
|
1059
|
+
def __contains__(self, item):
|
|
1060
|
+
"""
|
|
1061
|
+
Returns True if the item is in the object set
|
|
1062
|
+
"""
|
|
1063
|
+
return item in self.objects
|
|
1064
|
+
|
|
1065
|
+
def link(self):
|
|
1066
|
+
"""
|
|
1067
|
+
Link object set
|
|
1068
|
+
|
|
1069
|
+
Links the object in the object set by replacing string object
|
|
1070
|
+
references with references to the objects themselves. e.g.
|
|
1071
|
+
a property that references object "https://foo/bar" by a string
|
|
1072
|
+
reference will be replaced with an actual reference to the object in
|
|
1073
|
+
the object set with the same ID if it exists in the object set
|
|
1074
|
+
|
|
1075
|
+
If multiple objects with the same ID are found, the duplicates are
|
|
1076
|
+
eliminated
|
|
1077
|
+
"""
|
|
1078
|
+
self.create_index()
|
|
1079
|
+
return self._link()
|
|
1080
|
+
|
|
1081
|
+
def _link(self):
|
|
1082
|
+
self.missing_ids = set()
|
|
1083
|
+
visited = set()
|
|
1084
|
+
|
|
1085
|
+
new_objects = set()
|
|
1086
|
+
|
|
1087
|
+
for o in self.objects:
|
|
1088
|
+
if o._id:
|
|
1089
|
+
o = self.find_by_id(o._id, o)
|
|
1090
|
+
o.link_helper(self, self.missing_ids, visited)
|
|
1091
|
+
new_objects.add(o)
|
|
1092
|
+
|
|
1093
|
+
self.objects = new_objects
|
|
1094
|
+
|
|
1095
|
+
# Remove blank nodes
|
|
1096
|
+
obj_by_id = {}
|
|
1097
|
+
for _id, obj in self.obj_by_id.items():
|
|
1098
|
+
if _id.startswith("_:"):
|
|
1099
|
+
del obj._id
|
|
1100
|
+
else:
|
|
1101
|
+
obj_by_id[_id] = obj
|
|
1102
|
+
self.obj_by_id = obj_by_id
|
|
1103
|
+
|
|
1104
|
+
return self.missing_ids
|
|
1105
|
+
|
|
1106
|
+
def find_by_id(self, _id, default=None):
|
|
1107
|
+
"""
|
|
1108
|
+
Find object by ID
|
|
1109
|
+
|
|
1110
|
+
Returns objects that match the specified ID, or default if there is no
|
|
1111
|
+
object with the specified ID
|
|
1112
|
+
"""
|
|
1113
|
+
if _id not in self.obj_by_id:
|
|
1114
|
+
return default
|
|
1115
|
+
return self.obj_by_id[_id]
|
|
1116
|
+
|
|
1117
|
+
def foreach(self):
|
|
1118
|
+
"""
|
|
1119
|
+
Iterate over every object in the object set, and all child objects
|
|
1120
|
+
"""
|
|
1121
|
+
visited = set()
|
|
1122
|
+
for o in self.objects:
|
|
1123
|
+
if o not in visited:
|
|
1124
|
+
yield o
|
|
1125
|
+
visited.add(o)
|
|
1126
|
+
|
|
1127
|
+
for child in o.iter_objects(recursive=True, visited=visited):
|
|
1128
|
+
yield child
|
|
1129
|
+
|
|
1130
|
+
def foreach_type(self, typ, *, match_subclass=True):
|
|
1131
|
+
"""
|
|
1132
|
+
Iterate over each object of a specified type (or subclass there of)
|
|
1133
|
+
|
|
1134
|
+
If match_subclass is True, and class derived from typ will also match
|
|
1135
|
+
(similar to isinstance()). If False, only exact matches will be
|
|
1136
|
+
returned
|
|
1137
|
+
"""
|
|
1138
|
+
if not isinstance(typ, str):
|
|
1139
|
+
if not issubclass(typ, SHACLObject):
|
|
1140
|
+
raise TypeError(f"Type must be derived from SHACLObject, got {typ}")
|
|
1141
|
+
typ = typ._OBJ_TYPE
|
|
1142
|
+
|
|
1143
|
+
if typ not in self.obj_by_type:
|
|
1144
|
+
return
|
|
1145
|
+
|
|
1146
|
+
for exact, o in self.obj_by_type[typ]:
|
|
1147
|
+
if match_subclass or exact:
|
|
1148
|
+
yield o
|
|
1149
|
+
|
|
1150
|
+
def merge(self, *objectsets):
|
|
1151
|
+
"""
|
|
1152
|
+
Merge object sets
|
|
1153
|
+
|
|
1154
|
+
Returns a new object set that is the combination of this object set and
|
|
1155
|
+
all provided arguments
|
|
1156
|
+
"""
|
|
1157
|
+
new_objects = set()
|
|
1158
|
+
new_objects |= self.objects
|
|
1159
|
+
for d in objectsets:
|
|
1160
|
+
new_objects |= d.objects
|
|
1161
|
+
|
|
1162
|
+
return SHACLObjectSet(new_objects, link=True)
|
|
1163
|
+
|
|
1164
|
+
def encode(self, encoder, force_list=False):
|
|
1165
|
+
"""
|
|
1166
|
+
Serialize a list of objects to a serialization encoder
|
|
1167
|
+
|
|
1168
|
+
If force_list is true, a list will always be written using the encoder.
|
|
1169
|
+
"""
|
|
1170
|
+
ref_counts = {}
|
|
1171
|
+
state = EncodeState()
|
|
1172
|
+
|
|
1173
|
+
def walk_callback(value, path):
|
|
1174
|
+
nonlocal state
|
|
1175
|
+
nonlocal ref_counts
|
|
1176
|
+
|
|
1177
|
+
if not isinstance(value, SHACLObject):
|
|
1178
|
+
return True
|
|
1179
|
+
|
|
1180
|
+
# Remove blank node ID for re-assignment
|
|
1181
|
+
if value._id and value._id.startswith("_:"):
|
|
1182
|
+
del value._id
|
|
1183
|
+
|
|
1184
|
+
if value._id:
|
|
1185
|
+
state.add_refed(value)
|
|
1186
|
+
|
|
1187
|
+
# If the object is referenced more than once, add it to the set of
|
|
1188
|
+
# referenced objects
|
|
1189
|
+
ref_counts.setdefault(value, 0)
|
|
1190
|
+
ref_counts[value] += 1
|
|
1191
|
+
if ref_counts[value] > 1:
|
|
1192
|
+
state.add_refed(value)
|
|
1193
|
+
return False
|
|
1194
|
+
|
|
1195
|
+
return True
|
|
1196
|
+
|
|
1197
|
+
for o in self.objects:
|
|
1198
|
+
if o._id:
|
|
1199
|
+
state.add_refed(o)
|
|
1200
|
+
o.walk(walk_callback)
|
|
1201
|
+
|
|
1202
|
+
use_list = force_list or len(self.objects) > 1
|
|
1203
|
+
|
|
1204
|
+
if use_list:
|
|
1205
|
+
# If we are making a list add all the objects referred to by reference
|
|
1206
|
+
# to the list
|
|
1207
|
+
objects = list(self.objects | state.ref_objects)
|
|
1208
|
+
else:
|
|
1209
|
+
objects = list(self.objects)
|
|
1210
|
+
|
|
1211
|
+
objects.sort()
|
|
1212
|
+
|
|
1213
|
+
if use_list:
|
|
1214
|
+
# Ensure top level objects are only written in the top level graph
|
|
1215
|
+
# node, and referenced by ID everywhere else. This is done by setting
|
|
1216
|
+
# the flag that indicates this object has been written for all the top
|
|
1217
|
+
# level objects, then clearing it right before serializing the object.
|
|
1218
|
+
#
|
|
1219
|
+
# In this way, if an object is referenced before it is supposed to be
|
|
1220
|
+
# serialized into the @graph, it will serialize as a string instead of
|
|
1221
|
+
# the actual object
|
|
1222
|
+
for o in objects:
|
|
1223
|
+
state.written_objects.add(o)
|
|
1224
|
+
|
|
1225
|
+
with encoder.write_list() as list_s:
|
|
1226
|
+
for o in objects:
|
|
1227
|
+
# Allow this specific object to be written now
|
|
1228
|
+
state.written_objects.remove(o)
|
|
1229
|
+
with list_s.write_list_item() as item_s:
|
|
1230
|
+
o.encode(item_s, state)
|
|
1231
|
+
|
|
1232
|
+
else:
|
|
1233
|
+
objects[0].encode(encoder, state)
|
|
1234
|
+
|
|
1235
|
+
def decode(self, decoder):
|
|
1236
|
+
self.create_index()
|
|
1237
|
+
|
|
1238
|
+
for obj_d in decoder.read_list():
|
|
1239
|
+
o = SHACLObject.decode(obj_d, objectset=self)
|
|
1240
|
+
self.objects.add(o)
|
|
1241
|
+
|
|
1242
|
+
self._link()
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
class EncodeState(object):
|
|
1246
|
+
def __init__(self):
|
|
1247
|
+
self.ref_objects = set()
|
|
1248
|
+
self.written_objects = set()
|
|
1249
|
+
self.blank_objects = {}
|
|
1250
|
+
|
|
1251
|
+
def get_object_id(self, o):
|
|
1252
|
+
if o._id:
|
|
1253
|
+
return o._id
|
|
1254
|
+
|
|
1255
|
+
if o not in self.blank_objects:
|
|
1256
|
+
_id = f"_:{o.__class__.__name__}{len(self.blank_objects)}"
|
|
1257
|
+
self.blank_objects[o] = _id
|
|
1258
|
+
|
|
1259
|
+
return self.blank_objects[o]
|
|
1260
|
+
|
|
1261
|
+
def is_refed(self, o):
|
|
1262
|
+
return o in self.ref_objects
|
|
1263
|
+
|
|
1264
|
+
def add_refed(self, o):
|
|
1265
|
+
self.ref_objects.add(o)
|
|
1266
|
+
|
|
1267
|
+
def is_written(self, o):
|
|
1268
|
+
return o in self.written_objects
|
|
1269
|
+
|
|
1270
|
+
def add_written(self, o):
|
|
1271
|
+
self.written_objects.add(o)
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
class Decoder(ABC):
|
|
1275
|
+
@abstractmethod
|
|
1276
|
+
def read_value(self):
|
|
1277
|
+
"""
|
|
1278
|
+
Consume next item
|
|
1279
|
+
|
|
1280
|
+
Consumes the next item of any type
|
|
1281
|
+
"""
|
|
1282
|
+
pass
|
|
1283
|
+
|
|
1284
|
+
@abstractmethod
|
|
1285
|
+
def read_string(self):
|
|
1286
|
+
"""
|
|
1287
|
+
Consume the next item as a string.
|
|
1288
|
+
|
|
1289
|
+
Returns the string value of the next item, or `None` if the next item
|
|
1290
|
+
is not a string
|
|
1291
|
+
"""
|
|
1292
|
+
pass
|
|
1293
|
+
|
|
1294
|
+
@abstractmethod
|
|
1295
|
+
def read_datetime(self):
|
|
1296
|
+
"""
|
|
1297
|
+
Consumes the next item as a date & time string
|
|
1298
|
+
|
|
1299
|
+
Returns the string value of the next item, if it is a ISO datetime, or
|
|
1300
|
+
`None` if the next item is not a ISO datetime string.
|
|
1301
|
+
|
|
1302
|
+
Note that validation of the string is done by the caller, so a minimal
|
|
1303
|
+
implementation can just check if the next item is a string without
|
|
1304
|
+
worrying about the format
|
|
1305
|
+
"""
|
|
1306
|
+
pass
|
|
1307
|
+
|
|
1308
|
+
@abstractmethod
|
|
1309
|
+
def read_integer(self):
|
|
1310
|
+
"""
|
|
1311
|
+
Consumes the next item as an integer
|
|
1312
|
+
|
|
1313
|
+
Returns the integer value of the next item, or `None` if the next item
|
|
1314
|
+
is not an integer
|
|
1315
|
+
"""
|
|
1316
|
+
pass
|
|
1317
|
+
|
|
1318
|
+
@abstractmethod
|
|
1319
|
+
def read_iri(self):
|
|
1320
|
+
"""
|
|
1321
|
+
Consumes the next item as an IRI string
|
|
1322
|
+
|
|
1323
|
+
Returns the string value of the next item an IRI, or `None` if the next
|
|
1324
|
+
item is not an IRI.
|
|
1325
|
+
|
|
1326
|
+
The returned string should be either a fully-qualified IRI, or a blank
|
|
1327
|
+
node ID
|
|
1328
|
+
"""
|
|
1329
|
+
pass
|
|
1330
|
+
|
|
1331
|
+
@abstractmethod
|
|
1332
|
+
def read_enum(self, e):
|
|
1333
|
+
"""
|
|
1334
|
+
Consumes the next item as an Enum value string
|
|
1335
|
+
|
|
1336
|
+
Returns the fully qualified IRI of the next enum item, or `None` if the
|
|
1337
|
+
next item is not an enum value.
|
|
1338
|
+
|
|
1339
|
+
The callee is responsible for validating that the returned IRI is
|
|
1340
|
+
actually a member of the specified Enum, so the `Decoder` does not need
|
|
1341
|
+
to check that, but can if it wishes
|
|
1342
|
+
"""
|
|
1343
|
+
pass
|
|
1344
|
+
|
|
1345
|
+
@abstractmethod
|
|
1346
|
+
def read_bool(self):
|
|
1347
|
+
"""
|
|
1348
|
+
Consume the next item as a boolean value
|
|
1349
|
+
|
|
1350
|
+
Returns the boolean value of the next item, or `None` if the next item
|
|
1351
|
+
is not a boolean
|
|
1352
|
+
"""
|
|
1353
|
+
pass
|
|
1354
|
+
|
|
1355
|
+
@abstractmethod
|
|
1356
|
+
def read_float(self):
|
|
1357
|
+
"""
|
|
1358
|
+
Consume the next item as a float value
|
|
1359
|
+
|
|
1360
|
+
Returns the float value of the next item, or `None` if the next item is
|
|
1361
|
+
not a float
|
|
1362
|
+
"""
|
|
1363
|
+
pass
|
|
1364
|
+
|
|
1365
|
+
@abstractmethod
|
|
1366
|
+
def read_list(self):
|
|
1367
|
+
"""
|
|
1368
|
+
Consume the next item as a list generator
|
|
1369
|
+
|
|
1370
|
+
This should generate a `Decoder` object for each item in the list. The
|
|
1371
|
+
generated `Decoder` can be used to read the corresponding item from the
|
|
1372
|
+
list
|
|
1373
|
+
"""
|
|
1374
|
+
pass
|
|
1375
|
+
|
|
1376
|
+
@abstractmethod
|
|
1377
|
+
def read_object(self):
|
|
1378
|
+
"""
|
|
1379
|
+
Consume next item as an object
|
|
1380
|
+
|
|
1381
|
+
A context manager that "enters" the next item as a object and yields a
|
|
1382
|
+
`Decoder` that can read properties from it. If the next item is not an
|
|
1383
|
+
object, yields `None`
|
|
1384
|
+
|
|
1385
|
+
Properties will be read out of the object using `read_property` and
|
|
1386
|
+
`read_object_id`
|
|
1387
|
+
"""
|
|
1388
|
+
pass
|
|
1389
|
+
|
|
1390
|
+
@abstractmethod
|
|
1391
|
+
@contextmanager
|
|
1392
|
+
def read_property(self, key):
|
|
1393
|
+
"""
|
|
1394
|
+
Read property from object
|
|
1395
|
+
|
|
1396
|
+
A context manager that yields a `Decoder` that can be used to read the
|
|
1397
|
+
value of the property with the given key in current object, or `None`
|
|
1398
|
+
if the property does not exist in the current object.
|
|
1399
|
+
"""
|
|
1400
|
+
pass
|
|
1401
|
+
|
|
1402
|
+
@abstractmethod
|
|
1403
|
+
def object_keys(self):
|
|
1404
|
+
"""
|
|
1405
|
+
Read property keys from an object
|
|
1406
|
+
|
|
1407
|
+
Iterates over all the serialized keys for the current object
|
|
1408
|
+
"""
|
|
1409
|
+
pass
|
|
1410
|
+
|
|
1411
|
+
@abstractmethod
|
|
1412
|
+
def read_object_id(self, alias=None):
|
|
1413
|
+
"""
|
|
1414
|
+
Read current object ID property
|
|
1415
|
+
|
|
1416
|
+
Returns the ID of the current object if one is defined, or `None` if
|
|
1417
|
+
the current object has no ID.
|
|
1418
|
+
|
|
1419
|
+
The ID must be a fully qualified IRI or a blank node
|
|
1420
|
+
|
|
1421
|
+
If `alias` is provided, is is a hint as to another name by which the ID
|
|
1422
|
+
might be found, if the `Decoder` supports aliases for an ID
|
|
1423
|
+
"""
|
|
1424
|
+
pass
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
class JSONLDDecoder(Decoder):
|
|
1428
|
+
def __init__(self, data, root=False):
|
|
1429
|
+
self.data = data
|
|
1430
|
+
self.root = root
|
|
1431
|
+
|
|
1432
|
+
def read_value(self):
|
|
1433
|
+
if isinstance(self.data, str):
|
|
1434
|
+
try:
|
|
1435
|
+
return float(self.data)
|
|
1436
|
+
except ValueError:
|
|
1437
|
+
pass
|
|
1438
|
+
return self.data
|
|
1439
|
+
|
|
1440
|
+
def read_string(self):
|
|
1441
|
+
if isinstance(self.data, str):
|
|
1442
|
+
return self.data
|
|
1443
|
+
return None
|
|
1444
|
+
|
|
1445
|
+
def read_datetime(self):
|
|
1446
|
+
return self.read_string()
|
|
1447
|
+
|
|
1448
|
+
def read_integer(self):
|
|
1449
|
+
if isinstance(self.data, int):
|
|
1450
|
+
return self.data
|
|
1451
|
+
return None
|
|
1452
|
+
|
|
1453
|
+
def read_bool(self):
|
|
1454
|
+
if isinstance(self.data, bool):
|
|
1455
|
+
return self.data
|
|
1456
|
+
return None
|
|
1457
|
+
|
|
1458
|
+
def read_float(self):
|
|
1459
|
+
if isinstance(self.data, (int, float, str)):
|
|
1460
|
+
return float(self.data)
|
|
1461
|
+
return None
|
|
1462
|
+
|
|
1463
|
+
def read_iri(self):
|
|
1464
|
+
if isinstance(self.data, str):
|
|
1465
|
+
return self.data
|
|
1466
|
+
return None
|
|
1467
|
+
|
|
1468
|
+
def read_enum(self, e):
|
|
1469
|
+
if isinstance(self.data, str):
|
|
1470
|
+
return self.data
|
|
1471
|
+
return None
|
|
1472
|
+
|
|
1473
|
+
def read_list(self):
|
|
1474
|
+
if isinstance(self.data, (list, tuple, set)):
|
|
1475
|
+
for v in self.data:
|
|
1476
|
+
yield self.__class__(v)
|
|
1477
|
+
else:
|
|
1478
|
+
yield self
|
|
1479
|
+
|
|
1480
|
+
def __get_value(self, *keys):
|
|
1481
|
+
for k in keys:
|
|
1482
|
+
if k and k in self.data:
|
|
1483
|
+
return self.data[k]
|
|
1484
|
+
return None
|
|
1485
|
+
|
|
1486
|
+
@contextmanager
|
|
1487
|
+
def read_property(self, key):
|
|
1488
|
+
v = self.__get_value(key)
|
|
1489
|
+
if v is not None:
|
|
1490
|
+
yield self.__class__(v)
|
|
1491
|
+
else:
|
|
1492
|
+
yield None
|
|
1493
|
+
|
|
1494
|
+
def object_keys(self):
|
|
1495
|
+
for key in self.data.keys():
|
|
1496
|
+
if key in ("@type", "{{ context.compact('@type') }}"):
|
|
1497
|
+
continue
|
|
1498
|
+
if self.root and key == "@context":
|
|
1499
|
+
continue
|
|
1500
|
+
yield key
|
|
1501
|
+
|
|
1502
|
+
def read_object(self):
|
|
1503
|
+
typ = self.__get_value("@type", "{{ context.compact('@type') }}")
|
|
1504
|
+
if typ is not None:
|
|
1505
|
+
return typ, self
|
|
1506
|
+
|
|
1507
|
+
return None, self
|
|
1508
|
+
|
|
1509
|
+
def read_object_id(self, alias=None):
|
|
1510
|
+
return self.__get_value(alias, "@id")
|
|
1511
|
+
|
|
1512
|
+
|
|
1513
|
+
class JSONLDDeserializer(object):
|
|
1514
|
+
def deserialize_data(self, data, objectset: SHACLObjectSet):
|
|
1515
|
+
if "@graph" in data:
|
|
1516
|
+
h = JSONLDDecoder(data["@graph"], True)
|
|
1517
|
+
else:
|
|
1518
|
+
h = JSONLDDecoder(data, True)
|
|
1519
|
+
|
|
1520
|
+
objectset.decode(h)
|
|
1521
|
+
|
|
1522
|
+
def read(self, f, objectset: SHACLObjectSet):
|
|
1523
|
+
data = json.load(f)
|
|
1524
|
+
self.deserialize_data(data, objectset)
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
class Encoder(ABC):
|
|
1528
|
+
@abstractmethod
|
|
1529
|
+
def write_string(self, v):
|
|
1530
|
+
"""
|
|
1531
|
+
Write a string value
|
|
1532
|
+
|
|
1533
|
+
Encodes the value as a string in the output
|
|
1534
|
+
"""
|
|
1535
|
+
pass
|
|
1536
|
+
|
|
1537
|
+
@abstractmethod
|
|
1538
|
+
def write_datetime(self, v):
|
|
1539
|
+
"""
|
|
1540
|
+
Write a date & time string
|
|
1541
|
+
|
|
1542
|
+
Encodes the value as an ISO datetime string
|
|
1543
|
+
|
|
1544
|
+
Note: The provided string is already correctly encoded as an ISO datetime
|
|
1545
|
+
"""
|
|
1546
|
+
pass
|
|
1547
|
+
|
|
1548
|
+
@abstractmethod
|
|
1549
|
+
def write_integer(self, v):
|
|
1550
|
+
"""
|
|
1551
|
+
Write an integer value
|
|
1552
|
+
|
|
1553
|
+
Encodes the value as an integer in the output
|
|
1554
|
+
"""
|
|
1555
|
+
pass
|
|
1556
|
+
|
|
1557
|
+
@abstractmethod
|
|
1558
|
+
def write_iri(self, v, compact=None):
|
|
1559
|
+
"""
|
|
1560
|
+
Write IRI
|
|
1561
|
+
|
|
1562
|
+
Encodes the string as an IRI. Note that the string will be either a
|
|
1563
|
+
fully qualified IRI or a blank node ID. If `compact` is provided and
|
|
1564
|
+
the serialization supports compacted IRIs, it should be preferred to
|
|
1565
|
+
the full IRI
|
|
1566
|
+
"""
|
|
1567
|
+
pass
|
|
1568
|
+
|
|
1569
|
+
@abstractmethod
|
|
1570
|
+
def write_enum(self, v, e, compact=None):
|
|
1571
|
+
"""
|
|
1572
|
+
Write enum value IRI
|
|
1573
|
+
|
|
1574
|
+
Encodes the string enum value IRI. Note that the string will be a fully
|
|
1575
|
+
qualified IRI. If `compact` is provided and the serialization supports
|
|
1576
|
+
compacted IRIs, it should be preferred to the full IRI.
|
|
1577
|
+
"""
|
|
1578
|
+
pass
|
|
1579
|
+
|
|
1580
|
+
@abstractmethod
|
|
1581
|
+
def write_bool(self, v):
|
|
1582
|
+
"""
|
|
1583
|
+
Write boolean
|
|
1584
|
+
|
|
1585
|
+
Encodes the value as a boolean in the output
|
|
1586
|
+
"""
|
|
1587
|
+
pass
|
|
1588
|
+
|
|
1589
|
+
@abstractmethod
|
|
1590
|
+
def write_float(self, v):
|
|
1591
|
+
"""
|
|
1592
|
+
Write float
|
|
1593
|
+
|
|
1594
|
+
Encodes the value as a floating point number in the output
|
|
1595
|
+
"""
|
|
1596
|
+
pass
|
|
1597
|
+
|
|
1598
|
+
@abstractmethod
|
|
1599
|
+
@contextmanager
|
|
1600
|
+
def write_object(self, o, _id, needs_id):
|
|
1601
|
+
"""
|
|
1602
|
+
Write object
|
|
1603
|
+
|
|
1604
|
+
A context manager that yields an `Encoder` that can be used to encode
|
|
1605
|
+
the given object properties.
|
|
1606
|
+
|
|
1607
|
+
The provided ID will always be a valid ID (even if o._id is `None`), in
|
|
1608
|
+
case the `Encoder` _must_ have an ID. `needs_id` is a hint to indicate
|
|
1609
|
+
to the `Encoder` if an ID must be written or not (if that is even an
|
|
1610
|
+
option). If it is `True`, the `Encoder` must encode an ID for the
|
|
1611
|
+
object. If `False`, the encoder is not required to encode an ID and may
|
|
1612
|
+
omit it.
|
|
1613
|
+
|
|
1614
|
+
The ID will be either a fully qualified IRI, or a blank node IRI.
|
|
1615
|
+
|
|
1616
|
+
Properties will be written the object using `write_property`
|
|
1617
|
+
"""
|
|
1618
|
+
pass
|
|
1619
|
+
|
|
1620
|
+
@abstractmethod
|
|
1621
|
+
@contextmanager
|
|
1622
|
+
def write_property(self, iri, compact=None):
|
|
1623
|
+
"""
|
|
1624
|
+
Write object property
|
|
1625
|
+
|
|
1626
|
+
A context manager that yields an `Encoder` that can be used to encode
|
|
1627
|
+
the value for the property with the given IRI in the current object
|
|
1628
|
+
|
|
1629
|
+
Note that the IRI will be fully qualified. If `compact` is provided and
|
|
1630
|
+
the serialization supports compacted IRIs, it should be preferred to
|
|
1631
|
+
the full IRI.
|
|
1632
|
+
"""
|
|
1633
|
+
pass
|
|
1634
|
+
|
|
1635
|
+
@abstractmethod
|
|
1636
|
+
@contextmanager
|
|
1637
|
+
def write_list(self):
|
|
1638
|
+
"""
|
|
1639
|
+
Write list
|
|
1640
|
+
|
|
1641
|
+
A context manager that yields an `Encoder` that can be used to encode a
|
|
1642
|
+
list.
|
|
1643
|
+
|
|
1644
|
+
Each item of the list will be added using `write_list_item`
|
|
1645
|
+
"""
|
|
1646
|
+
pass
|
|
1647
|
+
|
|
1648
|
+
@abstractmethod
|
|
1649
|
+
@contextmanager
|
|
1650
|
+
def write_list_item(self):
|
|
1651
|
+
"""
|
|
1652
|
+
Write list item
|
|
1653
|
+
|
|
1654
|
+
A context manager that yields an `Encoder` that can be used to encode
|
|
1655
|
+
the value for a list item
|
|
1656
|
+
"""
|
|
1657
|
+
pass
|
|
1658
|
+
|
|
1659
|
+
|
|
1660
|
+
class JSONLDEncoder(Encoder):
|
|
1661
|
+
def __init__(self, data=None):
|
|
1662
|
+
self.data = data
|
|
1663
|
+
|
|
1664
|
+
def write_string(self, v):
|
|
1665
|
+
self.data = v
|
|
1666
|
+
|
|
1667
|
+
def write_datetime(self, v):
|
|
1668
|
+
self.data = v
|
|
1669
|
+
|
|
1670
|
+
def write_integer(self, v):
|
|
1671
|
+
self.data = v
|
|
1672
|
+
|
|
1673
|
+
def write_iri(self, v, compact=None):
|
|
1674
|
+
self.write_string(compact or v)
|
|
1675
|
+
|
|
1676
|
+
def write_enum(self, v, e, compact=None):
|
|
1677
|
+
self.write_string(compact or v)
|
|
1678
|
+
|
|
1679
|
+
def write_bool(self, v):
|
|
1680
|
+
self.data = v
|
|
1681
|
+
|
|
1682
|
+
def write_float(self, v):
|
|
1683
|
+
self.data = str(v)
|
|
1684
|
+
|
|
1685
|
+
@contextmanager
|
|
1686
|
+
def write_property(self, iri, compact=None):
|
|
1687
|
+
s = self.__class__(None)
|
|
1688
|
+
yield s
|
|
1689
|
+
if s.data is not None:
|
|
1690
|
+
self.data[compact or iri] = s.data
|
|
1691
|
+
|
|
1692
|
+
@contextmanager
|
|
1693
|
+
def write_object(self, o, _id, needs_id):
|
|
1694
|
+
self.data = {
|
|
1695
|
+
"{{ context.compact('@type') }}": o.COMPACT_TYPE or o.TYPE,
|
|
1696
|
+
}
|
|
1697
|
+
if needs_id:
|
|
1698
|
+
self.data[o.ID_ALIAS or "@id"] = _id
|
|
1699
|
+
yield self
|
|
1700
|
+
|
|
1701
|
+
@contextmanager
|
|
1702
|
+
def write_list(self):
|
|
1703
|
+
self.data = []
|
|
1704
|
+
yield self
|
|
1705
|
+
if not self.data:
|
|
1706
|
+
self.data = None
|
|
1707
|
+
|
|
1708
|
+
@contextmanager
|
|
1709
|
+
def write_list_item(self):
|
|
1710
|
+
s = self.__class__(None)
|
|
1711
|
+
yield s
|
|
1712
|
+
if s.data is not None:
|
|
1713
|
+
self.data.append(s.data)
|
|
1714
|
+
|
|
1715
|
+
|
|
1716
|
+
class JSONLDSerializer(object):
|
|
1717
|
+
def __init__(self, **args):
|
|
1718
|
+
self.args = args
|
|
1719
|
+
|
|
1720
|
+
def serialize_data(
|
|
1721
|
+
self,
|
|
1722
|
+
objectset: SHACLObjectSet,
|
|
1723
|
+
force_at_graph=False,
|
|
1724
|
+
):
|
|
1725
|
+
h = JSONLDEncoder()
|
|
1726
|
+
objectset.encode(h, force_at_graph)
|
|
1727
|
+
data = {}
|
|
1728
|
+
if len(CONTEXT_URLS) == 1:
|
|
1729
|
+
data["@context"] = CONTEXT_URLS[0]
|
|
1730
|
+
elif CONTEXT_URLS:
|
|
1731
|
+
data["@context"] = CONTEXT_URLS
|
|
1732
|
+
|
|
1733
|
+
if isinstance(h.data, list):
|
|
1734
|
+
data["@graph"] = h.data
|
|
1735
|
+
else:
|
|
1736
|
+
for k, v in h.data.items():
|
|
1737
|
+
data[k] = v
|
|
1738
|
+
|
|
1739
|
+
return data
|
|
1740
|
+
|
|
1741
|
+
def write(
|
|
1742
|
+
self,
|
|
1743
|
+
objectset: SHACLObjectSet,
|
|
1744
|
+
f,
|
|
1745
|
+
force_at_graph=False,
|
|
1746
|
+
**kwargs,
|
|
1747
|
+
):
|
|
1748
|
+
"""
|
|
1749
|
+
Write a SHACLObjectSet to a JSON LD file
|
|
1750
|
+
|
|
1751
|
+
If force_at_graph is True, a @graph node will always be written
|
|
1752
|
+
"""
|
|
1753
|
+
data = self.serialize_data(objectset, force_at_graph)
|
|
1754
|
+
|
|
1755
|
+
args = {**self.args, **kwargs}
|
|
1756
|
+
|
|
1757
|
+
sha1 = hashlib.sha1()
|
|
1758
|
+
for chunk in json.JSONEncoder(**args).iterencode(data):
|
|
1759
|
+
chunk = chunk.encode("utf-8")
|
|
1760
|
+
f.write(chunk)
|
|
1761
|
+
sha1.update(chunk)
|
|
1762
|
+
|
|
1763
|
+
return sha1.hexdigest()
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
class JSONLDInlineEncoder(Encoder):
|
|
1767
|
+
def __init__(self, f, sha1):
|
|
1768
|
+
self.f = f
|
|
1769
|
+
self.comma = False
|
|
1770
|
+
self.sha1 = sha1
|
|
1771
|
+
|
|
1772
|
+
def write(self, s):
|
|
1773
|
+
s = s.encode("utf-8")
|
|
1774
|
+
self.f.write(s)
|
|
1775
|
+
self.sha1.update(s)
|
|
1776
|
+
|
|
1777
|
+
def _write_comma(self):
|
|
1778
|
+
if self.comma:
|
|
1779
|
+
self.write(",")
|
|
1780
|
+
self.comma = False
|
|
1781
|
+
|
|
1782
|
+
def write_string(self, v):
|
|
1783
|
+
self.write(json.dumps(v))
|
|
1784
|
+
|
|
1785
|
+
def write_datetime(self, v):
|
|
1786
|
+
self.write_string(v)
|
|
1787
|
+
|
|
1788
|
+
def write_integer(self, v):
|
|
1789
|
+
self.write(f"{v}")
|
|
1790
|
+
|
|
1791
|
+
def write_iri(self, v, compact=None):
|
|
1792
|
+
self.write_string(compact or v)
|
|
1793
|
+
|
|
1794
|
+
def write_enum(self, v, e, compact=None):
|
|
1795
|
+
self.write_iri(v, compact)
|
|
1796
|
+
|
|
1797
|
+
def write_bool(self, v):
|
|
1798
|
+
if v:
|
|
1799
|
+
self.write("true")
|
|
1800
|
+
else:
|
|
1801
|
+
self.write("false")
|
|
1802
|
+
|
|
1803
|
+
def write_float(self, v):
|
|
1804
|
+
self.write(json.dumps(str(v)))
|
|
1805
|
+
|
|
1806
|
+
@contextmanager
|
|
1807
|
+
def write_property(self, iri, compact=None):
|
|
1808
|
+
self._write_comma()
|
|
1809
|
+
self.write_string(compact or iri)
|
|
1810
|
+
self.write(":")
|
|
1811
|
+
yield self
|
|
1812
|
+
self.comma = True
|
|
1813
|
+
|
|
1814
|
+
@contextmanager
|
|
1815
|
+
def write_object(self, o, _id, needs_id):
|
|
1816
|
+
self._write_comma()
|
|
1817
|
+
|
|
1818
|
+
self.write("{")
|
|
1819
|
+
self.write_string("{{ context.compact('@type') }}")
|
|
1820
|
+
self.write(":")
|
|
1821
|
+
self.write_string(o.COMPACT_TYPE or o.TYPE)
|
|
1822
|
+
self.comma = True
|
|
1823
|
+
|
|
1824
|
+
if needs_id:
|
|
1825
|
+
self._write_comma()
|
|
1826
|
+
self.write_string(o.ID_ALIAS or "@id")
|
|
1827
|
+
self.write(":")
|
|
1828
|
+
self.write_string(_id)
|
|
1829
|
+
self.comma = True
|
|
1830
|
+
|
|
1831
|
+
self.comma = True
|
|
1832
|
+
yield self
|
|
1833
|
+
|
|
1834
|
+
self.write("}")
|
|
1835
|
+
self.comma = True
|
|
1836
|
+
|
|
1837
|
+
@contextmanager
|
|
1838
|
+
def write_list(self):
|
|
1839
|
+
self._write_comma()
|
|
1840
|
+
self.write("[")
|
|
1841
|
+
yield self.__class__(self.f, self.sha1)
|
|
1842
|
+
self.write("]")
|
|
1843
|
+
self.comma = True
|
|
1844
|
+
|
|
1845
|
+
@contextmanager
|
|
1846
|
+
def write_list_item(self):
|
|
1847
|
+
self._write_comma()
|
|
1848
|
+
yield self.__class__(self.f, self.sha1)
|
|
1849
|
+
self.comma = True
|
|
1850
|
+
|
|
1851
|
+
|
|
1852
|
+
class JSONLDInlineSerializer(object):
|
|
1853
|
+
def write(
|
|
1854
|
+
self,
|
|
1855
|
+
objectset: SHACLObjectSet,
|
|
1856
|
+
f,
|
|
1857
|
+
force_at_graph=False,
|
|
1858
|
+
):
|
|
1859
|
+
"""
|
|
1860
|
+
Write a SHACLObjectSet to a JSON LD file
|
|
1861
|
+
|
|
1862
|
+
Note: force_at_graph is included for compatibility, but ignored. This
|
|
1863
|
+
serializer always writes out a graph
|
|
1864
|
+
"""
|
|
1865
|
+
sha1 = hashlib.sha1()
|
|
1866
|
+
h = JSONLDInlineEncoder(f, sha1)
|
|
1867
|
+
h.write('{"@context":')
|
|
1868
|
+
if len(CONTEXT_URLS) == 1:
|
|
1869
|
+
h.write(f'"{CONTEXT_URLS[0]}"')
|
|
1870
|
+
elif CONTEXT_URLS:
|
|
1871
|
+
h.write('["')
|
|
1872
|
+
h.write('","'.join(CONTEXT_URLS))
|
|
1873
|
+
h.write('"]')
|
|
1874
|
+
h.write(",")
|
|
1875
|
+
|
|
1876
|
+
h.write('"@graph":')
|
|
1877
|
+
|
|
1878
|
+
objectset.encode(h, True)
|
|
1879
|
+
h.write("}")
|
|
1880
|
+
return sha1.hexdigest()
|
|
1881
|
+
|
|
1882
|
+
|
|
1883
|
+
def print_tree(objects, all_fields=False):
|
|
1884
|
+
"""
|
|
1885
|
+
Print object tree
|
|
1886
|
+
"""
|
|
1887
|
+
seen = set()
|
|
1888
|
+
|
|
1889
|
+
def callback(value, path):
|
|
1890
|
+
nonlocal seen
|
|
1891
|
+
|
|
1892
|
+
s = (" " * (len(path) - 1)) + f"{path[-1]}"
|
|
1893
|
+
if isinstance(value, SHACLObject):
|
|
1894
|
+
s += f" {value} ({id(value)})"
|
|
1895
|
+
is_empty = False
|
|
1896
|
+
elif isinstance(value, ListProxy):
|
|
1897
|
+
is_empty = len(value) == 0
|
|
1898
|
+
if is_empty:
|
|
1899
|
+
s += " []"
|
|
1900
|
+
else:
|
|
1901
|
+
s += f" {value!r}"
|
|
1902
|
+
is_empty = value is None
|
|
1903
|
+
|
|
1904
|
+
if all_fields or not is_empty:
|
|
1905
|
+
print(s)
|
|
1906
|
+
|
|
1907
|
+
if isinstance(value, SHACLObject):
|
|
1908
|
+
if value in seen:
|
|
1909
|
+
return False
|
|
1910
|
+
seen.add(value)
|
|
1911
|
+
return True
|
|
1912
|
+
|
|
1913
|
+
return True
|
|
1914
|
+
|
|
1915
|
+
for o in objects:
|
|
1916
|
+
o.walk(callback)
|
|
1917
|
+
|
|
1918
|
+
|
|
1919
|
+
# fmt: off
|
|
1920
|
+
"""Format Guard{{ '"' }}{{ '"' }}{{ '"' }}
|
|
1921
|
+
{% set
|
|
1922
|
+
DATATYPE_CLASSES = {
|
|
1923
|
+
"http://www.w3.org/2001/XMLSchema#string": "StringProp",
|
|
1924
|
+
"http://www.w3.org/2001/XMLSchema#anyURI": "AnyURIProp",
|
|
1925
|
+
"http://www.w3.org/2001/XMLSchema#integer": "IntegerProp",
|
|
1926
|
+
"http://www.w3.org/2001/XMLSchema#positiveInteger": "PositiveIntegerProp",
|
|
1927
|
+
"http://www.w3.org/2001/XMLSchema#nonNegativeInteger": "NonNegativeIntegerProp",
|
|
1928
|
+
"http://www.w3.org/2001/XMLSchema#boolean": "BooleanProp",
|
|
1929
|
+
"http://www.w3.org/2001/XMLSchema#decimal": "FloatProp",
|
|
1930
|
+
"http://www.w3.org/2001/XMLSchema#dateTime": "DateTimeProp",
|
|
1931
|
+
"http://www.w3.org/2001/XMLSchema#dateTimeStamp": "DateTimeStampProp",
|
|
1932
|
+
}
|
|
1933
|
+
%}
|
|
1934
|
+
|
|
1935
|
+
CONTEXT_URLS = [
|
|
1936
|
+
{%- for url in context.urls %}
|
|
1937
|
+
"{{ url }}",
|
|
1938
|
+
{%- endfor %}
|
|
1939
|
+
]
|
|
1940
|
+
|
|
1941
|
+
|
|
1942
|
+
# CLASSES
|
|
1943
|
+
{%- for class in classes %}
|
|
1944
|
+
{%- if class.comment %}
|
|
1945
|
+
{%- for l in class.comment.split("\n") %}
|
|
1946
|
+
#{{ (" " + l).rstrip() }}
|
|
1947
|
+
{%- endfor %}
|
|
1948
|
+
{%- endif %}
|
|
1949
|
+
@register("{{ class._id }}"{%- if context.compact(class._id) != class._id %}, compact_type="{{ context.compact(class._id) }}"{%- endif %}, abstract={{ class.is_abstract }})
|
|
1950
|
+
class {{ varname(*class.clsname) }}(
|
|
1951
|
+
{%- if class.is_extensible -%}
|
|
1952
|
+
SHACLExtensibleObject{{", "}}
|
|
1953
|
+
{%- endif %}
|
|
1954
|
+
{%- if class.parent_ids %}
|
|
1955
|
+
{%- for id in class.parent_ids %}
|
|
1956
|
+
{{- varname(*classes.get(id).clsname) }}{% if not loop.last %}, {% endif %}
|
|
1957
|
+
{%- endfor %}
|
|
1958
|
+
{%- else -%}
|
|
1959
|
+
SHACLObject
|
|
1960
|
+
{%- endif -%}):
|
|
1961
|
+
NODE_KIND = NodeKind.{{ class.node_kind.split("#")[-1] }}
|
|
1962
|
+
{%- if class.id_property %}
|
|
1963
|
+
ID_ALIAS = "{{ class.id_property }}"
|
|
1964
|
+
{%- endif %}
|
|
1965
|
+
NAMED_INDIVIDUALS = {
|
|
1966
|
+
{%- for member in class.named_individuals %}
|
|
1967
|
+
"{{ varname(member.varname) }}": "{{ member._id }}",
|
|
1968
|
+
{%- endfor %}
|
|
1969
|
+
}
|
|
1970
|
+
{%- for member in class.named_individuals %}
|
|
1971
|
+
{%- if member.comment %}
|
|
1972
|
+
{%- for l in member.comment.split("\n") %}
|
|
1973
|
+
#{{ (" " + l).rstrip() }}
|
|
1974
|
+
{%- endfor %}
|
|
1975
|
+
{%- endif %}
|
|
1976
|
+
{{ varname(member.varname) }} = "{{ member._id }}"
|
|
1977
|
+
{%- endfor %}
|
|
1978
|
+
{%- if class.properties %}
|
|
1979
|
+
|
|
1980
|
+
@classmethod
|
|
1981
|
+
def _register_props(cls):
|
|
1982
|
+
super()._register_props()
|
|
1983
|
+
{%- for prop in class.properties %}
|
|
1984
|
+
{%- set is_list = prop.max_count is none or prop.max_count != 1 %}
|
|
1985
|
+
{%- if prop.comment %}
|
|
1986
|
+
{%- for l in prop.comment.split("\n") %}
|
|
1987
|
+
#{{ (" " + l).rstrip() }}
|
|
1988
|
+
{%- endfor %}
|
|
1989
|
+
{%- endif %}
|
|
1990
|
+
cls._add_property(
|
|
1991
|
+
"{{ varname(prop.varname) }}",
|
|
1992
|
+
{% if is_list -%}ListProp({% endif %}
|
|
1993
|
+
{%- if prop.enum_values -%}
|
|
1994
|
+
EnumProp([
|
|
1995
|
+
{%- for value in prop.enum_values %}
|
|
1996
|
+
("{{ value }}", "{{ context.compact_vocab(value, prop.path) }}"),
|
|
1997
|
+
{%- endfor %}
|
|
1998
|
+
])
|
|
1999
|
+
{%- elif prop.class_id -%}
|
|
2000
|
+
ObjectProp({{ varname(*classes.get(prop.class_id).clsname) }}, {% if prop.min_count and not is_list %}True{% else %}False{% endif %})
|
|
2001
|
+
{%- else -%}
|
|
2002
|
+
{% if not prop.datatype in DATATYPE_CLASSES -%}
|
|
2003
|
+
{{ abort("Unknown data type " + prop.datatype) -}}
|
|
2004
|
+
{% endif -%}
|
|
2005
|
+
{{ DATATYPE_CLASSES[prop.datatype] }}({%- if prop.pattern %}pattern=r"{{ prop.pattern }}",{%- endif %})
|
|
2006
|
+
{%- endif %}{% if is_list %}){% endif %},
|
|
2007
|
+
iri="{{ prop.path }}",
|
|
2008
|
+
{%- if is_list and not prop.max_count is none %}
|
|
2009
|
+
max_count={{ prop.max_count }},
|
|
2010
|
+
{%- endif %}
|
|
2011
|
+
{%- if not prop.min_count is none %}
|
|
2012
|
+
min_count={{ prop.min_count }},
|
|
2013
|
+
{%- endif %}
|
|
2014
|
+
{%- if context.compact_vocab(prop.path) != prop.path %}
|
|
2015
|
+
compact="{{ context.compact_vocab(prop.path) }}",
|
|
2016
|
+
{%- endif %}
|
|
2017
|
+
)
|
|
2018
|
+
{%- endfor %}
|
|
2019
|
+
{%- endif %}
|
|
2020
|
+
|
|
2021
|
+
{% endfor %}
|
|
2022
|
+
{{ '"' }}{{ '"' }}{{ '"' }}Format Guard"""
|
|
2023
|
+
# fmt: on
|
|
2024
|
+
|
|
2025
|
+
|
|
2026
|
+
def main():
|
|
2027
|
+
import argparse
|
|
2028
|
+
from pathlib import Path
|
|
2029
|
+
|
|
2030
|
+
parser = argparse.ArgumentParser(description="Python SHACL model test")
|
|
2031
|
+
parser.add_argument("infile", type=Path, help="Input file")
|
|
2032
|
+
parser.add_argument("--print", action="store_true", help="Print object tree")
|
|
2033
|
+
parser.add_argument("--outfile", type=Path, help="Output file")
|
|
2034
|
+
|
|
2035
|
+
args = parser.parse_args()
|
|
2036
|
+
|
|
2037
|
+
objectset = SHACLObjectSet()
|
|
2038
|
+
with args.infile.open("r") as f:
|
|
2039
|
+
d = JSONLDDeserializer()
|
|
2040
|
+
d.read(f, objectset)
|
|
2041
|
+
|
|
2042
|
+
if args.print:
|
|
2043
|
+
print_tree(objectset.objects)
|
|
2044
|
+
|
|
2045
|
+
if args.outfile:
|
|
2046
|
+
with args.outfile.open("wb") as f:
|
|
2047
|
+
s = JSONLDSerializer()
|
|
2048
|
+
s.write(objectset, f)
|
|
2049
|
+
|
|
2050
|
+
return 0
|
|
2051
|
+
|
|
2052
|
+
|
|
2053
|
+
if __name__ == "__main__":
|
|
2054
|
+
sys.exit(main())
|