Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/bones/relational.py: 6%

581 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-27 07:59 +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 

12 

13from viur.core import db, utils 

14from viur.core.bones.base import BaseBone, ReadFromClientError, ReadFromClientErrorSeverity, getSystemInitialized 

15 

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 

18 

19 

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 """ 

35 

36 

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.""" 

47 

48 

49class RelDict(t.TypedDict): 

50 dest: "SkeletonInstance" 

51 rel: t.Optional["RelSkel"] 

52 

53class RelationalBone(BaseBone): 

54 """ 

55 The base class for all relational bones in the ViUR framework. 

56 RelationalBone is used to create and manage relationships between database entities. This class provides 

57 basic functionality and attributes that can be extended by other specialized relational bone classes, 

58 such as N1Relation, N2NRelation, and Hierarchy. 

59 This implementation prioritizes read efficiency and is suitable for situations where data is read more 

60 frequently than written. However, it comes with increased write operations when writing an entity to the 

61 database. The additional write operations depend on the type of relationship: multiple=True RelationalBones 

62 or 1:N relations. 

63 

64 The implementation does not instantly update relational information when a skeleton is updated; instead, 

65 it triggers a deferred task to update references. This may result in outdated data until the task is completed. 

66 

67 Note: Filtering a list by relational properties uses the outdated data. 

68 

69 Example: 

70 - Entity A references Entity B. 

71 - Both have a property "name." 

72 - Entity B is updated (its name changes). 

73 - Entity A's RelationalBone values still show Entity B's old name. 

74 

75 It is not recommended for cases where data is read less frequently than written, as there is no 

76 write-efficient method available yet. 

77 

78 :param kind: KindName of the referenced property. 

79 :param module: Name of the module which should be used to select entities of kind "kind". If not set, 

80 the value of "kind" will be used (the kindName must match the moduleName) 

81 :param refKeys: A list of properties to include from the referenced property. These properties will be 

82 available in the template without having to fetch the referenced property. Filtering is also only possible 

83 by properties named here! 

84 :param parentKeys: A list of properties from the current skeleton to include. If mixing filtering by 

85 relational properties and properties of the class itself, these must be named here. 

86 :param multiple: If True, allow referencing multiple Elements of the given class. (Eg. n:n-relation). 

87 Otherwise its n:1, (you can only select exactly one). It's possible to use a unique constraint on this 

88 bone, allowing for at-most-1:1 or at-most-1:n relations. Instead of true, it's also possible to use 

89 a ```class MultipleConstraints``` instead. 

90 

91 :param format: 

92 Hint for the frontend how to display such an relation. This is now a python expression 

93 evaluated by safeeval on the client side. The following values will be passed to the expression: 

94 

95 - value 

96 The value to display. This will be always a dict (= a single value) - even if the relation is 

97 multiple (in which case the expression is evaluated once per referenced entity) 

98 

99 - structure 

100 The structure of the skeleton this bone is part of as a dictionary as it's transferred to the 

101 fronted by the admin/vi-render. 

102 

103 - language 

104 The current language used by the frontend in ISO2 code (eg. "de"). This will be always set, even if 

105 the project did not enable the multi-language feature. 

106 

107 :param updateLevel: 

108 Indicates how ViUR should keep the values copied from the referenced entity into our 

109 entity up to date. If this bone is indexed, it's recommended to leave this set to 

110 RelationalUpdateLevel.Always, as filtering/sorting by this bone will produce stale results. 

111 

112 :param RelationalUpdateLevel.Always: 

113 

114 always update refkeys (old behavior). If the referenced entity is edited, ViUR will update this 

115 entity also (after a small delay, as these updates happen deferred) 

116 

117 :param RelationalUpdateLevel.OnRebuildSearchIndex: 

118 

119 update refKeys only on rebuildSearchIndex. If the referenced entity changes, this entity will 

120 remain unchanged (this RelationalBone will still have the old values), but it can be updated 

121 by either by editing this entity or running a rebuildSearchIndex over our kind. 

122 

123 :param RelationalUpdateLevel.OnValueAssignment: 

124 

125 update only if explicitly set. A rebuildSearchIndex will not trigger an update, this bone has to be 

126 explicitly modified (in an edit) to have it's values updated 

127 

128 :param consistency: 

129 Can be used to implement SQL-like constrains on this relation. Possible values are: 

130 - RelationalConsistency.Ignore 

131 If the referenced entity gets deleted, this bone will not change. It will still reflect the old 

132 values. This will be even be preserved over edits, however if that referenced value is once 

133 deleted by the user (assigning a different value to this bone or removing that value of the list 

134 of relations if we are multiple) there's no way of restoring it 

135 

136 - RelationalConsistency.PreventDeletion 

137 Will prevent deleting the referenced entity as long as it's selected in this bone (calling 

138 skel.delete() on the referenced entity will raise errors.Locked). It's still (technically) 

139 possible to remove the underlying datastore entity using db.Delete manually, but this *must not* 

140 be used on a skeleton object as it will leave a whole bunch of references in a stale state. 

141 

142 - RelationalConsistency.SetNull 

143 Will set this bone to None (or remove the relation from the list in 

144 case we are multiple) when the referenced entity is deleted. 

145 

146 - RelationalConsistency.CascadeDeletion: 

147 (Dangerous!) Will delete this entity when the referenced entity is deleted. Warning: Unlike 

148 relational updates this will cascade. If Entity A references B with CascadeDeletion set, and 

149 B references C also with CascadeDeletion; if C gets deleted, both B and A will be deleted as well. 

150 

151 """ 

152 type = "relational" 

153 kind = None 

154 

155 def __init__( 

156 self, 

157 *, 

158 consistency: RelationalConsistency = RelationalConsistency.Ignore, 

159 format: str = "$(dest.name)", 

160 kind: str = None, 

161 module: t.Optional[str] = None, 

162 parentKeys: t.Optional[t.Iterable[str]] = {"name"}, 

163 refKeys: t.Optional[t.Iterable[str]] = {"name"}, 

164 updateLevel: RelationalUpdateLevel = RelationalUpdateLevel.Always, 

165 using: t.Optional["RelSkel"] = None, 

166 **kwargs 

167 ): 

168 """ 

169 Initialize a new RelationalBone. 

170 

171 :param kind: 

172 KindName of the referenced property. 

173 :param module: 

174 Name of the module which should be used to select entities of kind "type". If not set, 

175 the value of "type" will be used (the kindName must match the moduleName) 

176 :param refKeys: 

177 An iterable of properties to include from the referenced property. These properties will be 

178 available in the template without having to fetch the referenced property. Filtering is also only 

179 possible by properties named here! 

180 :param parentKeys: 

181 An iterable of properties from the current skeleton to include. If mixing filtering by 

182 relational properties and properties of the class itself, these must be named here. 

183 :param multiple: 

184 If True, allow referencing multiple Elements of the given class. (Eg. n:n-relation). 

185 Otherwise its n:1, (you can only select exactly one). It's possible to use a unique constraint on this 

186 bone, allowing for at-most-1:1 or at-most-1:n relations. Instead of true, it's also possible to use 

187 a :class:MultipleConstraints instead. 

188 

189 :param format: Hint for the frontend how to display such an relation. This is now a python expression 

190 evaluated by safeeval on the client side. The following values will be passed to the expression 

191 

192 :param value: 

193 The value to display. This will be always a dict (= a single value) - even if the 

194 relation is multiple (in which case the expression is evaluated once per referenced entity) 

195 :param structure: 

196 The structure of the skeleton this bone is part of as a dictionary as it's 

197 transferred to the fronted by the admin/vi-render. 

198 :param language: 

199 The current language used by the frontend in ISO2 code (eg. "de"). This will be 

200 always set, even if the project did not enable the multi-language feature. 

201 

202 :param updateLevel: 

203 Indicates how ViUR should keep the values copied from the referenced entity into our 

204 entity up to date. If this bone is indexed, it's recommended to leave this set to 

205 RelationalUpdateLevel.Always, as filtering/sorting by this bone will produce stale results. 

206 

207 :param RelationalUpdateLevel.Always: 

208 always update refkeys (old behavior). If the referenced entity is edited, ViUR will update this 

209 entity also (after a small delay, as these updates happen deferred) 

210 :param RelationalUpdateLevel.OnRebuildSearchIndex: 

211 update refKeys only on rebuildSearchIndex. If the 

212 referenced entity changes, this entity will remain unchanged 

213 (this RelationalBone will still have the old values), but it can be updated 

214 by either by editing this entity or running a rebuildSearchIndex over our kind. 

215 :param RelationalUpdateLevel.OnValueAssignment: 

216 update only if explicitly set. A rebuildSearchIndex will not trigger 

217 an update, this bone has to be explicitly modified (in an edit) to have it's values updated 

218 

219 :param consistency: 

220 Can be used to implement SQL-like constrains on this relation. 

221 

222 :param RelationalConsistency.Ignore: 

223 If the referenced entity gets deleted, this bone will not change. It 

224 will still reflect the old values. This will be even be preserved over edits, however if that 

225 referenced value is once deleted by the user (assigning a different value to this bone or 

226 removing that value of the list of relations if we are multiple) there's no way of restoring it 

227 

228 :param RelationalConsistency.PreventDeletion: 

229 Will prevent deleting the referenced entity as long as it's 

230 selected in this bone (calling skel.delete() on the referenced entity will raise errors.Locked). 

231 It's still (technically) possible to remove the underlying datastore entity using db.Delete 

232 manually, but this *must not* be used on a skeleton object as it will leave a whole bunch of 

233 references in a stale state. 

234 

235 :param RelationalConsistency.SetNull: 

236 Will set this bone to None (or remove the relation from the list in 

237 case we are multiple) when the referenced entity is deleted. 

238 

239 :param RelationalConsistency.CascadeDeletion: 

240 (Dangerous!) Will delete this entity when the referenced entity 

241 is deleted. Warning: Unlike relational updates this will cascade. If Entity A references B with 

242 CascadeDeletion set, and B references C also with CascadeDeletion; if C gets deleted, both B and 

243 A will be deleted as well. 

244 """ 

245 super().__init__(**kwargs) 

246 self.format = format 

247 

248 if kind: 

249 self.kind = kind 

250 

251 if module: 

252 self.module = module 

253 elif self.kind: 

254 self.module = self.kind 

255 

256 if self.kind is None or self.module is None: 

257 raise NotImplementedError("'kind' and 'module' of RelationalBone must not be None") 

258 

259 # Referenced keys 

260 self.refKeys = {"key"} 

261 if refKeys: 

262 self.refKeys |= set(refKeys) 

263 

264 # Parent keys 

265 self.parentKeys = {"key"} 

266 if parentKeys: 

267 self.parentKeys |= set(parentKeys) 

268 

269 self.using = using 

270 

271 # FIXME: Remove in VIUR4!! 

272 if isinstance(updateLevel, int): 

273 msg = f"parameter updateLevel={updateLevel} in RelationalBone is deprecated. " \ 

274 f"Please use the RelationalUpdateLevel enum instead" 

275 logging.warning(msg, stacklevel=3) 

276 warnings.warn(msg, DeprecationWarning, stacklevel=3) 

277 

278 assert 0 <= updateLevel < 3 

279 for n in RelationalUpdateLevel: 

280 if updateLevel == n.value: 

281 updateLevel = n 

282 

283 self.updateLevel = updateLevel 

284 self.consistency = consistency 

285 

286 if getSystemInitialized(): 

287 from viur.core.skeleton import RefSkel, SkeletonInstance 

288 self._refSkelCache = RefSkel.fromSkel(self.kind, *self.refKeys) 

289 self._skeletonInstanceClassRef = SkeletonInstance 

290 self._ref_keys = set(self._refSkelCache.__boneMap__.keys()) 

291 

292 def setSystemInitialized(self): 

293 """ 

294 Set the system initialized for the current class and cache the RefSkel and SkeletonInstance. 

295 

296 This method calls the superclass's setSystemInitialized method and initializes the RefSkel 

297 and SkeletonInstance classes. The RefSkel is created from the current kind and refKeys, 

298 while the SkeletonInstance class is stored as a reference. 

299 

300 :rtype: None 

301 """ 

302 super().setSystemInitialized() 

303 from viur.core.skeleton import RefSkel, SkeletonInstance 

304 

305 try: 

306 self._refSkelCache = RefSkel.fromSkel(self.kind, *self.refKeys) 

307 except AssertionError: 

308 raise NotImplementedError( 

309 f"Skeleton {self.skel_cls!r} {self.__class__.__name__} {self.name!r}: Kind {self.kind!r} unknown" 

310 ) 

311 

312 self._skeletonInstanceClassRef = SkeletonInstance 

313 self._ref_keys = set(self._refSkelCache.__boneMap__.keys()) 

314 

315 def _getSkels(self): 

316 """ 

317 Retrieve the reference skeleton and the 'using' skeleton for the current RelationalBone instance. 

318 

319 This method returns a tuple containing the reference skeleton (RefSkel) and the 'using' skeleton 

320 (UsingSkel) associated with the current RelationalBone instance. The 'using' skeleton is only 

321 retrieved if the 'using' attribute is defined. 

322 

323 :return: A tuple containing the reference skeleton and the 'using' skeleton. 

324 :rtype: tuple 

325 """ 

326 refSkel = self._refSkelCache() 

327 usingSkel = self.using() if self.using else None 

328 return refSkel, usingSkel 

329 

330 def singleValueUnserialize(self, val): 

331 """ 

332 Restore a value, including the Rel- and Using-Skeleton, from the serialized data read from the datastore. 

333 

334 This method takes a serialized value from the datastore, deserializes it, and returns the corresponding 

335 value with restored RelSkel and Using-Skel. It also handles ViUR 2 compatibility by handling string values. 

336 

337 :param val: A JSON-encoded datastore property. 

338 :type val: str or dict 

339 :return: The deserialized value with restored RelSkel and Using-Skel. 

340 :rtype: dict 

341 

342 :raises AssertionError: If the deserialized value is not a dictionary. 

343 """ 

344 

345 def fixFromDictToEntry(inDict): 

346 """ 

347 Convert a dictionary to an entry with properly restored keys and values. 

348 

349 :param dict inDict: The input dictionary to convert. 

350 : return: The resulting entry. 

351 :rtype: dict 

352 """ 

353 if not isinstance(inDict, dict): 

354 return None 

355 res = {} 

356 if "dest" in inDict: 

357 res["dest"] = db.Entity() 

358 for k, v in inDict["dest"].items(): 

359 res["dest"][k] = v 

360 if "key" in res["dest"]: 

361 res["dest"].key = utils.normalizeKey(db.Key.from_legacy_urlsafe(res["dest"]["key"])) 

362 if "rel" in inDict and inDict["rel"]: 

363 res["rel"] = db.Entity() 

364 for k, v in inDict["rel"].items(): 

365 res["rel"][k] = v 

366 else: 

367 res["rel"] = None 

368 return res 

369 

370 if isinstance(val, str): # ViUR2 compatibility 

371 try: 

372 value = json.loads(val) 

373 if isinstance(value, list): 

374 value = [fixFromDictToEntry(x) for x in value] 

375 elif isinstance(value, dict): 

376 value = fixFromDictToEntry(value) 

377 else: 

378 value = None 

379 except ValueError: 

380 value = None 

381 else: 

382 value = val 

383 if not value: 

384 return None 

385 elif isinstance(value, list) and value: 

386 value = value[0] 

387 assert isinstance(value, dict), f"Read something from the datastore thats not a dict: {type(value)}" 

388 if "dest" not in value: 

389 return None 

390 relSkel, usingSkel = self._getSkels() 

391 relSkel.unserialize(value["dest"]) 

392 if self.using is not None: 

393 usingSkel.unserialize(value["rel"] or db.Entity()) 

394 usingData = usingSkel 

395 else: 

396 usingData = None 

397 return {"dest": relSkel, "rel": usingData} 

398 

399 def serialize(self, skel: "SkeletonInstance", name: str, parentIndexed: bool) -> bool: 

400 """ 

401 Serialize the RelationalBone for the given skeleton, updating relational locks as necessary. 

402 

403 This method serializes the RelationalBone values for a given skeleton and stores the serialized 

404 values in the skeleton's dbEntity. It also updates the relational locks, adding new locks and 

405 removing old ones as needed. 

406 

407 :param SkeletonInstance skel: The skeleton instance containing the values to be serialized. 

408 :param str name: The name of the bone to be serialized. 

409 :param bool parentIndexed: A flag indicating whether the parent bone is indexed. 

410 :return: True if the serialization is successful, False otherwise. 

411 :rtype: bool 

412 

413 :raises AssertionError: If a programming error is detected. 

414 """ 

415 

416 def serialize_dest_rel(in_value: dict | None = None) -> (dict | None, dict | None): 

417 if not in_value: 

418 return None, None 

419 if dest_val := in_value.get("dest"): 

420 ref_data_serialized = dest_val.serialize(parentIndexed=indexed) 

421 else: 

422 ref_data_serialized = None 

423 if rel_data := in_value.get("rel"): 

424 using_data_serialized = rel_data.serialize(parentIndexed=indexed) 

425 else: 

426 using_data_serialized = None 

427 

428 return using_data_serialized, ref_data_serialized 

429 

430 

431 super().serialize(skel, name, parentIndexed) 

432 

433 # Clean old properties from entry (prevent name collision) 

434 for key in tuple(skel.dbEntity.keys()): 

435 if key.startswith(f"{name}."): 

436 del skel.dbEntity[key] 

437 

438 indexed = self.indexed and parentIndexed 

439 

440 if not (new_vals := skel.accessedValues.get(name)): 

441 return False 

442 elif self.languages: 

443 res = {"_viurLanguageWrapper_": True} 

444 for language in self.languages: 

445 if language in new_vals: 

446 if self.multiple: 

447 res[language] = [] 

448 for val in new_vals[language]: 

449 using_data, ref_data = serialize_dest_rel(val) 

450 res[language].append({"rel": using_data, "dest": ref_data}) 

451 else: 

452 if (val := new_vals[language]) and val["dest"]: 

453 using_data, ref_data = serialize_dest_rel(val) 

454 res[language] = {"rel": using_data, "dest": ref_data} 

455 elif self.multiple: 

456 res = [] 

457 for val in new_vals: 

458 using_data, ref_data = serialize_dest_rel(val) 

459 res.append({"rel": using_data, "dest": ref_data}) 

460 else: 

461 using_data, ref_data = serialize_dest_rel(new_vals) 

462 res = {"rel": using_data, "dest": ref_data} 

463 skel.dbEntity[name] = res 

464 

465 # Ensure our indexed flag is up2date 

466 if indexed and name in skel.dbEntity.exclude_from_indexes: 

467 skel.dbEntity.exclude_from_indexes.discard(name) 

468 elif not indexed and name not in skel.dbEntity.exclude_from_indexes: 

469 skel.dbEntity.exclude_from_indexes.add(name) 

470 

471 # Delete legacy property (PR #1244) #TODO: Remove in ViUR4 

472 skel.dbEntity.pop(f"{name}_outgoingRelationalLocks", None) 

473 

474 return True 

475 

476 def _get_single_destinct_hash(self, value): 

477 parts = [value["dest"]["key"]] 

478 

479 if self.using: 

480 for name, bone in self.using.__boneMap__.items(): 

481 parts.append(bone._get_destinct_hash(value["rel"][name])) 

482 

483 return tuple(parts) 

484 

485 def postSavedHandler(self, skel, boneName, key) -> None: 

486 """ 

487 Handle relational updates after a skeleton is saved. 

488 

489 This method updates, removes, or adds relations between the saved skeleton and the referenced entities. 

490 It also takes care of updating the relational properties and consistency levels. 

491 

492 :param skel: The saved skeleton instance. 

493 :param boneName: The name of the relational bone. 

494 :param key: The key of the saved skeleton instance. 

495 """ 

496 if key is None: # RelSkel container (e.g. RecordBone) has no key, it's covered by it's parent 

497 return 

498 if not skel[boneName]: 

499 values = [] 

500 elif self.multiple and self.languages: 

501 values = chain(*skel[boneName].values()) 

502 elif self.languages: 

503 values = list(skel[boneName].values()) 

504 elif self.multiple: 

505 values = skel[boneName] 

506 else: 

507 values = [skel[boneName]] 

508 values = [x for x in values if x is not None] 

509 parentValues = db.Entity() 

510 srcEntity = skel.dbEntity 

511 parentValues.key = srcEntity.key 

512 for boneKey in (self.parentKeys or []): 

513 if boneKey == "key": # this is a relcit from viur2, as the key is encoded in the embedded entity 

514 continue 

515 parentValues[boneKey] = srcEntity.get(boneKey) 

516 dbVals = db.Query("viur-relations") 

517 dbVals.filter("viur_src_kind =", skel.kindName) 

518 dbVals.filter("viur_dest_kind =", self.kind) 

519 dbVals.filter("viur_src_property =", boneName) 

520 dbVals.filter("src.__key__ =", key) 

521 for dbObj in dbVals.iter(): 

522 try: 

523 if not dbObj["dest"].key in [x["dest"]["key"] for x in values]: # Relation has been removed 

524 db.Delete(dbObj.key) 

525 continue 

526 except: # This entry is corrupt 

527 db.Delete(dbObj.key) 

528 else: # Relation: Updated 

529 data = [x for x in values if x["dest"]["key"] == dbObj["dest"].key][0] 

530 # Write our (updated) values in 

531 refSkel = data["dest"] 

532 dbObj["dest"] = refSkel.serialize(parentIndexed=True) 

533 dbObj["src"] = parentValues 

534 if self.using is not None: 

535 usingSkel = data["rel"] 

536 dbObj["rel"] = usingSkel.serialize(parentIndexed=True) 

537 dbObj["viur_delayed_update_tag"] = time() 

538 dbObj["viur_relational_updateLevel"] = self.updateLevel.value 

539 dbObj["viur_relational_consistency"] = self.consistency.value 

540 dbObj["viur_foreign_keys"] = list(self.refKeys) 

541 dbObj["viurTags"] = srcEntity.get("viurTags") # Copy tags over so we can still use our searchengine 

542 db.Put(dbObj) 

543 values.remove(data) 

544 # Add any new Relation 

545 for val in values: 

546 dbObj = db.Entity(db.Key("viur-relations", parent=key)) 

547 refSkel = val["dest"] 

548 dbObj["dest"] = refSkel.serialize(parentIndexed=True) 

549 dbObj["src"] = parentValues 

550 if self.using is not None: 

551 usingSkel = val["rel"] 

552 dbObj["rel"] = usingSkel.serialize(parentIndexed=True) 

553 dbObj["viur_delayed_update_tag"] = time() 

554 dbObj["viur_src_kind"] = skel.kindName # The kind of the entry referencing 

555 dbObj["viur_src_property"] = boneName # The key of the bone referencing 

556 dbObj["viur_dest_kind"] = self.kind 

557 dbObj["viur_relational_updateLevel"] = self.updateLevel.value 

558 dbObj["viur_relational_consistency"] = self.consistency.value 

559 dbObj["viur_foreign_keys"] = list(self._ref_keys) 

560 db.Put(dbObj) 

561 

562 def postDeletedHandler(self, skel: "SkeletonInstance", boneName: str, key: db.Key) -> None: 

563 """ 

564 Handle relational updates after a skeleton is deleted. 

565 

566 This method deletes all relations associated with the deleted skeleton and the referenced entities 

567 for the given relational bone. 

568 

569 :param skel: The deleted SkeletonInstance. 

570 :param boneName: The name of the RelationalBone in the Skeleton. 

571 :param key: The key of the deleted Entity. 

572 """ 

573 query = db.Query("viur-relations") 

574 query.filter("viur_src_kind =", skel.kindName) 

575 query.filter("viur_dest_kind =", self.kind) 

576 query.filter("viur_src_property =", boneName) 

577 query.filter("src.__key__ =", key) 

578 db.Delete([entity for entity in query.run()]) 

579 

580 def isInvalid(self, key) -> None: 

581 """ 

582 Check if the given key is invalid for this relational bone. 

583 

584 This method always returns None, as the actual validation of the key 

585 is performed in other methods of the RelationalBone class. 

586 

587 :param key: The key to be checked for validity. 

588 :return: None, as the actual validation is performed elsewhere. 

589 """ 

590 return None 

591 

592 def parseSubfieldsFromClient(self): 

593 """ 

594 Determine if the RelationalBone should parse subfields from the client. 

595 

596 This method returns True if the `using` attribute is not None, indicating 

597 that this RelationalBone has a using-skeleton, and its subfields should 

598 be parsed. Otherwise, it returns False. 

599 

600 :return: True if the using-skeleton is not None and subfields should be parsed, False otherwise. 

601 :rtype: bool 

602 """ 

603 return self.using is not None 

604 

605 def singleValueFromClient(self, value, skel, bone_name, client_data): 

606 oldValues = skel[bone_name] 

607 

608 def restoreSkels(key, usingData, index=None): 

609 refSkel, usingSkel = self._getSkels() 

610 isEntryFromBackup = False # If the referenced entry has been deleted, restore information from backup 

611 entry = None 

612 dbKey = None 

613 errors = [] 

614 try: 

615 dbKey = db.keyHelper(key, self.kind) 

616 entry = db.Get(dbKey) 

617 assert entry 

618 except: # Invalid key or something like that 

619 logging.info(f"Invalid reference key >{key}< detected on bone '{bone_name}'") 

620 if isinstance(oldValues, dict): 

621 if oldValues["dest"]["key"] == dbKey: 

622 entry = oldValues["dest"] 

623 isEntryFromBackup = True 

624 elif isinstance(oldValues, list): 

625 for dbVal in oldValues: 

626 if dbVal["dest"]["key"] == dbKey: 

627 entry = dbVal["dest"] 

628 isEntryFromBackup = True 

629 if isEntryFromBackup: 

630 refSkel = entry 

631 elif entry: 

632 refSkel.dbEntity = entry 

633 for k in refSkel.keys(): 

634 # Unserialize all bones from refKeys, then drop dbEntity - otherwise all properties will be copied 

635 _ = refSkel[k] 

636 refSkel.dbEntity = None 

637 else: 

638 if index: 

639 errors.append( 

640 ReadFromClientError(ReadFromClientErrorSeverity.Invalid, "Invalid value submitted", 

641 [str(index)])) 

642 else: 

643 errors.append( 

644 ReadFromClientError(ReadFromClientErrorSeverity.Invalid, "Invalid value submitted")) 

645 return None, None, errors # We could not parse this 

646 if usingSkel: 

647 if not usingSkel.fromClient(usingData): 

648 usingSkel.errors.append(ReadFromClientError(ReadFromClientErrorSeverity.Invalid, "Incomplete data")) 

649 if index: 

650 for error in usingSkel.errors: 

651 error.fieldPath.insert(0, str(index)) 

652 errors.extend(usingSkel.errors) 

653 return refSkel, usingSkel, errors 

654 

655 if self.using and isinstance(value, dict): 

656 usingData = value 

657 destKey = usingData["key"] 

658 del usingData["key"] 

659 else: 

660 destKey = value 

661 usingData = None 

662 

663 destKey = str(destKey) 

664 

665 refSkel, usingSkel, errors = restoreSkels(destKey, usingData) 

666 if refSkel: 

667 resVal = {"dest": refSkel, "rel": usingSkel} 

668 err = self.isInvalid(resVal) 

669 if err: 

670 return self.getEmptyValue(), [ReadFromClientError(ReadFromClientErrorSeverity.Invalid, err)] 

671 return resVal, errors 

672 else: 

673 return self.getEmptyValue(), errors 

674 

675 def _rewriteQuery(self, name, skel, dbFilter, rawFilter): 

676 """ 

677 Rewrites a datastore query to operate on "viur-relations" instead of the original kind. 

678 

679 This method is needed to perform relational queries on n:m relations. It takes the original datastore query 

680 and rewrites it to target the "viur-relations" kind. It also adjusts filters and sort orders accordingly. 

681 

682 :param str name: The name of the bone. 

683 :param SkeletonInstance skel: The skeleton instance the bone is a part of. 

684 :param viur.core.db.Query dbFilter: The original datastore query to be rewritten. 

685 :param dict rawFilter: The raw filter applied to the original datastore query. 

686 

687 :return: A tuple containing the name, skeleton, rewritten query, and raw filter. 

688 :rtype: Tuple[str, 'viur.core.skeleton.SkeletonInstance', 'viur.core.db.Query', dict] 

689 

690 :raises NotImplementedError: If the original query contains multiple filters with "IN" or "!=" operators. 

691 :raises RuntimeError: If the filtering is invalid, e.g., using multiple key filters or querying 

692 properties not in parentKeys. 

693 """ 

694 origQueries = dbFilter.queries 

695 if isinstance(origQueries, list): 

696 raise NotImplementedError( 

697 "Doing a relational Query with multiple=True and \"IN or !=\"-filters is currently unsupported!") 

698 dbFilter.queries = db.QueryDefinition("viur-relations", { 

699 "viur_src_kind =": skel.kindName, 

700 "viur_dest_kind =": self.kind, 

701 "viur_src_property =": name 

702 

703 }, orders=[], startCursor=origQueries.startCursor, endCursor=origQueries.endCursor) 

704 for k, v in origQueries.filters.items(): # Merge old filters in 

705 # Ensure that all non-relational-filters are in parentKeys 

706 if k == db.KEY_SPECIAL_PROPERTY: 

707 # We must process the key-property separately as its meaning changes as we change the datastore kind were querying 

708 if isinstance(v, list) or isinstance(v, tuple): 

709 logging.warning(f"Invalid filtering! Doing an relational Query on {name} with multiple key= " 

710 f"filters is unsupported!") 

711 raise RuntimeError() 

712 if not isinstance(v, db.Key): 

713 v = db.Key(v) 

714 dbFilter.ancestor(v) 

715 continue 

716 boneName = k.split(".")[0].split(" ")[0] 

717 if boneName not in self.parentKeys and boneName != "__key__": 

718 logging.warning(f"Invalid filtering! {boneName} is not in parentKeys of RelationalBone {name}!") 

719 raise RuntimeError() 

720 dbFilter.filter(f"src.{k}", v) 

721 orderList = [] 

722 for k, d in origQueries.orders: # Merge old sort orders in 

723 if k == db.KEY_SPECIAL_PROPERTY: 

724 orderList.append((f"{k}", d)) 

725 elif not k in self.parentKeys: 

726 logging.warning(f"Invalid filtering! {k} is not in parentKeys of RelationalBone {name}!") 

727 raise RuntimeError() 

728 else: 

729 orderList.append((f"src.{k}", d)) 

730 if orderList: 

731 dbFilter.order(*orderList) 

732 return name, skel, dbFilter, rawFilter 

733 

734 def buildDBFilter( 

735 self, 

736 name: str, 

737 skel: "SkeletonInstance", 

738 dbFilter: db.Query, 

739 rawFilter: dict, 

740 prefix: t.Optional[str] = None 

741 ) -> db.Query: 

742 """ 

743 Builds a datastore query by modifying the given filter based on the RelationalBone's properties. 

744 

745 This method takes a datastore query and modifies it according to the relational bone properties. 

746 It also merges any related filters based on the 'refKeys' and 'using' attributes of the bone. 

747 

748 :param str name: The name of the bone. 

749 :param SkeletonInstance skel: The skeleton instance the bone is a part of. 

750 :param db.Query dbFilter: The original datastore query to be modified. 

751 :param dict rawFilter: The raw filter applied to the original datastore query. 

752 :param str prefix: Optional prefix to be applied to filter keys. 

753 

754 :return: The modified datastore query. 

755 :rtype: db.Query 

756 

757 :raises RuntimeError: If the filtering is invalid, e.g., querying properties not in 'refKeys' 

758 or not a bone in 'using'. 

759 """ 

760 relSkel, _usingSkelCache = self._getSkels() 

761 origQueries = dbFilter.queries 

762 

763 if origQueries is None: # This query is unsatisfiable 

764 return dbFilter 

765 

766 myKeys = [x for x in rawFilter.keys() if x.startswith(f"{name}.")] 

767 if len(myKeys) > 0: # We filter by some properties 

768 if dbFilter.getKind() != "viur-relations" and self.multiple: 

769 name, skel, dbFilter, rawFilter = self._rewriteQuery(name, skel, dbFilter, rawFilter) 

770 

771 # Merge the relational filters in 

772 for myKey in myKeys: 

773 value = rawFilter[myKey] 

774 

775 try: 

776 unused, _type, key = myKey.split(".", 2) 

777 assert _type in ["dest", "rel"] 

778 except: 

779 if self.using is None: 

780 # This will be a "dest" query 

781 _type = "dest" 

782 try: 

783 unused, key = myKey.split(".", 1) 

784 except: 

785 continue 

786 else: 

787 continue 

788 

789 # just use the first part of "key" to check against our refSkel / relSkel (strip any leading .something and $something) 

790 checkKey = key 

791 if "." in checkKey: 

792 checkKey = checkKey.split(".")[0] 

793 

794 if "$" in checkKey: 

795 checkKey = checkKey.split("$")[0] 

796 

797 if _type == "dest": 

798 

799 # Ensure that the relational-filter is in refKeys 

800 if checkKey not in self._ref_keys: 

801 logging.warning(f"Invalid filtering! {key} is not in refKeys of RelationalBone {name}!") 

802 raise RuntimeError() 

803 

804 # Iterate our relSkel and let these bones write their filters in 

805 for bname, bone in relSkel.items(): 

806 if checkKey == bname: 

807 newFilter = {key: value} 

808 if self.multiple: 

809 bone.buildDBFilter(bname, relSkel, dbFilter, newFilter, prefix=(prefix or "") + "dest.") 

810 else: 

811 bone.buildDBFilter(bname, relSkel, dbFilter, newFilter, 

812 prefix=(prefix or "") + name + ".dest.") 

813 

814 elif _type == "rel": 

815 

816 # Ensure that the relational-filter is in refKeys 

817 if self.using is None or checkKey not in self.using(): 

818 logging.warning(f"Invalid filtering! {key} is not a bone in 'using' of {name}") 

819 raise RuntimeError() 

820 

821 # Iterate our usingSkel and let these bones write their filters in 

822 for bname, bone in self.using().items(): 

823 if key.startswith(bname): 

824 newFilter = {key: value} 

825 if self.multiple: 

826 bone.buildDBFilter(bname, relSkel, dbFilter, newFilter, prefix=(prefix or "") + "rel.") 

827 else: 

828 bone.buildDBFilter(bname, relSkel, dbFilter, newFilter, 

829 prefix=(prefix or "") + name + ".rel.") 

830 

831 if self.multiple: 

832 dbFilter.setFilterHook(lambda s, filter, value: self.filterHook(name, s, filter, value)) 

833 dbFilter.setOrderHook(lambda s, orderings: self.orderHook(name, s, orderings)) 

834 

835 elif name in rawFilter and isinstance(rawFilter[name], str) and rawFilter[name].lower() == "none": 

836 dbFilter = dbFilter.filter(f"{name} =", None) 

837 

838 return dbFilter 

839 

840 def buildDBSort( 

841 self, 

842 name: str, 

843 skel: "SkeletonInstance", 

844 query: db.Query, 

845 params: dict, 

846 postfix: str = "", 

847 ) -> t.Optional[db.Query]: 

848 """ 

849 Builds a datastore query by modifying the given filter based on the RelationalBone's properties for sorting. 

850 

851 This method takes a datastore query and modifies its sorting behavior according to the relational bone 

852 properties. It also checks if the sorting is valid based on the 'refKeys' and 'using' attributes of the bone. 

853 

854 :param name: The name of the bone. 

855 :param skel: The skeleton instance the bone is a part of. 

856 :param query: The original datastore query to be modified. 

857 :param params: The raw filter applied to the original datastore query. 

858 

859 :return: The modified datastore query with updated sorting behavior. 

860 :rtype: t.Optional[db.Query] 

861 

862 :raises RuntimeError: If the sorting is invalid, e.g., using properties not in 'refKeys' 

863 or not a bone in 'using'. 

864 """ 

865 if query.queries and (orderby := params.get("orderby")) and utils.string.is_prefix(orderby, name): 

866 if self.multiple and query.getKind() != "viur-relations": 

867 # This query has not been rewritten (yet) 

868 name, skel, query, params = self._rewriteQuery(name, skel, query, params) 

869 

870 try: 

871 _, _type, param = orderby.split(".") 

872 except ValueError as e: 

873 logging.exception(f"Invalid layout of {orderby=}: {e}") 

874 return query 

875 if _type not in ("dest", "rel"): 

876 logging.error("Invalid type {_type}") 

877 return query 

878 

879 # Ensure that the relational-filter is in refKeys 

880 if _type == "dest" and param not in self._ref_keys: 

881 raise RuntimeError(f"Invalid filtering! {param!r} is not in refKeys of RelationalBone {name!r}!") 

882 elif _type == "rel" and (self.using is None or param not in self.using()): 

883 raise RuntimeError(f"Invalid filtering! {param!r} is not a bone in 'using' of RelationalBone {name!r}") 

884 

885 if self.multiple: 

886 path = f"{_type}.{param}" 

887 else: 

888 path = f"{name}.{_type}.{param}" 

889 

890 order = utils.parse.sortorder(params.get("orderdir")) 

891 query = query.order((path, order)) 

892 

893 if self.multiple: 

894 query.setFilterHook(lambda s, query, value: self.filterHook(name, s, query, value)) 

895 query.setOrderHook(lambda s, orderings: self.orderHook(name, s, orderings)) 

896 

897 return query 

898 

899 def filterHook(self, name, query, param, value): # FIXME 

900 """ 

901 Hook installed by buildDbFilter that rewrites filters added to the query to match the layout of the 

902 viur-relations index and performs sanity checks on the query. 

903 

904 This method rewrites and validates filters added to a datastore query after the `buildDbFilter` method 

905 has been executed. It ensures that the filters are compatible with the structure of the viur-relations 

906 index and checks if the query is possible. 

907 

908 :param str name: The name of the bone. 

909 :param db.Query query: The datastore query to be modified. 

910 :param str param: The filter parameter to be checked and potentially modified. 

911 :param value: The value associated with the filter parameter. 

912 

913 :return: A tuple containing the modified filter parameter and its associated value, or None if 

914 the filter parameter is a key special property. 

915 :rtype: Tuple[str, Any] or None 

916 

917 :raises RuntimeError: If the filtering is invalid, e.g., using properties not in 'refKeys' or 'parentKeys'. 

918 """ 

919 if param.startswith("src.") or param.startswith("dest.") or param.startswith("viur_"): 

920 # This filter is already valid in our relation 

921 return param, value 

922 if param.startswith(f"{name}."): 

923 # We add a constrain filtering by properties of the referenced entity 

924 refKey = param.replace(f"{name}.", "") 

925 if " " in refKey: # Strip >, < or = params 

926 refKey = refKey[:refKey.find(" ")] 

927 if refKey not in self._ref_keys: 

928 logging.warning(f"Invalid filtering! {refKey} is not in refKeys of RelationalBone {name}!") 

929 raise RuntimeError() 

930 if self.multiple: 

931 return param.replace(f"{name}.", "dest."), value 

932 else: 

933 return param, value 

934 else: 

935 # We filter by a property of this entity 

936 if not self.multiple: 

937 # Not relational, not multiple - nothing to do here 

938 return param, value 

939 # Prepend "src." 

940 srcKey = param 

941 if " " in srcKey: 

942 srcKey = srcKey[: srcKey.find(" ")] # Cut <, >, and = 

943 if srcKey == db.KEY_SPECIAL_PROPERTY: # Rewrite key= filter as its meaning has changed 

944 if isinstance(value, list) or isinstance(value, tuple): 

945 logging.warning(f"Invalid filtering! Doing an relational Query on {name} " 

946 f"with multiple key= filters is unsupported!") 

947 raise RuntimeError() 

948 if not isinstance(value, db.Key): 

949 value = db.Key(value) 

950 query.ancestor(value) 

951 return None 

952 if srcKey not in self.parentKeys: 

953 logging.warning(f"Invalid filtering! {srcKey} is not in parentKeys of RelationalBone {name}!") 

954 raise RuntimeError() 

955 return f"src.{param}", value 

956 

957 def orderHook(self, name: str, query: db.Query, orderings): # FIXME 

958 """ 

959 Hook installed by buildDbFilter that rewrites orderings added to the query to match the layout of the 

960 viur-relations index and performs sanity checks on the query. 

961 

962 This method rewrites and validates orderings added to a datastore query after the `buildDbFilter` method 

963 has been executed. It ensures that the orderings are compatible with the structure of the viur-relations 

964 index and checks if the query is possible. 

965 

966 :param name: The name of the bone. 

967 :param query: The datastore query to be modified. 

968 :param orderings: A list or tuple of orderings to be checked and potentially modified. 

969 :type orderings: List[Union[str, Tuple[str, db.SortOrder]]] or Tuple[Union[str, Tuple[str, db.SortOrder]]] 

970 

971 :return: A list of modified orderings that are compatible with the viur-relations index. 

972 :rtype: List[Union[str, Tuple[str, db.SortOrder]]] 

973 

974 :raises RuntimeError: If the ordering is invalid, e.g., using properties not in 'refKeys' or 'parentKeys'. 

975 """ 

976 res = [] 

977 if not isinstance(orderings, list) and not isinstance(orderings, tuple): 

978 orderings = [orderings] 

979 for order in orderings: 

980 if isinstance(order, tuple): 

981 orderKey = order[0] 

982 else: 

983 orderKey = order 

984 if orderKey.startswith("dest.") or orderKey.startswith("rel.") or orderKey.startswith("src."): 

985 # This is already valid for our relational index 

986 res.append(order) 

987 continue 

988 if orderKey.startswith(f"{name}."): 

989 k = orderKey.replace(f"{name}.", "") 

990 if k not in self._ref_keys: 

991 logging.warning(f"Invalid ordering! {k} is not in refKeys of RelationalBone {name}!") 

992 raise RuntimeError() 

993 if not self.multiple: 

994 res.append(order) 

995 else: 

996 if isinstance(order, tuple): 

997 res.append((f"dest.{k}", order[1])) 

998 else: 

999 res.append(f"dest.{k}") 

1000 else: 

1001 if not self.multiple: 

1002 # Nothing to do here 

1003 res.append(order) 

1004 continue 

1005 else: 

1006 if orderKey not in self.parentKeys: 

1007 logging.warning( 

1008 f"Invalid ordering! {orderKey} is not in parentKeys of RelationalBone {name}!") 

1009 raise RuntimeError() 

1010 if isinstance(order, tuple): 

1011 res.append((f"src.{orderKey}", order[1])) 

1012 else: 

1013 res.append(f"src.{orderKey}") 

1014 return res 

1015 

1016 def refresh(self, skel: "SkeletonInstance", name: str) -> None: 

1017 """ 

1018 Refreshes all values that might be cached from other entities in the provided skeleton. 

1019 

1020 This method updates the cached values for relational bones in the provided skeleton, which 

1021 correspond to other entities. It fetches the updated values for the relational bone's 

1022 reference keys and replaces the cached values in the skeleton with the fetched values. 

1023 

1024 :param SkeletonInstance skel: The skeleton containing the bone to be refreshed. 

1025 :param str boneName: The name of the bone to be refreshed. 

1026 """ 

1027 if not skel[name] or self.updateLevel == RelationalUpdateLevel.OnValueAssignment: 

1028 return 

1029 

1030 for _, _, value in self.iter_bone_value(skel, name): 

1031 if value and value["dest"]: 

1032 try: 

1033 target_skel = value["dest"].read() 

1034 except ValueError: 

1035 logging.error( 

1036 f"{name}: The key {value['dest']['key']!r} ({value['dest'].get('name')!r}) seems to be gone" 

1037 ) 

1038 continue 

1039 

1040 for key in self.refKeys: 

1041 value["dest"][key] = target_skel[key] 

1042 

1043 def getSearchTags(self, skel: "SkeletonInstance", name: str) -> set[str]: 

1044 """ 

1045 Retrieves the search tags for the given RelationalBone in the provided skeleton. 

1046 

1047 This method iterates over the values of the relational bone and gathers search tags from the 

1048 reference and using skeletons. It combines all the tags into a set to avoid duplicates. 

1049 

1050 :param skel: The skeleton containing the bone for which search tags are to be retrieved. 

1051 :param name: The name of the bone for which search tags are to be retrieved. 

1052 

1053 :return: A set of search tags for the specified relational bone. 

1054 """ 

1055 result = set() 

1056 

1057 def get_values(skel_, values_cache): 

1058 for key, bone in skel_.items(): 

1059 if not bone.searchable: 

1060 continue 

1061 for tag in bone.getSearchTags(values_cache, key): 

1062 result.add(tag) 

1063 

1064 ref_skel_cache, using_skel_cache = self._getSkels() 

1065 for idx, lang, value in self.iter_bone_value(skel, name): 

1066 if value is None: 

1067 continue 

1068 if value["dest"]: 

1069 get_values(ref_skel_cache, value["dest"]) 

1070 if value["rel"]: 

1071 get_values(using_skel_cache, value["rel"]) 

1072 

1073 return result 

1074 

1075 def createRelSkelFromKey(self, key: db.Key, rel: dict | None = None) -> RelDict | None: 

1076 if rel_skel := self.relskels_from_keys([(key, rel)]): 

1077 return rel_skel[0] 

1078 return None 

1079 

1080 def relskels_from_keys(self, key_rel_list: list[tuple[db.Key, dict | None]]) -> list[RelDict]: 

1081 """ 

1082 Creates a list of RelSkel instances valid for this bone from the given database key. 

1083 

1084 This method retrieves the entity corresponding to the provided key from the database, unserializes it 

1085 into a reference skeleton, and returns a dictionary containing the reference skeleton and optional 

1086 relation data. 

1087 

1088 :param key_rel_list: List of tuples with the first value in the tuple is the 

1089 key and the second is and RelSkel or None 

1090 

1091 :return: A dictionary containing a reference skeleton and optional relation data. 

1092 """ 

1093 

1094 if not all(db_objs := db.Get([db.keyHelper(value[0], self.kind) for value in key_rel_list])): 

1095 return [] # return emtpy data when not all data is found 

1096 res_rel_skels = [] 

1097 for (key, rel), db_obj in zip(key_rel_list, db_objs): 

1098 dest_skel = self._refSkelCache() 

1099 dest_skel.unserialize(db_obj) 

1100 for bone_name in dest_skel: 

1101 # Unserialize all bones from refKeys, then drop dbEntity - otherwise all properties will be copied 

1102 _ = dest_skel[bone_name] 

1103 dest_skel.dbEntity = None 

1104 res_rel_skels.append( 

1105 { 

1106 "dest": dest_skel, 

1107 "rel": rel or None 

1108 } 

1109 ) 

1110 return res_rel_skels 

1111 

1112 def setBoneValue( 

1113 self, 

1114 skel: "SkeletonInstance", 

1115 boneName: str, 

1116 value: t.Any, 

1117 append: bool, 

1118 language: None | str = None 

1119 ) -> bool: 

1120 """ 

1121 Sets the value of the specified bone in the given skeleton. Sanity checks are performed to ensure the 

1122 value is valid. If the value is invalid, no modifications are made. 

1123 

1124 :param skel: Dictionary with the current values from the skeleton we belong to. 

1125 :param boneName: The name of the bone to be modified. 

1126 :param value: The value to be assigned. The type depends on the bone type. 

1127 :param append: If true, the given value is appended to the values of the bone instead of replacing it. 

1128 Only supported on bones with multiple=True. 

1129 :param language: Set/append for a specific language (optional). Required if the bone 

1130 supports languages. 

1131 

1132 :return: True if the operation succeeded, False otherwise. 

1133 """ 

1134 assert not (bool(self.languages) ^ bool(language)), "Language is required or not supported" 

1135 assert not append or self.multiple, "Can't append - bone is not multiple" 

1136 

1137 def tuple_check(in_value: tuple | None = None) -> bool: 

1138 """ 

1139 Return False if the given value is a tuple with a length of two. 

1140 In addition, the first field in the tuple must be a str,int or db.key. 

1141 Furthermore, the second field must be a skeletonInstanceClassRef. 

1142 """ 

1143 return not (isinstance(in_value, tuple) and len(in_value) == 2 

1144 and isinstance(in_value[0], (str, int, db.Key)) 

1145 and isinstance(in_value[1], self._skeletonInstanceClassRef)) 

1146 

1147 if not self.multiple and not self.using: 

1148 if not isinstance(value, (str, int, db.Key)): 

1149 raise ValueError(f"You must supply exactly one Database-Key str or int to {boneName}") 

1150 parsed_value = (value, None) 

1151 elif not self.multiple and self.using: 

1152 if tuple_check(value): 

1153 raise ValueError(f"You must supply a tuple of (Database-Key, relSkel) to {boneName}") 

1154 parsed_value = value 

1155 elif self.multiple and not self.using: 

1156 if not isinstance(value, (str, int, db.Key)) and not (isinstance(value, list)) \ 

1157 and all([isinstance(val, (str, int, db.Key)) for val in value]): 

1158 raise ValueError(f"You must supply a Database-Key or a list hereof to {boneName}") 

1159 if isinstance(value, list): 

1160 parsed_value = [(key, None) for key in value] 

1161 else: 

1162 parsed_value = [(value, None)] 

1163 else: # which means (self.multiple and self.using) 

1164 if tuple_check(value) and not (isinstance(value, list) and all(tuple_check(val) for val in value)): 

1165 raise ValueError(f"You must supply (db.Key, RelSkel) or a list hereof to {boneName}") 

1166 if isinstance(value, list): 

1167 parsed_value = value 

1168 else: 

1169 parsed_value = [value] 

1170 

1171 if boneName not in skel: 

1172 skel[boneName] = {} 

1173 if language: 

1174 skel[boneName].setdefault(language, []) 

1175 

1176 if self.multiple: 

1177 rel_list = self.relskels_from_keys(parsed_value) 

1178 if append: 

1179 if language: 

1180 skel[boneName][language].extend(rel_list) 

1181 else: 

1182 if not isinstance(skel[boneName], list): 

1183 skel[boneName] = [] 

1184 skel[boneName].extend(rel_list) 

1185 else: 

1186 if language: 

1187 skel[boneName][language] = rel_list 

1188 else: 

1189 skel[boneName] = rel_list 

1190 else: 

1191 if not (rel := self.createRelSkelFromKey(parsed_value[0], parsed_value[1])): 

1192 return False 

1193 if language: 

1194 skel[boneName][language] = rel 

1195 else: 

1196 skel[boneName] = rel 

1197 return True 

1198 

1199 def getReferencedBlobs(self, skel: "SkeletonInstance", name: str) -> set[str]: 

1200 """ 

1201 Retrieves the set of referenced blobs from the specified bone in the given skeleton instance. 

1202 

1203 :param SkeletonInstance skel: The skeleton instance to extract the referenced blobs from. 

1204 :param str name: The name of the bone to retrieve the referenced blobs from. 

1205 

1206 :return: A set containing the unique blob keys referenced by the specified bone. 

1207 :rtype: Set[str] 

1208 """ 

1209 result = set() 

1210 for idx, lang, value in self.iter_bone_value(skel, name): 

1211 if value is None: 

1212 continue 

1213 for key, bone_ in value["dest"].items(): 

1214 result.update(bone_.getReferencedBlobs(value["dest"], key)) 

1215 if value["rel"]: 

1216 for key, bone_ in value["rel"].items(): 

1217 result.update(bone_.getReferencedBlobs(value["rel"], key)) 

1218 return result 

1219 

1220 def getUniquePropertyIndexValues(self, valuesCache: dict, name: str) -> list[str]: 

1221 """ 

1222 Generates unique property index values for the RelationalBone based on the referenced keys. 

1223 Can be overridden if different behavior is required (e.g., examining values from `prop:usingSkel`). 

1224 

1225 :param dict valuesCache: The cache containing the current values of the bone. 

1226 :param str name: The name of the bone for which to generate unique property index values. 

1227 

1228 :return: A list containing the unique property index values for the specified bone. 

1229 :rtype: List[str] 

1230 """ 

1231 value = valuesCache.get(name) 

1232 if not value: # We don't have a value to lock 

1233 return [] 

1234 if isinstance(value, dict): 

1235 return self._hashValueForUniquePropertyIndex(value["dest"]["key"]) 

1236 elif isinstance(value, list): 

1237 return self._hashValueForUniquePropertyIndex([x["dest"]["key"] for x in value]) 

1238 

1239 def structure(self) -> dict: 

1240 return super().structure() | { 

1241 "type": f"{self.type}.{self.kind}", 

1242 "module": self.module, 

1243 "format": self.format, 

1244 "using": self.using().structure() if self.using else None, 

1245 "relskel": self._refSkelCache().structure(), 

1246 }