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.
@@ -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())