Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/bones/relational.py: 5%
623 statements
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-16 22:16 +0000
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-16 22:16 +0000
1"""
2This module contains the RelationalBone to create and manage relationships between skeletons
3and enums to parameterize it.
4"""
5import enum
6import json
7import logging
8import typing as t
9import warnings
10from itertools import chain
11from time import time
13from viur.core import db, utils
14from viur.core.bones.base import BaseBone, ReadFromClientError, ReadFromClientErrorSeverity, getSystemInitialized
16if t.TYPE_CHECKING: 16 ↛ 17line 16 didn't jump to line 17 because the condition on line 16 was never true
17 from viur.core.skeleton import SkeletonInstance, RelSkel
20class RelationalConsistency(enum.IntEnum):
21 """
22 An enumeration representing the different consistency strategies for handling stale relations in
23 the RelationalBone class.
24 """
25 Ignore = 1
26 """Ignore stale relations, which represents the old behavior."""
27 PreventDeletion = 2
28 """Lock the target object so that it cannot be deleted."""
29 SetNull = 3
30 """Drop the relation if the target object is deleted."""
31 CascadeDeletion = 4
32 """
33 .. warning:: Delete this object also if the referenced entry is deleted (Dangerous!)
34 """
37class RelationalUpdateLevel(enum.Enum):
38 """
39 An enumeration representing the different update levels for the RelationalBone class.
40 """
41 Always = 0
42 """Always update the relational information, regardless of the context."""
43 OnRebuildSearchIndex = 1
44 """Update the relational information only when rebuilding the search index."""
45 OnValueAssignment = 2
46 """Update the relational information only when a new value is assigned to the bone."""
49class RelationalBone(BaseBone):
50 """
51 The base class for all relational bones in the ViUR framework.
52 RelationalBone is used to create and manage relationships between database entities. This class provides
53 basic functionality and attributes that can be extended by other specialized relational bone classes,
54 such as N1Relation, N2NRelation, and Hierarchy.
55 This implementation prioritizes read efficiency and is suitable for situations where data is read more
56 frequently than written. However, it comes with increased write operations when writing an entity to the
57 database. The additional write operations depend on the type of relationship: multiple=True RelationalBones
58 or 1:N relations.
60 The implementation does not instantly update relational information when a skeleton is updated; instead,
61 it triggers a deferred task to update references. This may result in outdated data until the task is completed.
63 Note: Filtering a list by relational properties uses the outdated data.
65 Example:
66 - Entity A references Entity B.
67 - Both have a property "name."
68 - Entity B is updated (its name changes).
69 - Entity A's RelationalBone values still show Entity B's old name.
71 It is not recommended for cases where data is read less frequently than written, as there is no
72 write-efficient method available yet.
74 :param kind: KindName of the referenced property.
75 :param module: Name of the module which should be used to select entities of kind "kind". If not set,
76 the value of "kind" will be used (the kindName must match the moduleName)
77 :param refKeys: A list of properties to include from the referenced property. These properties will be
78 available in the template without having to fetch the referenced property. Filtering is also only possible
79 by properties named here!
80 :param parentKeys: A list of properties from the current skeleton to include. If mixing filtering by
81 relational properties and properties of the class itself, these must be named here.
82 :param multiple: If True, allow referencing multiple Elements of the given class. (Eg. n:n-relation).
83 Otherwise its n:1, (you can only select exactly one). It's possible to use a unique constraint on this
84 bone, allowing for at-most-1:1 or at-most-1:n relations. Instead of true, it's also possible to use
85 a ```class MultipleConstraints``` instead.
87 :param format:
88 Hint for the frontend how to display such an relation. This is now a python expression
89 evaluated by safeeval on the client side. The following values will be passed to the expression:
91 - value
92 The value to display. This will be always a dict (= a single value) - even if the relation is
93 multiple (in which case the expression is evaluated once per referenced entity)
95 - structure
96 The structure of the skeleton this bone is part of as a dictionary as it's transferred to the
97 fronted by the admin/vi-render.
99 - language
100 The current language used by the frontend in ISO2 code (eg. "de"). This will be always set, even if
101 the project did not enable the multi-language feature.
103 :param updateLevel:
104 Indicates how ViUR should keep the values copied from the referenced entity into our
105 entity up to date. If this bone is indexed, it's recommended to leave this set to
106 RelationalUpdateLevel.Always, as filtering/sorting by this bone will produce stale results.
108 :param RelationalUpdateLevel.Always:
110 always update refkeys (old behavior). If the referenced entity is edited, ViUR will update this
111 entity also (after a small delay, as these updates happen deferred)
113 :param RelationalUpdateLevel.OnRebuildSearchIndex:
115 update refKeys only on rebuildSearchIndex. If the referenced entity changes, this entity will
116 remain unchanged (this RelationalBone will still have the old values), but it can be updated
117 by either by editing this entity or running a rebuildSearchIndex over our kind.
119 :param RelationalUpdateLevel.OnValueAssignment:
121 update only if explicitly set. A rebuildSearchIndex will not trigger an update, this bone has to be
122 explicitly modified (in an edit) to have it's values updated
124 :param consistency:
125 Can be used to implement SQL-like constrains on this relation. Possible values are:
126 - RelationalConsistency.Ignore
127 If the referenced entity gets deleted, this bone will not change. It will still reflect the old
128 values. This will be even be preserved over edits, however if that referenced value is once
129 deleted by the user (assigning a different value to this bone or removing that value of the list
130 of relations if we are multiple) there's no way of restoring it
132 - RelationalConsistency.PreventDeletion
133 Will prevent deleting the referenced entity as long as it's selected in this bone (calling
134 skel.delete() on the referenced entity will raise errors.Locked). It's still (technically)
135 possible to remove the underlying datastore entity using db.Delete manually, but this *must not*
136 be used on a skeleton object as it will leave a whole bunch of references in a stale state.
138 - RelationalConsistency.SetNull
139 Will set this bone to None (or remove the relation from the list in
140 case we are multiple) when the referenced entity is deleted.
142 - RelationalConsistency.CascadeDeletion:
143 (Dangerous!) Will delete this entity when the referenced entity is deleted. Warning: Unlike
144 relational updates this will cascade. If Entity A references B with CascadeDeletion set, and
145 B references C also with CascadeDeletion; if C gets deleted, both B and A will be deleted as well.
147 """
148 type = "relational"
149 kind = None
151 def __init__(
152 self,
153 *,
154 consistency: RelationalConsistency = RelationalConsistency.Ignore,
155 format: str = "$(dest.name)",
156 kind: str = None,
157 module: t.Optional[str] = None,
158 parentKeys: t.Optional[t.Iterable[str]] = {"name"},
159 refKeys: t.Optional[t.Iterable[str]] = {"name"},
160 updateLevel: RelationalUpdateLevel = RelationalUpdateLevel.Always,
161 using: t.Optional["RelSkel"] = None,
162 **kwargs
163 ):
164 """
165 Initialize a new RelationalBone.
167 :param kind:
168 KindName of the referenced property.
169 :param module:
170 Name of the module which should be used to select entities of kind "type". If not set,
171 the value of "type" will be used (the kindName must match the moduleName)
172 :param refKeys:
173 An iterable of properties to include from the referenced property. These properties will be
174 available in the template without having to fetch the referenced property. Filtering is also only
175 possible by properties named here!
176 :param parentKeys:
177 An iterable of properties from the current skeleton to include. If mixing filtering by
178 relational properties and properties of the class itself, these must be named here.
179 :param multiple:
180 If True, allow referencing multiple Elements of the given class. (Eg. n:n-relation).
181 Otherwise its n:1, (you can only select exactly one). It's possible to use a unique constraint on this
182 bone, allowing for at-most-1:1 or at-most-1:n relations. Instead of true, it's also possible to use
183 a :class:MultipleConstraints instead.
185 :param format: Hint for the frontend how to display such an relation. This is now a python expression
186 evaluated by safeeval on the client side. The following values will be passed to the expression
188 :param value:
189 The value to display. This will be always a dict (= a single value) - even if the
190 relation is multiple (in which case the expression is evaluated once per referenced entity)
191 :param structure:
192 The structure of the skeleton this bone is part of as a dictionary as it's
193 transferred to the fronted by the admin/vi-render.
194 :param language:
195 The current language used by the frontend in ISO2 code (eg. "de"). This will be
196 always set, even if the project did not enable the multi-language feature.
198 :param updateLevel:
199 Indicates how ViUR should keep the values copied from the referenced entity into our
200 entity up to date. If this bone is indexed, it's recommended to leave this set to
201 RelationalUpdateLevel.Always, as filtering/sorting by this bone will produce stale results.
203 :param RelationalUpdateLevel.Always:
204 always update refkeys (old behavior). If the referenced entity is edited, ViUR will update this
205 entity also (after a small delay, as these updates happen deferred)
206 :param RelationalUpdateLevel.OnRebuildSearchIndex:
207 update refKeys only on rebuildSearchIndex. If the
208 referenced entity changes, this entity will remain unchanged
209 (this RelationalBone will still have the old values), but it can be updated
210 by either by editing this entity or running a rebuildSearchIndex over our kind.
211 :param RelationalUpdateLevel.OnValueAssignment:
212 update only if explicitly set. A rebuildSearchIndex will not trigger
213 an update, this bone has to be explicitly modified (in an edit) to have it's values updated
215 :param consistency:
216 Can be used to implement SQL-like constrains on this relation.
218 :param RelationalConsistency.Ignore:
219 If the referenced entity gets deleted, this bone will not change. It
220 will still reflect the old values. This will be even be preserved over edits, however if that
221 referenced value is once deleted by the user (assigning a different value to this bone or
222 removing that value of the list of relations if we are multiple) there's no way of restoring it
224 :param RelationalConsistency.PreventDeletion:
225 Will prevent deleting the referenced entity as long as it's
226 selected in this bone (calling skel.delete() on the referenced entity will raise errors.Locked).
227 It's still (technically) possible to remove the underlying datastore entity using db.Delete
228 manually, but this *must not* be used on a skeleton object as it will leave a whole bunch of
229 references in a stale state.
231 :param RelationalConsistency.SetNull:
232 Will set this bone to None (or remove the relation from the list in
233 case we are multiple) when the referenced entity is deleted.
235 :param RelationalConsistency.CascadeDeletion:
236 (Dangerous!) Will delete this entity when the referenced entity
237 is deleted. Warning: Unlike relational updates this will cascade. If Entity A references B with
238 CascadeDeletion set, and B references C also with CascadeDeletion; if C gets deleted, both B and
239 A will be deleted as well.
240 """
241 super().__init__(**kwargs)
242 self.format = format
244 if kind:
245 self.kind = kind
247 if module:
248 self.module = module
249 elif self.kind:
250 self.module = self.kind
252 if self.kind is None or self.module is None:
253 raise NotImplementedError("'kind' and 'module' of RelationalBone must not be None")
255 # Referenced keys
256 self.refKeys = {"key"}
257 if refKeys:
258 self.refKeys |= set(refKeys)
260 # Parent keys
261 self.parentKeys = {"key"}
262 if parentKeys:
263 self.parentKeys |= set(parentKeys)
265 self.using = using
267 # FIXME: Remove in VIUR4!!
268 if isinstance(updateLevel, int):
269 msg = f"parameter updateLevel={updateLevel} in RelationalBone is deprecated. " \
270 f"Please use the RelationalUpdateLevel enum instead"
271 logging.warning(msg, stacklevel=3)
272 warnings.warn(msg, DeprecationWarning, stacklevel=3)
274 assert 0 <= updateLevel < 3
275 for n in RelationalUpdateLevel:
276 if updateLevel == n.value:
277 updateLevel = n
279 self.updateLevel = updateLevel
280 self.consistency = consistency
282 if getSystemInitialized():
283 from viur.core.skeleton import RefSkel, SkeletonInstance
284 self._refSkelCache = RefSkel.fromSkel(self.kind, *self.refKeys)
285 self._skeletonInstanceClassRef = SkeletonInstance
286 self._ref_keys = set(self._refSkelCache.__boneMap__.keys())
288 def setSystemInitialized(self):
289 """
290 Set the system initialized for the current class and cache the RefSkel and SkeletonInstance.
292 This method calls the superclass's setSystemInitialized method and initializes the RefSkel
293 and SkeletonInstance classes. The RefSkel is created from the current kind and refKeys,
294 while the SkeletonInstance class is stored as a reference.
296 :rtype: None
297 """
298 super().setSystemInitialized()
299 from viur.core.skeleton import RefSkel, SkeletonInstance
300 self._refSkelCache = RefSkel.fromSkel(self.kind, *self.refKeys)
301 self._skeletonInstanceClassRef = SkeletonInstance
302 self._ref_keys = set(self._refSkelCache.__boneMap__.keys())
304 def _getSkels(self):
305 """
306 Retrieve the reference skeleton and the 'using' skeleton for the current RelationalBone instance.
308 This method returns a tuple containing the reference skeleton (RefSkel) and the 'using' skeleton
309 (UsingSkel) associated with the current RelationalBone instance. The 'using' skeleton is only
310 retrieved if the 'using' attribute is defined.
312 :return: A tuple containing the reference skeleton and the 'using' skeleton.
313 :rtype: tuple
314 """
315 refSkel = self._refSkelCache()
316 usingSkel = self.using() if self.using else None
317 return refSkel, usingSkel
319 def singleValueUnserialize(self, val):
320 """
321 Restore a value, including the Rel- and Using-Skeleton, from the serialized data read from the datastore.
323 This method takes a serialized value from the datastore, deserializes it, and returns the corresponding
324 value with restored RelSkel and Using-Skel. It also handles ViUR 2 compatibility by handling string values.
326 :param val: A JSON-encoded datastore property.
327 :type val: str or dict
328 :return: The deserialized value with restored RelSkel and Using-Skel.
329 :rtype: dict
331 :raises AssertionError: If the deserialized value is not a dictionary.
332 """
334 def fixFromDictToEntry(inDict):
335 """
336 Convert a dictionary to an entry with properly restored keys and values.
338 :param dict inDict: The input dictionary to convert.
339 : return: The resulting entry.
340 :rtype: dict
341 """
342 if not isinstance(inDict, dict):
343 return None
344 res = {}
345 if "dest" in inDict:
346 res["dest"] = db.Entity()
347 for k, v in inDict["dest"].items():
348 res["dest"][k] = v
349 if "key" in res["dest"]:
350 res["dest"].key = utils.normalizeKey(db.Key.from_legacy_urlsafe(res["dest"]["key"]))
351 if "rel" in inDict and inDict["rel"]:
352 res["rel"] = db.Entity()
353 for k, v in inDict["rel"].items():
354 res["rel"][k] = v
355 else:
356 res["rel"] = None
357 return res
359 if isinstance(val, str): # ViUR2 compatibility
360 try:
361 value = json.loads(val)
362 if isinstance(value, list):
363 value = [fixFromDictToEntry(x) for x in value]
364 elif isinstance(value, dict):
365 value = fixFromDictToEntry(value)
366 else:
367 value = None
368 except ValueError:
369 value = None
370 else:
371 value = val
372 if not value:
373 return None
374 elif isinstance(value, list) and value:
375 value = value[0]
376 assert isinstance(value, dict), f"Read something from the datastore thats not a dict: {type(value)}"
377 if "dest" not in value:
378 return None
379 relSkel, usingSkel = self._getSkels()
380 relSkel.unserialize(value["dest"])
381 if self.using is not None:
382 usingSkel.unserialize(value["rel"] or db.Entity())
383 usingData = usingSkel
384 else:
385 usingData = None
386 return {"dest": relSkel, "rel": usingData}
388 def serialize(self, skel: "SkeletonInstance", name: str, parentIndexed: bool) -> bool:
389 """
390 Serialize the RelationalBone for the given skeleton, updating relational locks as necessary.
392 This method serializes the RelationalBone values for a given skeleton and stores the serialized
393 values in the skeleton's dbEntity. It also updates the relational locks, adding new locks and
394 removing old ones as needed.
396 :param SkeletonInstance skel: The skeleton instance containing the values to be serialized.
397 :param str name: The name of the bone to be serialized.
398 :param bool parentIndexed: A flag indicating whether the parent bone is indexed.
399 :return: True if the serialization is successful, False otherwise.
400 :rtype: bool
402 :raises AssertionError: If a programming error is detected.
403 """
404 super().serialize(skel, name, parentIndexed)
405 # Clean old properties from entry (prevent name collision)
406 for k in list(skel.dbEntity.keys()):
407 if k.startswith(f"{name}."):
408 del skel.dbEntity[k]
409 indexed = self.indexed and parentIndexed
410 if name not in skel.accessedValues:
411 return
412 elif not skel.accessedValues[name]:
413 res = None
414 elif self.languages and self.multiple:
415 res = {"_viurLanguageWrapper_": True}
416 newVals = skel.accessedValues[name]
417 for language in self.languages:
418 res[language] = []
419 if language in newVals:
420 for val in newVals[language]:
421 if val["dest"]:
422 refData = val["dest"].serialize(parentIndexed=indexed)
423 else:
424 refData = None
425 if val["rel"]:
426 usingData = val["rel"].serialize(parentIndexed=indexed)
427 else:
428 usingData = None
429 r = {"rel": usingData, "dest": refData}
430 res[language].append(r)
431 elif self.languages:
432 res = {"_viurLanguageWrapper_": True}
433 newVals = skel.accessedValues[name]
434 for language in self.languages:
435 res[language] = []
436 if language in newVals:
437 val = newVals[language]
438 if val and val["dest"]:
439 refData = val["dest"].serialize(parentIndexed=indexed)
440 if val["rel"]:
441 usingData = val["rel"].serialize(parentIndexed=indexed)
442 else:
443 usingData = None
444 r = {"rel": usingData, "dest": refData}
445 res[language] = r
446 else:
447 res[language] = None
448 elif self.multiple:
449 res = []
450 for val in skel.accessedValues[name]:
451 if val["dest"]:
452 refData = val["dest"].serialize(parentIndexed=indexed)
453 else:
454 refData = None
455 if val["rel"]:
456 usingData = val["rel"].serialize(parentIndexed=indexed)
457 else:
458 usingData = None
459 r = {"rel": usingData, "dest": refData}
460 res.append(r)
461 else:
462 if skel.accessedValues[name]["dest"]:
463 refData = skel.accessedValues[name]["dest"].serialize(parentIndexed=indexed)
464 else:
465 refData = None
466 if skel.accessedValues[name]["rel"]:
467 usingData = skel.accessedValues[name]["rel"].serialize(parentIndexed=indexed)
468 else:
469 usingData = None
470 res = {"rel": usingData, "dest": refData}
472 skel.dbEntity[name] = res
474 # Ensure our indexed flag is up2date
475 if indexed and name in skel.dbEntity.exclude_from_indexes:
476 skel.dbEntity.exclude_from_indexes.discard(name)
477 elif not indexed and name not in skel.dbEntity.exclude_from_indexes:
478 skel.dbEntity.exclude_from_indexes.add(name)
480 # Delete legacy property (PR #1244) #TODO: Remove in ViUR4
481 skel.dbEntity.pop(f"{name}_outgoingRelationalLocks", None)
483 return True
485 def _get_single_destinct_hash(self, value):
486 parts = [value["dest"]["key"]]
488 if self.using:
489 for name, bone in self.using.__boneMap__.items():
490 parts.append(bone._get_destinct_hash(value["rel"][name]))
492 return tuple(parts)
494 def postSavedHandler(self, skel: "SkeletonInstance", boneName: str, key: db.Key) -> None:
495 """
496 Handle relational updates after a skeleton is saved.
498 This method updates, removes, or adds relations between the saved skeleton and the referenced entities.
499 It also takes care of updating the relational properties and consistency levels.
501 :param skel: The saved skeleton instance.
502 :param boneName: The name of the relational bone.
503 :param key: The key of the saved skeleton instance.
504 """
505 if not skel[boneName]:
506 values = []
507 elif self.multiple and self.languages:
508 values = chain(*skel[boneName].values())
509 elif self.languages:
510 values = list(skel[boneName].values())
511 elif self.multiple:
512 values = skel[boneName]
513 else:
514 values = [skel[boneName]]
515 values = [x for x in values if x is not None]
516 parentValues = db.Entity()
517 srcEntity = skel.dbEntity
518 parentValues.key = srcEntity.key
519 for boneKey in (self.parentKeys or []):
520 if boneKey == "key": # this is a relcit from viur2, as the key is encoded in the embedded entity
521 continue
522 parentValues[boneKey] = srcEntity.get(boneKey)
523 dbVals = db.Query("viur-relations")
524 dbVals.filter("viur_src_kind =", skel.kindName)
525 dbVals.filter("viur_dest_kind =", self.kind)
526 dbVals.filter("viur_src_property =", boneName)
527 dbVals.filter("src.__key__ =", key)
528 for dbObj in dbVals.iter():
529 try:
530 if not dbObj["dest"].key in [x["dest"]["key"] for x in values]: # Relation has been removed
531 db.Delete(dbObj.key)
532 continue
533 except: # This entry is corrupt
534 db.Delete(dbObj.key)
535 else: # Relation: Updated
536 data = [x for x in values if x["dest"]["key"] == dbObj["dest"].key][0]
537 # Write our (updated) values in
538 refSkel = data["dest"]
539 dbObj["dest"] = refSkel.serialize(parentIndexed=True)
540 dbObj["src"] = parentValues
541 if self.using is not None:
542 usingSkel = data["rel"]
543 dbObj["rel"] = usingSkel.serialize(parentIndexed=True)
544 dbObj["viur_delayed_update_tag"] = time()
545 dbObj["viur_relational_updateLevel"] = self.updateLevel.value
546 dbObj["viur_relational_consistency"] = self.consistency.value
547 dbObj["viur_foreign_keys"] = list(self.refKeys)
548 dbObj["viurTags"] = srcEntity.get("viurTags") # Copy tags over so we can still use our searchengine
549 db.Put(dbObj)
550 values.remove(data)
551 # Add any new Relation
552 for val in values:
553 dbObj = db.Entity(db.Key("viur-relations", parent=key))
554 refSkel = val["dest"]
555 dbObj["dest"] = refSkel.serialize(parentIndexed=True)
556 dbObj["src"] = parentValues
557 if self.using is not None:
558 usingSkel = val["rel"]
559 dbObj["rel"] = usingSkel.serialize(parentIndexed=True)
560 dbObj["viur_delayed_update_tag"] = time()
561 dbObj["viur_src_kind"] = skel.kindName # The kind of the entry referencing
562 dbObj["viur_src_property"] = boneName # The key of the bone referencing
563 dbObj["viur_dest_kind"] = self.kind
564 dbObj["viur_relational_updateLevel"] = self.updateLevel.value
565 dbObj["viur_relational_consistency"] = self.consistency.value
566 dbObj["viur_foreign_keys"] = list(self._ref_keys)
567 db.Put(dbObj)
569 def postDeletedHandler(self, skel: "SkeletonInstance", boneName: str, key: db.Key) -> None:
570 """
571 Handle relational updates after a skeleton is deleted.
573 This method deletes all relations associated with the deleted skeleton and the referenced entities
574 for the given relational bone.
576 :param skel: The deleted SkeletonInstance.
577 :param boneName: The name of the RelationalBone in the Skeleton.
578 :param key: The key of the deleted Entity.
579 """
580 query = db.Query("viur-relations")
581 query.filter("viur_src_kind =", skel.kindName)
582 query.filter("viur_dest_kind =", self.kind)
583 query.filter("viur_src_property =", boneName)
584 query.filter("src.__key__ =", key)
585 db.Delete([entity for entity in query.run()])
587 def isInvalid(self, key) -> None:
588 """
589 Check if the given key is invalid for this relational bone.
591 This method always returns None, as the actual validation of the key
592 is performed in other methods of the RelationalBone class.
594 :param key: The key to be checked for validity.
595 :return: None, as the actual validation is performed elsewhere.
596 """
597 return None
599 def parseSubfieldsFromClient(self):
600 """
601 Determine if the RelationalBone should parse subfields from the client.
603 This method returns True if the `using` attribute is not None, indicating
604 that this RelationalBone has a using-skeleton, and its subfields should
605 be parsed. Otherwise, it returns False.
607 :return: True if the using-skeleton is not None and subfields should be parsed, False otherwise.
608 :rtype: bool
609 """
610 return self.using is not None
612 def singleValueFromClient(self, value, skel, bone_name, client_data):
613 oldValues = skel[bone_name]
615 def restoreSkels(key, usingData, index=None):
616 refSkel, usingSkel = self._getSkels()
617 isEntryFromBackup = False # If the referenced entry has been deleted, restore information from backup
618 entry = None
619 dbKey = None
620 errors = []
621 try:
622 dbKey = db.keyHelper(key, self.kind)
623 entry = db.Get(dbKey)
624 assert entry
625 except: # Invalid key or something like that
626 logging.info(f"Invalid reference key >{key}< detected on bone '{bone_name}'")
627 if isinstance(oldValues, dict):
628 if oldValues["dest"]["key"] == dbKey:
629 entry = oldValues["dest"]
630 isEntryFromBackup = True
631 elif isinstance(oldValues, list):
632 for dbVal in oldValues:
633 if dbVal["dest"]["key"] == dbKey:
634 entry = dbVal["dest"]
635 isEntryFromBackup = True
636 if isEntryFromBackup:
637 refSkel = entry
638 elif entry:
639 refSkel.dbEntity = entry
640 for k in refSkel.keys():
641 # Unserialize all bones from refKeys, then drop dbEntity - otherwise all properties will be copied
642 _ = refSkel[k]
643 refSkel.dbEntity = None
644 else:
645 if index:
646 errors.append(
647 ReadFromClientError(ReadFromClientErrorSeverity.Invalid, "Invalid value submitted",
648 [str(index)]))
649 else:
650 errors.append(
651 ReadFromClientError(ReadFromClientErrorSeverity.Invalid, "Invalid value submitted"))
652 return None, None, errors # We could not parse this
653 if usingSkel:
654 if not usingSkel.fromClient(usingData):
655 usingSkel.errors.append(ReadFromClientError(ReadFromClientErrorSeverity.Invalid, "Incomplete data"))
656 if index:
657 for error in usingSkel.errors:
658 error.fieldPath.insert(0, str(index))
659 errors.extend(usingSkel.errors)
660 return refSkel, usingSkel, errors
662 if self.using and isinstance(value, dict):
663 usingData = value
664 destKey = usingData["key"]
665 del usingData["key"]
666 else:
667 destKey = value
668 usingData = None
669 assert isinstance(destKey, str)
670 refSkel, usingSkel, errors = restoreSkels(destKey, usingData)
671 if refSkel:
672 resVal = {"dest": refSkel, "rel": usingSkel}
673 err = self.isInvalid(resVal)
674 if err:
675 return self.getEmptyValue(), [ReadFromClientError(ReadFromClientErrorSeverity.Invalid, err)]
676 return resVal, errors
677 else:
678 return self.getEmptyValue(), errors
680 def _rewriteQuery(self, name, skel, dbFilter, rawFilter):
681 """
682 Rewrites a datastore query to operate on "viur-relations" instead of the original kind.
684 This method is needed to perform relational queries on n:m relations. It takes the original datastore query
685 and rewrites it to target the "viur-relations" kind. It also adjusts filters and sort orders accordingly.
687 :param str name: The name of the bone.
688 :param SkeletonInstance skel: The skeleton instance the bone is a part of.
689 :param viur.core.db.Query dbFilter: The original datastore query to be rewritten.
690 :param dict rawFilter: The raw filter applied to the original datastore query.
692 :return: A tuple containing the name, skeleton, rewritten query, and raw filter.
693 :rtype: Tuple[str, 'viur.core.skeleton.SkeletonInstance', 'viur.core.db.Query', dict]
695 :raises NotImplementedError: If the original query contains multiple filters with "IN" or "!=" operators.
696 :raises RuntimeError: If the filtering is invalid, e.g., using multiple key filters or querying
697 properties not in parentKeys.
698 """
699 origQueries = dbFilter.queries
700 if isinstance(origQueries, list):
701 raise NotImplementedError(
702 "Doing a relational Query with multiple=True and \"IN or !=\"-filters is currently unsupported!")
703 dbFilter.queries = db.QueryDefinition("viur-relations", {
704 "viur_src_kind =": skel.kindName,
705 "viur_dest_kind =": self.kind,
706 "viur_src_property =": name
708 }, orders=[], startCursor=origQueries.startCursor, endCursor=origQueries.endCursor)
709 for k, v in origQueries.filters.items(): # Merge old filters in
710 # Ensure that all non-relational-filters are in parentKeys
711 if k == db.KEY_SPECIAL_PROPERTY:
712 # We must process the key-property separately as its meaning changes as we change the datastore kind were querying
713 if isinstance(v, list) or isinstance(v, tuple):
714 logging.warning(f"Invalid filtering! Doing an relational Query on {name} with multiple key= "
715 f"filters is unsupported!")
716 raise RuntimeError()
717 if not isinstance(v, db.Key):
718 v = db.Key(v)
719 dbFilter.ancestor(v)
720 continue
721 boneName = k.split(".")[0].split(" ")[0]
722 if boneName not in self.parentKeys and boneName != "__key__":
723 logging.warning(f"Invalid filtering! {boneName} is not in parentKeys of RelationalBone {name}!")
724 raise RuntimeError()
725 dbFilter.filter(f"src.{k}", v)
726 orderList = []
727 for k, d in origQueries.orders: # Merge old sort orders in
728 if k == db.KEY_SPECIAL_PROPERTY:
729 orderList.append((f"{k}", d))
730 elif not k in self.parentKeys:
731 logging.warning(f"Invalid filtering! {k} is not in parentKeys of RelationalBone {name}!")
732 raise RuntimeError()
733 else:
734 orderList.append((f"src.{k}", d))
735 if orderList:
736 dbFilter.order(*orderList)
737 return name, skel, dbFilter, rawFilter
739 def buildDBFilter(
740 self,
741 name: str,
742 skel: "SkeletonInstance",
743 dbFilter: db.Query,
744 rawFilter: dict,
745 prefix: t.Optional[str] = None
746 ) -> db.Query:
747 """
748 Builds a datastore query by modifying the given filter based on the RelationalBone's properties.
750 This method takes a datastore query and modifies it according to the relational bone properties.
751 It also merges any related filters based on the 'refKeys' and 'using' attributes of the bone.
753 :param str name: The name of the bone.
754 :param SkeletonInstance skel: The skeleton instance the bone is a part of.
755 :param db.Query dbFilter: The original datastore query to be modified.
756 :param dict rawFilter: The raw filter applied to the original datastore query.
757 :param str prefix: Optional prefix to be applied to filter keys.
759 :return: The modified datastore query.
760 :rtype: db.Query
762 :raises RuntimeError: If the filtering is invalid, e.g., querying properties not in 'refKeys'
763 or not a bone in 'using'.
764 """
765 relSkel, _usingSkelCache = self._getSkels()
766 origQueries = dbFilter.queries
768 if origQueries is None: # This query is unsatisfiable
769 return dbFilter
771 myKeys = [x for x in rawFilter.keys() if x.startswith(f"{name}.")]
772 if len(myKeys) > 0: # We filter by some properties
773 if dbFilter.getKind() != "viur-relations" and self.multiple:
774 name, skel, dbFilter, rawFilter = self._rewriteQuery(name, skel, dbFilter, rawFilter)
776 # Merge the relational filters in
777 for myKey in myKeys:
778 value = rawFilter[myKey]
780 try:
781 unused, _type, key = myKey.split(".", 2)
782 assert _type in ["dest", "rel"]
783 except:
784 if self.using is None:
785 # This will be a "dest" query
786 _type = "dest"
787 try:
788 unused, key = myKey.split(".", 1)
789 except:
790 continue
791 else:
792 continue
794 # just use the first part of "key" to check against our refSkel / relSkel (strip any leading .something and $something)
795 checkKey = key
796 if "." in checkKey:
797 checkKey = checkKey.split(".")[0]
799 if "$" in checkKey:
800 checkKey = checkKey.split("$")[0]
802 if _type == "dest":
804 # Ensure that the relational-filter is in refKeys
805 if checkKey not in self._ref_keys:
806 logging.warning(f"Invalid filtering! {key} is not in refKeys of RelationalBone {name}!")
807 raise RuntimeError()
809 # Iterate our relSkel and let these bones write their filters in
810 for bname, bone in relSkel.items():
811 if checkKey == bname:
812 newFilter = {key: value}
813 if self.multiple:
814 bone.buildDBFilter(bname, relSkel, dbFilter, newFilter, prefix=(prefix or "") + "dest.")
815 else:
816 bone.buildDBFilter(bname, relSkel, dbFilter, newFilter,
817 prefix=(prefix or "") + name + ".dest.")
819 elif _type == "rel":
821 # Ensure that the relational-filter is in refKeys
822 if self.using is None or checkKey not in self.using():
823 logging.warning(f"Invalid filtering! {key} is not a bone in 'using' of {name}")
824 raise RuntimeError()
826 # Iterate our usingSkel and let these bones write their filters in
827 for bname, bone in self.using().items():
828 if key.startswith(bname):
829 newFilter = {key: value}
830 if self.multiple:
831 bone.buildDBFilter(bname, relSkel, dbFilter, newFilter, prefix=(prefix or "") + "rel.")
832 else:
833 bone.buildDBFilter(bname, relSkel, dbFilter, newFilter,
834 prefix=(prefix or "") + name + ".rel.")
836 if self.multiple:
837 dbFilter.setFilterHook(lambda s, filter, value: self.filterHook(name, s, filter, value))
838 dbFilter.setOrderHook(lambda s, orderings: self.orderHook(name, s, orderings))
840 elif name in rawFilter and isinstance(rawFilter[name], str) and rawFilter[name].lower() == "none":
841 dbFilter = dbFilter.filter(f"{name} =", None)
843 return dbFilter
845 def buildDBSort(
846 self,
847 name: str,
848 skel: "SkeletonInstance",
849 dbFilter: db.Query,
850 rawFilter: dict
851 ) -> t.Optional[db.Query]:
852 """
853 Builds a datastore query by modifying the given filter based on the RelationalBone's properties for sorting.
855 This method takes a datastore query and modifies its sorting behavior according to the relational bone
856 properties. It also checks if the sorting is valid based on the 'refKeys' and 'using' attributes of the bone.
858 :param str name: The name of the bone.
859 :param SkeletonInstance skel: The skeleton instance the bone is a part of.
860 :param db.Query dbFilter: The original datastore query to be modified.
861 :param dict rawFilter: The raw filter applied to the original datastore query.
863 :return: The modified datastore query with updated sorting behavior.
864 :rtype: t.Optional[db.Query]
866 :raises RuntimeError: If the sorting is invalid, e.g., using properties not in 'refKeys'
867 or not a bone in 'using'.
868 """
869 origFilter = dbFilter.queries
870 if origFilter is None or not "orderby" in rawFilter: # This query is unsatisfiable or not sorted
871 return dbFilter
872 if "orderby" in rawFilter and isinstance(rawFilter["orderby"], str) and rawFilter["orderby"].startswith(
873 f"{name}."):
874 if not dbFilter.getKind() == "viur-relations" and self.multiple: # This query has not been rewritten (yet)
875 name, skel, dbFilter, rawFilter = self._rewriteQuery(name, skel, dbFilter, rawFilter)
876 key = rawFilter["orderby"]
877 try:
878 unused, _type, param = key.split(".")
879 assert _type in ["dest", "rel"]
880 except:
881 return dbFilter # We cant parse that
882 # Ensure that the relational-filter is in refKeys
883 if _type == "dest" and param not in self._ref_keys:
884 logging.warning(f"Invalid filtering! {param} is not in refKeys of RelationalBone {name}!")
885 raise RuntimeError()
886 if _type == "rel" and (self.using is None or param not in self.using()):
887 logging.warning(f"Invalid filtering! {param} is not a bone in 'using' of {name}")
888 raise RuntimeError()
889 if self.multiple:
890 orderPropertyPath = f"{_type}.{param}"
891 else: # Also inject our bonename again
892 orderPropertyPath = f"{name}.{_type}.{param}"
893 if "orderdir" in rawFilter and rawFilter["orderdir"] == "1":
894 order = (orderPropertyPath, db.SortOrder.Descending)
895 elif "orderdir" in rawFilter and rawFilter["orderdir"] == "2":
896 order = (orderPropertyPath, db.SortOrder.InvertedAscending)
897 elif "orderdir" in rawFilter and rawFilter["orderdir"] == "3":
898 order = (orderPropertyPath, db.SortOrder.InvertedDescending)
899 else:
900 order = (orderPropertyPath, db.SortOrder.Ascending)
901 dbFilter = dbFilter.order(order)
902 if self.multiple:
903 dbFilter.setFilterHook(lambda s, filter, value: self.filterHook(name, s, filter, value))
904 dbFilter.setOrderHook(lambda s, orderings: self.orderHook(name, s, orderings))
905 return dbFilter
907 def filterHook(self, name, query, param, value): # FIXME
908 """
909 Hook installed by buildDbFilter that rewrites filters added to the query to match the layout of the
910 viur-relations index and performs sanity checks on the query.
912 This method rewrites and validates filters added to a datastore query after the `buildDbFilter` method
913 has been executed. It ensures that the filters are compatible with the structure of the viur-relations
914 index and checks if the query is possible.
916 :param str name: The name of the bone.
917 :param db.Query query: The datastore query to be modified.
918 :param str param: The filter parameter to be checked and potentially modified.
919 :param value: The value associated with the filter parameter.
921 :return: A tuple containing the modified filter parameter and its associated value, or None if
922 the filter parameter is a key special property.
923 :rtype: Tuple[str, Any] or None
925 :raises RuntimeError: If the filtering is invalid, e.g., using properties not in 'refKeys' or 'parentKeys'.
926 """
927 if param.startswith("src.") or param.startswith("dest.") or param.startswith("viur_"):
928 # This filter is already valid in our relation
929 return param, value
930 if param.startswith(f"{name}."):
931 # We add a constrain filtering by properties of the referenced entity
932 refKey = param.replace(f"{name}.", "")
933 if " " in refKey: # Strip >, < or = params
934 refKey = refKey[:refKey.find(" ")]
935 if refKey not in self._ref_keys:
936 logging.warning(f"Invalid filtering! {refKey} is not in refKeys of RelationalBone {name}!")
937 raise RuntimeError()
938 if self.multiple:
939 return param.replace(f"{name}.", "dest."), value
940 else:
941 return param, value
942 else:
943 # We filter by a property of this entity
944 if not self.multiple:
945 # Not relational, not multiple - nothing to do here
946 return param, value
947 # Prepend "src."
948 srcKey = param
949 if " " in srcKey:
950 srcKey = srcKey[: srcKey.find(" ")] # Cut <, >, and =
951 if srcKey == db.KEY_SPECIAL_PROPERTY: # Rewrite key= filter as its meaning has changed
952 if isinstance(value, list) or isinstance(value, tuple):
953 logging.warning(f"Invalid filtering! Doing an relational Query on {name} "
954 f"with multiple key= filters is unsupported!")
955 raise RuntimeError()
956 if not isinstance(value, db.Key):
957 value = db.Key(value)
958 query.ancestor(value)
959 return None
960 if srcKey not in self.parentKeys:
961 logging.warning(f"Invalid filtering! {srcKey} is not in parentKeys of RelationalBone {name}!")
962 raise RuntimeError()
963 return f"src.{param}", value
965 def orderHook(self, name: str, query: db.Query, orderings): # FIXME
966 """
967 Hook installed by buildDbFilter that rewrites orderings added to the query to match the layout of the
968 viur-relations index and performs sanity checks on the query.
970 This method rewrites and validates orderings added to a datastore query after the `buildDbFilter` method
971 has been executed. It ensures that the orderings are compatible with the structure of the viur-relations
972 index and checks if the query is possible.
974 :param name: The name of the bone.
975 :param query: The datastore query to be modified.
976 :param orderings: A list or tuple of orderings to be checked and potentially modified.
977 :type orderings: List[Union[str, Tuple[str, db.SortOrder]]] or Tuple[Union[str, Tuple[str, db.SortOrder]]]
979 :return: A list of modified orderings that are compatible with the viur-relations index.
980 :rtype: List[Union[str, Tuple[str, db.SortOrder]]]
982 :raises RuntimeError: If the ordering is invalid, e.g., using properties not in 'refKeys' or 'parentKeys'.
983 """
984 res = []
985 if not isinstance(orderings, list) and not isinstance(orderings, tuple):
986 orderings = [orderings]
987 for order in orderings:
988 if isinstance(order, tuple):
989 orderKey = order[0]
990 else:
991 orderKey = order
992 if orderKey.startswith("dest.") or orderKey.startswith("rel.") or orderKey.startswith("src."):
993 # This is already valid for our relational index
994 res.append(order)
995 continue
996 if orderKey.startswith(f"{name}."):
997 k = orderKey.replace(f"{name}.", "")
998 if k not in self._ref_keys:
999 logging.warning(f"Invalid ordering! {k} is not in refKeys of RelationalBone {name}!")
1000 raise RuntimeError()
1001 if not self.multiple:
1002 res.append(order)
1003 else:
1004 if isinstance(order, tuple):
1005 res.append((f"dest.{k}", order[1]))
1006 else:
1007 res.append(f"dest.{k}")
1008 else:
1009 if not self.multiple:
1010 # Nothing to do here
1011 res.append(order)
1012 continue
1013 else:
1014 if orderKey not in self.parentKeys:
1015 logging.warning(
1016 f"Invalid ordering! {orderKey} is not in parentKeys of RelationalBone {name}!")
1017 raise RuntimeError()
1018 if isinstance(order, tuple):
1019 res.append((f"src.{orderKey}", order[1]))
1020 else:
1021 res.append(f"src.{orderKey}")
1022 return res
1024 def refresh(self, skel: "SkeletonInstance", boneName: str):
1025 """
1026 Refreshes all values that might be cached from other entities in the provided skeleton.
1028 This method updates the cached values for relational bones in the provided skeleton, which
1029 correspond to other entities. It fetches the updated values for the relational bone's
1030 reference keys and replaces the cached values in the skeleton with the fetched values.
1032 :param SkeletonInstance skel: The skeleton containing the bone to be refreshed.
1033 :param str boneName: The name of the bone to be refreshed.
1034 """
1036 def updateInplace(relDict):
1037 """
1038 Fetches the entity referenced by valDict["dest.key"] and updates all dest.* keys
1039 accordingly
1040 """
1041 if not (isinstance(relDict, dict) and "dest" in relDict):
1042 logging.error(f"Invalid dictionary in updateInplace: {relDict}")
1043 return
1044 newValues = db.Get(db.keyHelper(relDict["dest"]["key"], self.kind))
1045 if newValues is None:
1046 logging.info(f"""The key {relDict["dest"]["key"]} does not exist""")
1047 return
1048 for boneName in self._ref_keys:
1049 if boneName != "key" and boneName in newValues:
1050 relDict["dest"].dbEntity[boneName] = newValues[boneName]
1052 if not skel[boneName] or self.updateLevel == RelationalUpdateLevel.OnValueAssignment:
1053 return
1055 # logging.debug("Refreshing RelationalBone %s of %s" % (boneName, skel.kindName))
1056 if isinstance(skel[boneName], dict) and "dest" not in skel[boneName]: # multi lang
1057 for l in skel[boneName]:
1058 if isinstance(skel[boneName][l], dict):
1059 updateInplace(skel[boneName][l])
1060 elif isinstance(skel[boneName][l], list):
1061 for k in skel[boneName][l]:
1062 updateInplace(k)
1063 else:
1064 if isinstance(skel[boneName], dict):
1065 updateInplace(skel[boneName])
1066 elif isinstance(skel[boneName], list):
1067 for k in skel[boneName]:
1068 updateInplace(k)
1070 def getSearchTags(self, skel: "SkeletonInstance", name: str) -> set[str]:
1071 """
1072 Retrieves the search tags for the given RelationalBone in the provided skeleton.
1074 This method iterates over the values of the relational bone and gathers search tags from the
1075 reference and using skeletons. It combines all the tags into a set to avoid duplicates.
1077 :param skel: The skeleton containing the bone for which search tags are to be retrieved.
1078 :param name: The name of the bone for which search tags are to be retrieved.
1080 :return: A set of search tags for the specified relational bone.
1081 """
1082 result = set()
1084 def get_values(skel_, values_cache):
1085 for key, bone in skel_.items():
1086 if not bone.searchable:
1087 continue
1088 for tag in bone.getSearchTags(values_cache, key):
1089 result.add(tag)
1091 ref_skel_cache, using_skel_cache = self._getSkels()
1092 for idx, lang, value in self.iter_bone_value(skel, name):
1093 if value is None:
1094 continue
1095 if value["dest"]:
1096 get_values(ref_skel_cache, value["dest"])
1097 if value["rel"]:
1098 get_values(using_skel_cache, value["rel"])
1100 return result
1102 def createRelSkelFromKey(self, key: t.Union[str, "db.Key"], rel: dict | None = None):
1103 """
1104 Creates a relSkel instance valid for this bone from the given database key.
1106 This method retrieves the entity corresponding to the provided key from the database, unserializes it
1107 into a reference skeleton, and returns a dictionary containing the reference skeleton and optional
1108 relation data.
1110 :param Union[str, db.Key] key: The database key of the entity for which a relSkel instance is to be created.
1111 :param Union[dict, None]rel: Optional relation data to be included in the resulting dictionary. Default is None.
1113 :return: A dictionary containing a reference skeleton and optional relation data.
1114 :rtype: dict
1115 """
1117 key = db.keyHelper(key, self.kind)
1118 entity = db.Get(key)
1119 if not entity:
1120 logging.error(f"Key {key} not found")
1121 return None
1122 relSkel = self._refSkelCache()
1123 relSkel.unserialize(entity)
1124 for k in relSkel.keys():
1125 # Unserialize all bones from refKeys, then drop dbEntity - otherwise all properties will be copied
1126 _ = relSkel[k]
1127 relSkel.dbEntity = None
1128 return {
1129 "dest": relSkel,
1130 "rel": rel or None
1131 }
1133 def setBoneValue(
1134 self,
1135 skel: "SkeletonInstance",
1136 boneName: str,
1137 value: t.Any,
1138 append: bool,
1139 language: None | str = None
1140 ) -> bool:
1141 """
1142 Sets the value of the specified bone in the given skeleton. Sanity checks are performed to ensure the
1143 value is valid. If the value is invalid, no modifications are made.
1145 :param skel: Dictionary with the current values from the skeleton we belong to.
1146 :param boneName: The name of the bone to be modified.
1147 :param value: The value to be assigned. The type depends on the bone type.
1148 :param append: If true, the given value is appended to the values of the bone instead of replacing it.
1149 Only supported on bones with multiple=True.
1150 :param language: Set/append for a specific language (optional). Required if the bone
1151 supports languages.
1153 :return: True if the operation succeeded, False otherwise.
1154 """
1155 assert not (bool(self.languages) ^ bool(language)), "Language is required or not supported"
1156 assert not append or self.multiple, "Can't append - bone is not multiple"
1157 if not self.multiple and not self.using:
1158 if not isinstance(value, (str, db.Key)):
1159 logging.error(value)
1160 logging.error(type(value))
1161 raise ValueError(f"You must supply exactly one Database-Key to {boneName}")
1162 realValue = (value, None)
1163 elif not self.multiple and self.using:
1164 if (
1165 not isinstance(value, tuple) or len(value) != 2
1166 or not isinstance(value[0], (str, db.Key))
1167 or not isinstance(value[1], self._skeletonInstanceClassRef)
1168 ):
1169 raise ValueError(f"You must supply a tuple of (Database-Key, relSkel) to {boneName}")
1170 realValue = value
1171 elif self.multiple and not self.using:
1172 if (
1173 not isinstance(value, (str, db.Key))
1174 and not (isinstance(value, list))
1175 and all(isinstance(k, (str, db.Key)) for k in value)
1176 ):
1177 raise ValueError(f"You must supply a Database-Key or a list hereof to {boneName}")
1178 if isinstance(value, list):
1179 realValue = [(x, None) for x in value]
1180 else:
1181 realValue = [(value, None)]
1182 else: # which means (self.multiple and self.using)
1183 if (
1184 not (isinstance(value, tuple) and len(value) == 2 and isinstance(value[0], (str, db.Key))
1185 and isinstance(value[1], self._skeletonInstanceClassRef))
1186 and not (isinstance(value, list)
1187 and all((isinstance(x, tuple) and len(x) == 2 and (isinstance(x[0], (str, db.Key)))
1188 and isinstance(x[1], self._skeletonInstanceClassRef) for x in value)))
1189 ):
1190 raise ValueError(f"You must supply (db.Key, RelSkel) or a list hereof to {boneName}")
1191 if not isinstance(value, list):
1192 realValue = [value]
1193 else:
1194 realValue = value
1195 if not self.multiple:
1196 rel = self.createRelSkelFromKey(realValue[0], realValue[1])
1197 if not rel:
1198 return False
1199 if language:
1200 if boneName not in skel or not isinstance(skel[boneName], dict):
1201 skel[boneName] = {}
1202 skel[boneName][language] = rel
1203 else:
1204 skel[boneName] = rel
1205 else:
1206 tmpRes = []
1207 for val in realValue:
1208 rel = self.createRelSkelFromKey(val[0], val[1])
1209 if not rel:
1210 return False
1211 tmpRes.append(rel)
1212 if append:
1213 if language:
1214 if boneName not in skel or not isinstance(skel[boneName], dict):
1215 skel[boneName] = {}
1216 if not isinstance(skel[boneName].get(language), list):
1217 skel[boneName][language] = []
1218 skel[boneName][language].extend(tmpRes)
1219 else:
1220 if boneName not in skel or not isinstance(skel[boneName], list):
1221 skel[boneName] = []
1222 skel[boneName].extend(tmpRes)
1223 else:
1224 if language:
1225 if boneName not in skel or not isinstance(skel[boneName], dict):
1226 skel[boneName] = {}
1227 skel[boneName][language] = tmpRes
1228 else:
1229 skel[boneName] = tmpRes
1230 return True
1232 def getReferencedBlobs(self, skel: "SkeletonInstance", name: str) -> set[str]:
1233 """
1234 Retrieves the set of referenced blobs from the specified bone in the given skeleton instance.
1236 :param SkeletonInstance skel: The skeleton instance to extract the referenced blobs from.
1237 :param str name: The name of the bone to retrieve the referenced blobs from.
1239 :return: A set containing the unique blob keys referenced by the specified bone.
1240 :rtype: Set[str]
1241 """
1242 result = set()
1243 for idx, lang, value in self.iter_bone_value(skel, name):
1244 if value is None:
1245 continue
1246 for key, bone_ in value["dest"].items():
1247 result.update(bone_.getReferencedBlobs(value["dest"], key))
1248 if value["rel"]:
1249 for key, bone_ in value["rel"].items():
1250 result.update(bone_.getReferencedBlobs(value["rel"], key))
1251 return result
1253 def getUniquePropertyIndexValues(self, valuesCache: dict, name: str) -> list[str]:
1254 """
1255 Generates unique property index values for the RelationalBone based on the referenced keys.
1256 Can be overridden if different behavior is required (e.g., examining values from `prop:usingSkel`).
1258 :param dict valuesCache: The cache containing the current values of the bone.
1259 :param str name: The name of the bone for which to generate unique property index values.
1261 :return: A list containing the unique property index values for the specified bone.
1262 :rtype: List[str]
1263 """
1264 value = valuesCache.get(name)
1265 if not value: # We don't have a value to lock
1266 return []
1267 if isinstance(value, dict):
1268 return self._hashValueForUniquePropertyIndex(value["dest"]["key"])
1269 elif isinstance(value, list):
1270 return self._hashValueForUniquePropertyIndex([x["dest"]["key"] for x in value])
1272 def structure(self) -> dict:
1273 return super().structure() | {
1274 "type": f"{self.type}.{self.kind}",
1275 "module": self.module,
1276 "format": self.format,
1277 "using": self.using().structure() if self.using else None,
1278 "relskel": self._refSkelCache().structure(),
1279 }