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.1, created at 2024-09-03 13:41 +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 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. 

59 

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. 

62 

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

64 

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. 

70 

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. 

73 

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. 

86 

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: 

90 

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) 

94 

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. 

98 

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. 

102 

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. 

107 

108 :param RelationalUpdateLevel.Always: 

109 

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) 

112 

113 :param RelationalUpdateLevel.OnRebuildSearchIndex: 

114 

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. 

118 

119 :param RelationalUpdateLevel.OnValueAssignment: 

120 

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 

123 

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 

131 

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. 

137 

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. 

141 

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. 

146 

147 """ 

148 type = "relational" 

149 kind = None 

150 

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. 

166 

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. 

184 

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 

187 

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. 

197 

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. 

202 

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 

214 

215 :param consistency: 

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

217 

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 

223 

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. 

230 

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. 

234 

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 

243 

244 if kind: 

245 self.kind = kind 

246 

247 if module: 

248 self.module = module 

249 elif self.kind: 

250 self.module = self.kind 

251 

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

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

254 

255 # Referenced keys 

256 self.refKeys = {"key"} 

257 if refKeys: 

258 self.refKeys |= set(refKeys) 

259 

260 # Parent keys 

261 self.parentKeys = {"key"} 

262 if parentKeys: 

263 self.parentKeys |= set(parentKeys) 

264 

265 self.using = using 

266 

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) 

273 

274 assert 0 <= updateLevel < 3 

275 for n in RelationalUpdateLevel: 

276 if updateLevel == n.value: 

277 updateLevel = n 

278 

279 self.updateLevel = updateLevel 

280 self.consistency = consistency 

281 

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

287 

288 def setSystemInitialized(self): 

289 """ 

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

291 

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. 

295 

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

303 

304 def _getSkels(self): 

305 """ 

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

307 

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. 

311 

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 

318 

319 def singleValueUnserialize(self, val): 

320 """ 

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

322 

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. 

325 

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 

330 

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

332 """ 

333 

334 def fixFromDictToEntry(inDict): 

335 """ 

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

337 

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 

358 

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} 

387 

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. 

391 

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. 

395 

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 

401 

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} 

471 

472 skel.dbEntity[name] = res 

473 

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) 

479 

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

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

482 

483 return True 

484 

485 def _get_single_destinct_hash(self, value): 

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

487 

488 if self.using: 

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

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

491 

492 return tuple(parts) 

493 

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

495 """ 

496 Handle relational updates after a skeleton is saved. 

497 

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. 

500 

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) 

568 

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

570 """ 

571 Handle relational updates after a skeleton is deleted. 

572 

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

574 for the given relational bone. 

575 

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()]) 

586 

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

588 """ 

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

590 

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

592 is performed in other methods of the RelationalBone class. 

593 

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

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

596 """ 

597 return None 

598 

599 def parseSubfieldsFromClient(self): 

600 """ 

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

602 

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. 

606 

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 

611 

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

613 oldValues = skel[bone_name] 

614 

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 

661 

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 

679 

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

681 """ 

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

683 

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. 

686 

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. 

691 

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] 

694 

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 

707 

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 

738 

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. 

749 

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. 

752 

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. 

758 

759 :return: The modified datastore query. 

760 :rtype: db.Query 

761 

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 

767 

768 if origQueries is None: # This query is unsatisfiable 

769 return dbFilter 

770 

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) 

775 

776 # Merge the relational filters in 

777 for myKey in myKeys: 

778 value = rawFilter[myKey] 

779 

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 

793 

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] 

798 

799 if "$" in checkKey: 

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

801 

802 if _type == "dest": 

803 

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

808 

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

818 

819 elif _type == "rel": 

820 

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

825 

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

835 

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

839 

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

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

842 

843 return dbFilter 

844 

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. 

854 

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. 

857 

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. 

862 

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

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

865 

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 

906 

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. 

911 

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. 

915 

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. 

920 

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 

924 

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 

964 

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. 

969 

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. 

973 

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

978 

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

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

981 

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 

1023 

1024 def refresh(self, skel: "SkeletonInstance", boneName: str): 

1025 """ 

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

1027 

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. 

1031 

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

1035 

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] 

1051 

1052 if not skel[boneName] or self.updateLevel == RelationalUpdateLevel.OnValueAssignment: 

1053 return 

1054 

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) 

1069 

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

1071 """ 

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

1073 

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. 

1076 

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. 

1079 

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

1081 """ 

1082 result = set() 

1083 

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) 

1090 

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

1099 

1100 return result 

1101 

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. 

1105 

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. 

1109 

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. 

1112 

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

1114 :rtype: dict 

1115 """ 

1116 

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 } 

1132 

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. 

1144 

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. 

1152 

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 

1231 

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. 

1235 

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. 

1238 

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 

1252 

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`). 

1257 

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. 

1260 

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]) 

1271 

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 }