Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/bones/base.py: 52%

857 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-27 10:50 +0000

1""" 

2This module contains the base classes for the bones in ViUR. Bones are the fundamental building blocks of 

3ViUR's data structures, representing the fields and their properties in the entities managed by the 

4framework. The base classes defined in this module are the foundation upon which specific bone types are 

5built, such as string, numeric, and date/time bones. 

6""" 

7 

8import contextlib 

9import copy 

10import enum 

11import hashlib 

12import inspect 

13import logging 

14import threading 

15import typing as t 

16from collections.abc import Iterable 

17from dataclasses import dataclass, field 

18from datetime import timedelta 

19from enum import Enum 

20 

21from viur.core import current, db, i18n, utils 

22from viur.core.config import conf 

23 

24if t.TYPE_CHECKING: 24 ↛ 25line 24 didn't jump to line 25 because the condition on line 24 was never true

25 from ..skeleton import Skeleton, SkeletonInstance 

26 

27__system_initialized = False 

28""" 

29Initializes the global variable __system_initialized 

30""" 

31 

32_computing_bones = threading.local() 

33"""The bone computations running in the current thread. 

34 

35See :meth:`BaseBone.is_computing` for why the recursion guard cannot live on the 

36bone itself. 

37""" 

38 

39 

40def _computation_key(skel: "SkeletonInstance", bone_name: str) -> tuple[int, str]: 

41 """ 

42 Identify one computation of one bone on one skeleton. 

43 

44 ``accessedValues`` stands in for the skeleton rather than the instance itself: 

45 :func:`~viur.core.skeleton.utils.without_render_preparation` hands out a second 

46 ``SkeletonInstance`` that shares it, and :meth:`BaseBone._compute` works on that 

47 one while :meth:`BaseBone.unserialize_compute` was entered with the original. 

48 

49 :param skel: The skeleton the bone is computed on. 

50 :param bone_name: Name of the bone. 

51 :return: A key identifying this computation. 

52 """ 

53 return id(skel.accessedValues), bone_name 

54 

55 

56def setSystemInitialized(): 

57 """ 

58 Sets the global __system_initialized variable to True, indicating that the system is 

59 initialized and ready for use. This function should be called once all necessary setup 

60 tasks have been completed. It also iterates over all skeleton classes and calls their 

61 setSystemInitialized() method. 

62 

63 Global variables: 

64 __system_initialized: A boolean flag indicating if the system is initialized. 

65 """ 

66 global __system_initialized 

67 from viur.core.skeleton import iterAllSkelClasses 

68 

69 for skelCls in iterAllSkelClasses(): 

70 skelCls.setSystemInitialized() 

71 

72 __system_initialized = True 

73 

74 

75def getSystemInitialized(): 

76 """ 

77 Retrieves the current state of the system initialization by returning the value of the 

78 global variable __system_initialized. 

79 """ 

80 global __system_initialized 

81 return __system_initialized 

82 

83 

84class ReadFromClientErrorSeverity(Enum): 

85 """ 

86 ReadFromClientErrorSeverity is an enumeration that represents the severity levels of errors 

87 that can occur while reading data from the client. 

88 """ 

89 NotSet = 0 

90 """No error occurred""" 

91 InvalidatesOther = 1 

92 # TODO: what is this error about? 

93 """The data is valid, for this bone, but in relation to other invalid""" 

94 Empty = 2 

95 """The data is empty, but the bone requires a value""" 

96 Invalid = 3 

97 """The data is invalid, but the bone requires a value""" 

98 

99 

100@dataclass 

101class ReadFromClientError: 

102 """ 

103 The ReadFromClientError class represents an error that occurs while reading data from the client. 

104 This class is used to store information about the error, including its severity, an error message, 

105 the field path where the error occurred, and a list of invalidated fields. 

106 """ 

107 severity: ReadFromClientErrorSeverity 

108 """A ReadFromClientErrorSeverity enumeration value representing the severity of the error.""" 

109 errorMessage: t.Optional[str] = None 

110 """A string containing a human-readable error message describing the issue.""" 

111 fieldPath: list[str] = field(default_factory=list) 

112 """A list of strings representing the path to the field where the error occurred.""" 

113 invalidatedFields: list[str] = None 

114 """A list of strings containing the names of invalidated fields, if any.""" 

115 

116 def __post_init__(self): 

117 if not self.errorMessage: 

118 self.errorMessage = { 

119 ReadFromClientErrorSeverity.NotSet: 

120 i18n.translate("core.bones.error.notset", "Field not submitted"), 

121 ReadFromClientErrorSeverity.InvalidatesOther: 

122 i18n.translate("core.bones.error.invalidatesother", "Field invalidates another field"), 

123 ReadFromClientErrorSeverity.Empty: 

124 i18n.translate("core.bones.error.empty", "Field not set"), 

125 ReadFromClientErrorSeverity.Invalid: 

126 i18n.translate("core.bones.error.invalid", "Invalid value provided"), 

127 }[self.severity] 

128 

129 def __str__(self): 

130 return f"{'.'.join(self.fieldPath)}: {self.errorMessage} [{self.severity.name}]" 

131 

132 

133class ReadFromClientException(Exception): 

134 """ 

135 ReadFromClientError as an Exception to raise. 

136 """ 

137 

138 def __init__(self, errors: ReadFromClientError | t.Iterable[ReadFromClientError]): 

139 """ 

140 This is an exception holding ReadFromClientErrors. 

141 

142 :param errors: Either one or an iterable of errors. 

143 """ 

144 super().__init__() 

145 

146 # Allow to specifiy a single ReadFromClientError 

147 if isinstance(errors, ReadFromClientError): 

148 errors = (errors, ) 

149 

150 self.errors = tuple(error for error in errors if isinstance(error, ReadFromClientError)) 

151 

152 # Disallow ReadFromClientException without any ReadFromClientErrors 

153 if not self.errors: 

154 raise ValueError("ReadFromClientException requires for at least one ReadFromClientError") 

155 

156 # Either show any errors with severity greater ReadFromClientErrorSeverity.NotSet to the Exception notes, 

157 # or otherwise all errors (all have ReadFromClientErrorSeverity.NotSet then) 

158 notes_errors = tuple( 

159 error for error in self.errors if error.severity.value > ReadFromClientErrorSeverity.NotSet.value 

160 ) 

161 

162 self.add_note("\n".join(str(error) for error in notes_errors or self.errors)) 

163 

164 

165class UniqueLockMethod(Enum): 

166 """ 

167 UniqueLockMethod is an enumeration that represents different locking methods for unique constraints 

168 on bones. This is used to specify how the uniqueness of a value or a set of values should be 

169 enforced. 

170 """ 

171 SameValue = 1 # Lock this value for just one entry or each value individually if bone is multiple 

172 """ 

173 Lock this value so that there is only one entry, or lock each value individually if the bone 

174 is multiple. 

175 """ 

176 SameSet = 2 # Same Set of entries (including duplicates), any order 

177 """Lock the same set of entries (including duplicates) regardless of their order.""" 

178 SameList = 3 # Same Set of entries (including duplicates), in this specific order 

179 """Lock the same set of entries (including duplicates) in a specific order.""" 

180 

181 

182@dataclass 

183class UniqueValue: # Mark a bone as unique (it must have a different value for each entry) 

184 """ 

185 The UniqueValue class represents a unique constraint on a bone, ensuring that it must have a 

186 different value for each entry. This class is used to store information about the unique 

187 constraint, such as the locking method, whether to lock empty values, and an error message to 

188 display to the user if the requested value is already taken. 

189 """ 

190 method: UniqueLockMethod # How to handle multiple values (for bones with multiple=True) 

191 """ 

192 A UniqueLockMethod enumeration value specifying how to handle multiple values for bones with 

193 multiple=True. 

194 """ 

195 lockEmpty: bool # If False, empty values ("", 0) are not locked - needed if unique but not required 

196 """ 

197 A boolean value indicating if empty values ("", 0) should be locked. If False, empty values are not 

198 locked, which is needed if a field is unique but not required. 

199 """ 

200 message: str # Error-Message displayed to the user if the requested value is already taken 

201 """ 

202 A string containing an error message displayed to the user if the requested value is already 

203 taken. 

204 """ 

205 

206 

207@dataclass 

208class MultipleConstraints: 

209 """ 

210 The MultipleConstraints class is used to define constraints on multiple bones, such as the minimum 

211 and maximum number of entries allowed and whether value duplicates are allowed. 

212 """ 

213 min: int = 0 

214 """An integer representing the lower bound of how many entries can be submitted (default: 0).""" 

215 max: int = 0 

216 """An integer representing the upper bound of how many entries can be submitted (default: 0 = unlimited).""" 

217 duplicates: bool = False 

218 """A boolean indicating if the same value can be used multiple times (default: False).""" 

219 sorted: bool | t.Callable = False 

220 """A boolean value or a method indicating if the value must be sorted (default: False).""" 

221 reversed: bool = False 

222 """ 

223 A boolean value indicating if sorted values shall be sorted in reversed order (default: False). 

224 It is only applied when the `sorted`-flag is set accordingly. 

225 """ 

226 

227 

228class ComputeMethod(Enum): 

229 Always = 0 

230 """Always compute on deserialization; Property is never written to DB and can't be used in filters""" 

231 Lifetime = 1 

232 """Update only when given lifetime is outrun; value is only being stored when the skeleton is written""" 

233 Once = 2 

234 """Compute only once, when it was previously unset and computes to a value other than `None`""" 

235 OnWrite = 3 

236 """Compute before every write of the skeleton""" 

237 

238 

239@dataclass 

240class ComputeInterval: 

241 method: ComputeMethod = ComputeMethod.Always 

242 """The compute-method to use for this bone""" 

243 lifetime: timedelta = None 

244 """Defines a timedelta until which the value stays valid (only used by `ComputeMethod.Lifetime`)""" 

245 

246 

247@dataclass 

248class Compute: 

249 fn: callable 

250 """The callable computing the value""" 

251 interval: ComputeInterval = field(default_factory=ComputeInterval) 

252 """The value caching interval""" 

253 raw: bool = True 

254 """Defines whether the value returned by fn is used as is, or is passed through `bone.fromClient()`""" 

255 

256 

257class CloneStrategy(enum.StrEnum): 

258 """Strategy for selecting the value of a cloned skeleton""" 

259 

260 SET_NULL = enum.auto() 

261 """Sets the cloned bone value to None.""" 

262 

263 SET_DEFAULT = enum.auto() 

264 """Sets the cloned bone value to its defaultValue.""" 

265 

266 SET_EMPTY = enum.auto() 

267 """Sets the cloned bone value to its emptyValue.""" 

268 

269 COPY_VALUE = enum.auto() 

270 """Copies the bone value from the source skeleton.""" 

271 

272 CUSTOM = enum.auto() 

273 """Uses a custom-defined logic for setting the cloned value. 

274 Requires :attr:`CloneBehavior.custom_func` to be set. 

275 """ 

276 

277 

278class CloneCustomFunc(t.Protocol): 

279 """Type for a custom clone function assigned to :attr:`CloneBehavior.custom_func`""" 

280 

281 def __call__(self, skel: "SkeletonInstance", src_skel: "SkeletonInstance", bone_name: str) -> t.Any: 

282 """Return the value for the cloned bone""" 

283 ... 

284 

285 

286@dataclass 

287class CloneBehavior: 

288 """Strategy configuration for selecting the value of a cloned skeleton""" 

289 

290 strategy: CloneStrategy 

291 """The strategy used to select a value from a cloned skeleton""" 

292 

293 custom_func: CloneCustomFunc = None 

294 """custom-defined logic for setting the cloned value 

295 Only required when :attr:`strategy` is set to :attr:`CloneStrategy.CUSTOM`. 

296 """ 

297 

298 def __post_init__(self): 

299 """Validate this configuration.""" 

300 if self.strategy == CloneStrategy.CUSTOM and self.custom_func is None: 300 ↛ 301line 300 didn't jump to line 301 because the condition on line 300 was never true

301 raise ValueError("CloneStrategy is CUSTOM, but custom_func is not set") 

302 elif self.strategy != CloneStrategy.CUSTOM and self.custom_func is not None: 302 ↛ 303line 302 didn't jump to line 303 because the condition on line 302 was never true

303 raise ValueError("custom_func is set, but CloneStrategy is not CUSTOM") 

304 

305 

306class BaseBone(object): 

307 """ 

308 The BaseBone class serves as the base class for all bone types in the ViUR framework. 

309 It defines the core functionality and properties that all bones should implement. 

310 

311 :param descr: Textual, human-readable description of that bone. Will be translated. 

312 :param defaultValue: If set, this bone will be preinitialized with this value 

313 :param required: If True, the user must enter a valid value for this bone (the viur.core refuses 

314 to save the skeleton otherwise). If a list/tuple of languages (strings) is provided, these 

315 language must be entered. 

316 :param multiple: If True, multiple values can be given. (ie. n:m relations instead of n:1) 

317 :param searchable: If True, this bone will be included in the fulltext search. Can be used 

318 without the need of also been indexed. 

319 :param type_suffix: Allows to specify an optional suffix for the bone-type, for bone customization 

320 :param vfunc: If given, a callable validating the user-supplied value for this bone. 

321 This callable must return None if the value is valid, a String containing an meaningful 

322 error-message for the user otherwise. 

323 :param readOnly: If True, the user is unable to change the value of this bone. If a value for this 

324 bone is given along the POST-Request during Add/Edit, this value will be ignored. Its still 

325 possible for the developer to modify this value by assigning skel.bone.value. 

326 :param visible: If False, the value of this bone should be hidden from the user. This does 

327 *not* protect the value from being exposed in a template, nor from being transferred 

328 to the client (ie to the admin or as hidden-value in html-form) 

329 :param tags: Optional classification tags for the bone's data content. Accepts a string, a list/tuple 

330 of strings, or any iterable of strings. Multiple tags can be combined; they are non-exclusive. 

331 Intended for privacy tooling, audits, anonymization workflows, and admin tooling - not for 

332 access control. Suggested values include ``"personal"``, ``"contact"``, ``"identifier"``, 

333 ``"location"``, ``"financial"``, and ``"technical"``. 

334 :param compute: If set, the bone's value will be computed in the given method. 

335 

336 .. NOTE:: 

337 The kwarg 'multiple' is not supported by all bones 

338 """ 

339 type = "hidden" 

340 isClonedInstance = False 

341 

342 skel_cls = None 

343 """Skeleton class to which this bone instance belongs""" 

344 

345 name = None 

346 """Name of this bone (attribute name in the skeletons containing this bone)""" 

347 

348 def __init__( 

349 self, 

350 *, 

351 compute: Compute = None, 

352 defaultValue: t.Any = None, 

353 descr: t.Optional[str | i18n.translate] = None, 

354 getEmptyValueFunc: callable = None, 

355 indexed: bool = True, 

356 isEmptyFunc: callable = None, # fixme: Rename this, see below. 

357 languages: None | list[str] = None, 

358 multiple: bool | MultipleConstraints = False, 

359 params: dict = None, 

360 readOnly: bool = None, # fixme: Rename into readonly (all lowercase!) soon. 

361 required: bool | list[str] | tuple[str] = False, 

362 searchable: bool = False, 

363 tags: str | t.Iterable[str] | None = None, 

364 type_suffix: str = "", 

365 unique: None | UniqueValue = None, 

366 vfunc: callable = None, # fixme: Rename this, see below. 

367 visible: bool = True, 

368 clone_behavior: CloneBehavior | CloneStrategy | None = None, 

369 ): 

370 """ 

371 Initializes a new Bone. 

372 """ 

373 self.isClonedInstance = getSystemInitialized() 

374 

375 # Standard definitions 

376 self.descr = descr 

377 self.params = params or {} 

378 self.multiple = multiple 

379 self.required = required 

380 self.readOnly = bool(readOnly) 

381 self.searchable = searchable 

382 self.tags = tuple(utils.ensure_iterable(tags, allow_callable=False)) 

383 self.visible = visible 

384 self.indexed = indexed 

385 

386 if type_suffix: 

387 self.type += f".{type_suffix}" 

388 

389 if conf.i18n.auto_translate_bones and isinstance(category := self.params.get("category"), str): 389 ↛ 390line 389 didn't jump to line 390 because the condition on line 389 was never true

390 self.params["category"] = i18n.translate(category, hint=f"category of a <{type(self).__name__}>") 

391 

392 # Multi-language support 

393 if not ( 393 ↛ 398line 393 didn't jump to line 398 because the condition on line 393 was never true

394 languages is None or 

395 (isinstance(languages, list) and len(languages) > 0 

396 and all([isinstance(x, str) for x in languages])) 

397 ): 

398 raise ValueError("languages must be None or a list of strings") 

399 

400 if languages and "__default__" in languages: 400 ↛ 401line 400 didn't jump to line 401 because the condition on line 400 was never true

401 raise ValueError("__default__ is not supported as a language") 

402 

403 if ( 403 ↛ 407line 403 didn't jump to line 407 because the condition on line 403 was never true

404 not isinstance(required, bool) 

405 and (not isinstance(required, (tuple, list)) or any(not isinstance(value, str) for value in required)) 

406 ): 

407 raise TypeError(f"required must be boolean or a tuple/list of strings. Got: {required!r}") 

408 

409 if isinstance(required, (tuple, list)) and not languages: 409 ↛ 410line 409 didn't jump to line 410 because the condition on line 409 was never true

410 raise ValueError("You set required to a list of languages, but defined no languages.") 

411 

412 if isinstance(required, (tuple, list)) and languages and (diff := set(required).difference(languages)): 412 ↛ 413line 412 didn't jump to line 413 because the condition on line 412 was never true

413 raise ValueError(f"The language(s) {', '.join(map(repr, diff))} can not be required, " 

414 f"because they're not defined.") 

415 

416 if callable(defaultValue): 

417 # check if the signature of defaultValue can bind two (fictive) parameters. 

418 try: 

419 inspect.signature(defaultValue).bind("skel", "bone") # the strings are just for the test! 

420 except TypeError: 

421 raise ValueError(f"Callable {defaultValue=} requires for the parameters 'skel' and 'bone'.") 

422 

423 self.languages = languages 

424 

425 # Default value 

426 # Convert a None default-value to the empty container that's expected if the bone is 

427 # multiple or has languages 

428 default = [] if defaultValue is None and self.multiple else defaultValue 

429 if self.languages: 

430 if callable(defaultValue): 430 ↛ 431line 430 didn't jump to line 431 because the condition on line 430 was never true

431 self.defaultValue = defaultValue 

432 elif not isinstance(defaultValue, dict): 

433 self.defaultValue = {lang: default for lang in self.languages} 

434 elif "__default__" in defaultValue: 434 ↛ 438line 434 didn't jump to line 438 because the condition on line 434 was always true

435 self.defaultValue = {lang: defaultValue.get(lang, defaultValue["__default__"]) 

436 for lang in self.languages} 

437 else: 

438 self.defaultValue = defaultValue # default will have the same value at this point 

439 else: 

440 self.defaultValue = default 

441 

442 # Unique values 

443 if unique: 

444 if not isinstance(unique, UniqueValue): 444 ↛ 445line 444 didn't jump to line 445 because the condition on line 444 was never true

445 raise ValueError("Unique must be an instance of UniqueValue") 

446 if not self.multiple and unique.method.value != 1: 446 ↛ 447line 446 didn't jump to line 447 because the condition on line 446 was never true

447 raise ValueError("'SameValue' is the only valid method on non-multiple bones") 

448 

449 self.unique = unique 

450 

451 # Overwrite some validations and value functions by parameter instead of subclassing 

452 # todo: This can be done better and more straightforward. 

453 if vfunc: 

454 self.isInvalid = vfunc # fixme: why is this called just vfunc, and not isInvalidValue/isInvalidValueFunc? 

455 

456 if isEmptyFunc: 456 ↛ 457line 456 didn't jump to line 457 because the condition on line 456 was never true

457 self.isEmpty = isEmptyFunc # fixme: why is this not called isEmptyValue/isEmptyValueFunc? 

458 

459 if getEmptyValueFunc: 

460 self.getEmptyValue = getEmptyValueFunc 

461 

462 if compute: 

463 if not isinstance(compute, Compute): 463 ↛ 464line 463 didn't jump to line 464 because the condition on line 463 was never true

464 raise TypeError("compute must be an instanceof of Compute") 

465 if not isinstance(compute.fn, t.Callable): 465 ↛ 466line 465 didn't jump to line 466 because the condition on line 465 was never true

466 raise ValueError("'compute.fn' must be callable") 

467 # When readOnly is None, handle flag automatically 

468 if readOnly is None: 

469 self.readOnly = True 

470 if not self.readOnly: 

471 raise ValueError("'compute' can only be used with bones configured as `readOnly=True`") 

472 

473 if ( 473 ↛ 477line 473 didn't jump to line 477 because the condition on line 473 was never true

474 compute.interval.method == ComputeMethod.Lifetime 

475 and not isinstance(compute.interval.lifetime, timedelta) 

476 ): 

477 raise ValueError( 

478 f"'compute' is configured as ComputeMethod.Lifetime, but {compute.interval.lifetime=} was specified" 

479 ) 

480 # Deprecated: the core guards a running computation with 

481 # :meth:`computing` / :meth:`is_computing`, which is scoped to the 

482 # skeleton and the thread. The flag is kept because it is set from 

483 # outside the core, and :meth:`unserialize_compute` still honours it. 

484 self._prevent_compute = False 

485 

486 self.compute = compute 

487 

488 if clone_behavior is None: # auto choose 

489 if self.unique and self.readOnly: 

490 self.clone_behavior = CloneBehavior(CloneStrategy.SET_DEFAULT) 

491 else: 

492 self.clone_behavior = CloneBehavior(CloneStrategy.COPY_VALUE) 

493 # TODO: Any different setting for computed bones? 

494 elif isinstance(clone_behavior, CloneStrategy): 494 ↛ 495line 494 didn't jump to line 495 because the condition on line 494 was never true

495 self.clone_behavior = CloneBehavior(strategy=clone_behavior) 

496 elif isinstance(clone_behavior, CloneBehavior): 496 ↛ 499line 496 didn't jump to line 499 because the condition on line 496 was always true

497 self.clone_behavior = clone_behavior 

498 else: 

499 raise TypeError(f"'clone_behavior' must be an instance of Clone, but {clone_behavior=} was specified") 

500 

501 def __set_name__(self, owner: "Skeleton", name: str) -> None: 

502 self.skel_cls = owner 

503 self.name = name 

504 

505 def setSystemInitialized(self) -> None: 

506 """ 

507 For the BaseBone, this performs some automatisms regarding bone descr and translations. 

508 It can be overwritten to initialize properties that depend on the Skeleton system being initialized. 

509 """ 

510 

511 # Set descr to the bone_name if no descr argument is given 

512 if self.descr is None: 

513 # TODO: The super().__setattr__() call is kinda hackish, 

514 # but unfortunately viur-core has no *during system initialisation* state 

515 super().__setattr__("descr", self.name or "") 

516 

517 if conf.i18n.auto_translate_bones and self.descr and isinstance(self.descr, str): 

518 # Make sure that it is a :class:i18n.translate` object. 

519 super().__setattr__( 

520 "descr", 

521 i18n.translate(self.descr, hint=f"descr of a <{type(self).__name__}>{self.name}") 

522 ) 

523 

524 def isInvalid(self, value): 

525 """ 

526 Checks if the current value of the bone in the given skeleton is invalid. 

527 Returns None if the value would be valid for this bone, an error-message otherwise. 

528 """ 

529 return False 

530 

531 def isEmpty(self, value: t.Any) -> bool: 

532 """ 

533 Check if the given single value represents the "empty" value. 

534 This usually is the empty string, 0 or False. 

535 

536 .. warning:: isEmpty takes precedence over isInvalid! The empty value is always 

537 valid - unless the bone is required. 

538 But even then the empty value will be reflected back to the client. 

539 

540 .. warning:: value might be the string/object received from the user (untrusted 

541 input!) or the value returned by get 

542 """ 

543 return not bool(value) 

544 

545 def getDefaultValue(self, skeletonInstance): 

546 """ 

547 Retrieves the default value for the bone. 

548 

549 This method is called by the framework to obtain the default value of a bone when no value 

550 is provided. Derived bone classes can overwrite this method to implement their own logic for 

551 providing a default value. 

552 

553 :return: The default value of the bone, which can be of any data type. 

554 """ 

555 if callable(self.defaultValue): 555 ↛ 556line 555 didn't jump to line 556 because the condition on line 555 was never true

556 res = self.defaultValue(skeletonInstance, self) 

557 if self.languages and self.multiple: 

558 if not isinstance(res, dict): 

559 if not isinstance(res, (list, set, tuple)): 

560 return {lang: [res] for lang in self.languages} 

561 else: 

562 return {lang: res for lang in self.languages} 

563 elif self.languages: 

564 if not isinstance(res, dict): 

565 return {lang: res for lang in self.languages} 

566 elif self.multiple: 

567 if not isinstance(res, (list, set, tuple)): 

568 return [res] 

569 return res 

570 

571 elif isinstance(self.defaultValue, list): 

572 return self.defaultValue[:] 

573 elif isinstance(self.defaultValue, dict): 

574 # A shallow dict copy is not enough: the inner lists must be copied as well, 

575 # otherwise all languages share one list instance living on the bone, and any 

576 # mutation bleeds into the other languages and into every other skeleton. 

577 return { 

578 lang: value[:] if isinstance(value, list) else value 

579 for lang, value in self.defaultValue.items() 

580 } 

581 else: 

582 return self.defaultValue 

583 

584 def getEmptyValue(self) -> t.Any: 

585 """ 

586 Returns the value representing an empty field for this bone. 

587 This might be the empty string for str/text Bones, Zero for numeric bones etc. 

588 """ 

589 return None 

590 

591 def __setattr__(self, key, value): 

592 """ 

593 Custom attribute setter for the BaseBone class. 

594 

595 This method is used to ensure that certain bone attributes, such as 'multiple', are only 

596 set once during the bone's lifetime. Derived bone classes should not need to overwrite this 

597 method unless they have additional attributes with similar constraints. 

598 

599 :param key: A string representing the attribute name. 

600 :param value: The value to be assigned to the attribute. 

601 

602 :raises AttributeError: If a protected attribute is attempted to be modified after its initial 

603 assignment. 

604 """ 

605 # Bound to a Skeleton (skel_cls is set in __set_name__) means construction is complete: 

606 # from here on bone_strict_mode rejects assignment of unknown attribute names. 

607 if (self.skel_cls is not None and conf.bone_strict_mode 

608 and not key.startswith("_") and not hasattr(self, key)): 

609 # name/obj let Python's traceback machinery append "Did you mean: 'readOnly'?" 

610 raise AttributeError( 

611 f"{type(self).__name__} has no attribute {key!r} -- set after construction with " 

612 f"bone_strict_mode enabled. Disable via conf.bone_strict_mode = False.", 

613 name=key, obj=self, 

614 ) 

615 if not self.isClonedInstance and getSystemInitialized() and key != "isClonedInstance" and not key.startswith( 615 ↛ 617line 615 didn't jump to line 617 because the condition on line 615 was never true

616 "_"): 

617 raise AttributeError("You cannot modify this Skeleton. Grab a copy using .clone() first") 

618 super().__setattr__(key, value) 

619 

620 def collectRawClientData(self, name, data, multiple, languages, collectSubfields): 

621 """ 

622 Collects raw client data for the bone and returns it in a dictionary. 

623 

624 This method is called by the framework to gather raw data from the client, such as form data or data from a 

625 request. Derived bone classes should overwrite this method to implement their own logic for collecting raw data. 

626 

627 :param name: A string representing the bone's name. 

628 :param data: A dictionary containing the raw data from the client. 

629 :param multiple: A boolean indicating whether the bone supports multiple values. 

630 :param languages: An optional list of strings representing the supported languages (default: None). 

631 :param collectSubfields: A boolean indicating whether to collect data for subfields (default: False). 

632 

633 :return: A dictionary containing the collected raw client data. 

634 """ 

635 fieldSubmitted = False 

636 

637 if languages: 

638 res = {} 

639 for lang in languages: 

640 if not collectSubfields: 640 ↛ 652line 640 didn't jump to line 652 because the condition on line 640 was always true

641 if f"{name}.{lang}" in data: 

642 fieldSubmitted = True 

643 res[lang] = data[f"{name}.{lang}"] 

644 if multiple and not isinstance(res[lang], list): 644 ↛ 645line 644 didn't jump to line 645 because the condition on line 644 was never true

645 res[lang] = [res[lang]] 

646 elif not multiple and isinstance(res[lang], list): 646 ↛ 647line 646 didn't jump to line 647 because the condition on line 646 was never true

647 if res[lang]: 

648 res[lang] = res[lang][0] 

649 else: 

650 res[lang] = None 

651 else: 

652 for key in data.keys(): # Allow setting relations with using, multiple and languages back to none 

653 if key == f"{name}.{lang}": 

654 fieldSubmitted = True 

655 prefix = f"{name}.{lang}." 

656 if multiple: 

657 tmpDict = {} 

658 for key, value in data.items(): 

659 if not key.startswith(prefix): 

660 continue 

661 fieldSubmitted = True 

662 partKey = key[len(prefix):] 

663 firstKey, remainingKey = partKey.split(".", maxsplit=1) 

664 try: 

665 firstKey = int(firstKey) 

666 except: 

667 continue 

668 if firstKey not in tmpDict: 

669 tmpDict[firstKey] = {} 

670 tmpDict[firstKey][remainingKey] = value 

671 tmpList = list(tmpDict.items()) 

672 tmpList.sort(key=lambda x: x[0]) 

673 res[lang] = [x[1] for x in tmpList] 

674 else: 

675 tmpDict = {} 

676 for key, value in data.items(): 

677 if not key.startswith(prefix): 

678 continue 

679 fieldSubmitted = True 

680 partKey = key[len(prefix):] 

681 tmpDict[partKey] = value 

682 res[lang] = tmpDict 

683 return res, fieldSubmitted 

684 else: # No multi-lang 

685 if not collectSubfields: 685 ↛ 699line 685 didn't jump to line 699 because the condition on line 685 was always true

686 if name not in data: # Empty! 686 ↛ 687line 686 didn't jump to line 687 because the condition on line 686 was never true

687 return None, False 

688 val = data[name] 

689 if multiple and not isinstance(val, list): 689 ↛ 690line 689 didn't jump to line 690 because the condition on line 689 was never true

690 return [val], True 

691 elif not multiple and isinstance(val, list): 691 ↛ 692line 691 didn't jump to line 692 because the condition on line 691 was never true

692 if val: 

693 return val[0], True 

694 else: 

695 return None, True # Empty! 

696 else: 

697 return val, True 

698 else: # No multi-lang but collect subfields 

699 for key in data.keys(): # Allow setting relations with using, multiple and languages back to none 

700 if key == name: 

701 fieldSubmitted = True 

702 prefix = f"{name}." 

703 if multiple: 

704 tmpDict = {} 

705 for key, value in data.items(): 

706 if not key.startswith(prefix): 

707 continue 

708 fieldSubmitted = True 

709 partKey = key[len(prefix):] 

710 try: 

711 firstKey, remainingKey = partKey.split(".", maxsplit=1) 

712 firstKey = int(firstKey) 

713 except: 

714 continue 

715 if firstKey not in tmpDict: 

716 tmpDict[firstKey] = {} 

717 tmpDict[firstKey][remainingKey] = value 

718 tmpList = list(tmpDict.items()) 

719 tmpList.sort(key=lambda x: x[0]) 

720 return [x[1] for x in tmpList], fieldSubmitted 

721 else: 

722 res = {} 

723 for key, value in data.items(): 

724 if not key.startswith(prefix): 

725 continue 

726 fieldSubmitted = True 

727 subKey = key[len(prefix):] 

728 res[subKey] = value 

729 return res, fieldSubmitted 

730 

731 def parseSubfieldsFromClient(self) -> bool: 

732 """ 

733 Determines whether the function should parse subfields submitted by the client. 

734 Set to True only when expecting a list of dictionaries to be transmitted. 

735 """ 

736 return False 

737 

738 def singleValueFromClient(self, value: t.Any, skel: 'SkeletonInstance', 

739 bone_name: str, client_data: dict 

740 ) -> tuple[t.Any, list[ReadFromClientError] | None]: 

741 """Load a single value from a client 

742 

743 :param value: The single value which should be loaded. 

744 :param skel: The SkeletonInstance where the value should be loaded into. 

745 :param bone_name: The bone name of this bone in the SkeletonInstance. 

746 :param client_data: The data taken from the client, 

747 a dictionary with usually bone names as key 

748 :return: A tuple. If the value is valid, the first element is 

749 the parsed value and the second is None. 

750 If the value is invalid or not parseable, the first element is a empty value 

751 and the second a list of *ReadFromClientError*. 

752 """ 

753 # The BaseBone will not read any client_data in fromClient. Use rawValueBone if needed. 

754 return self.getEmptyValue(), [ 

755 ReadFromClientError(ReadFromClientErrorSeverity.Invalid, "Will not read a BaseBone from client!") 

756 ] 

757 

758 def fromClient(self, skel: 'SkeletonInstance', name: str, data: dict) -> None | list[ReadFromClientError]: 

759 """ 

760 Reads a value from the client and stores it in the skeleton instance if it is valid for the bone. 

761 

762 This function reads a value from the client and processes it according to the bone's configuration. 

763 If the value is valid for the bone, it stores the value in the skeleton instance and returns None. 

764 Otherwise, the previous value remains unchanged, and a list of ReadFromClientError objects is returned. 

765 

766 :param skel: A SkeletonInstance object where the values should be loaded. 

767 :param name: A string representing the bone's name. 

768 :param data: A dictionary containing the raw data from the client. 

769 :return: None if no errors occurred, otherwise a list of ReadFromClientError objects. 

770 """ 

771 subFields = self.parseSubfieldsFromClient() 

772 parsedData, fieldSubmitted = self.collectRawClientData(name, data, self.multiple, self.languages, subFields) 

773 if not fieldSubmitted: 773 ↛ 774line 773 didn't jump to line 774 because the condition on line 773 was never true

774 return [ReadFromClientError(ReadFromClientErrorSeverity.NotSet)] 

775 

776 errors = [] 

777 isEmpty = True 

778 filled_languages = set() 

779 if self.languages and self.multiple: 

780 res = {} 

781 for language in self.languages: 

782 res[language] = [] 

783 if language in parsedData: 

784 for idx, singleValue in enumerate(parsedData[language]): 

785 if self.isEmpty(singleValue): 785 ↛ 786line 785 didn't jump to line 786 because the condition on line 785 was never true

786 continue 

787 isEmpty = False 

788 filled_languages.add(language) 

789 parsedVal, parseErrors = self.singleValueFromClient(singleValue, skel, name, data) 

790 res[language].append(parsedVal) 

791 if isinstance(self.multiple, MultipleConstraints) and self.multiple.sorted: 791 ↛ 792line 791 didn't jump to line 792 because the condition on line 791 was never true

792 if callable(self.multiple.sorted): 

793 res[language] = sorted( 

794 res[language], 

795 key=self.multiple.sorted, 

796 reverse=self.multiple.reversed, 

797 ) 

798 else: 

799 res[language] = sorted(res[language], reverse=self.multiple.reversed) 

800 if parseErrors: 800 ↛ 801line 800 didn't jump to line 801 because the condition on line 800 was never true

801 for parseError in parseErrors: 

802 parseError.fieldPath[:0] = [language, str(idx)] 

803 errors.extend(parseErrors) 

804 elif self.languages: # and not self.multiple is implicit - this would have been handled above 

805 res = {} 

806 for language in self.languages: 

807 res[language] = None 

808 if language in parsedData: 

809 if self.isEmpty(parsedData[language]): 809 ↛ 810line 809 didn't jump to line 810 because the condition on line 809 was never true

810 res[language] = self.getEmptyValue() 

811 continue 

812 isEmpty = False 

813 filled_languages.add(language) 

814 parsedVal, parseErrors = self.singleValueFromClient(parsedData[language], skel, name, data) 

815 res[language] = parsedVal 

816 if parseErrors: 816 ↛ 817line 816 didn't jump to line 817 because the condition on line 816 was never true

817 for parseError in parseErrors: 

818 parseError.fieldPath.insert(0, language) 

819 errors.extend(parseErrors) 

820 elif self.multiple: # and not self.languages is implicit - this would have been handled above 

821 res = [] 

822 for idx, singleValue in enumerate(parsedData): 

823 if self.isEmpty(singleValue): 823 ↛ 824line 823 didn't jump to line 824 because the condition on line 823 was never true

824 continue 

825 isEmpty = False 

826 parsedVal, parseErrors = self.singleValueFromClient(singleValue, skel, name, data) 

827 res.append(parsedVal) 

828 

829 if parseErrors: 829 ↛ 830line 829 didn't jump to line 830 because the condition on line 829 was never true

830 for parseError in parseErrors: 

831 parseError.fieldPath.insert(0, str(idx)) 

832 errors.extend(parseErrors) 

833 if isinstance(self.multiple, MultipleConstraints) and self.multiple.sorted: 833 ↛ 834line 833 didn't jump to line 834 because the condition on line 833 was never true

834 if callable(self.multiple.sorted): 

835 res = sorted(res, key=self.multiple.sorted, reverse=self.multiple.reversed) 

836 else: 

837 res = sorted(res, reverse=self.multiple.reversed) 

838 else: # No Languages, not multiple 

839 if self.isEmpty(parsedData): 

840 res = self.getEmptyValue() 

841 isEmpty = True 

842 else: 

843 isEmpty = False 

844 res, parseErrors = self.singleValueFromClient(parsedData, skel, name, data) 

845 if parseErrors: 

846 errors.extend(parseErrors) 

847 skel[name] = res 

848 if self.languages and isinstance(self.required, (list, tuple)): 848 ↛ 849line 848 didn't jump to line 849 because the condition on line 848 was never true

849 missing = set(self.required).difference(filled_languages) 

850 if missing: 

851 result_errors = [ 

852 ReadFromClientError(ReadFromClientErrorSeverity.Empty, fieldPath=[lang]) 

853 for lang in missing 

854 ] 

855 self.after_from_client(skel, name, result_errors) 

856 return result_errors or None 

857 

858 if isEmpty: 

859 result_errors = [ReadFromClientError(ReadFromClientErrorSeverity.Empty)] 

860 self.after_from_client(skel, name, result_errors) 

861 return result_errors or None 

862 

863 # Check multiple constraints on demand 

864 if self.multiple and isinstance(self.multiple, MultipleConstraints): 864 ↛ 865line 864 didn't jump to line 865 because the condition on line 864 was never true

865 errors.extend(self._validate_multiple_contraints(self.multiple, skel, name)) 

866 

867 self.after_from_client(skel, name, errors) 

868 return errors or None 

869 

870 def _get_single_destinct_hash(self, value) -> t.Any: 

871 """ 

872 Returns a distinct hash value for a single entry of this bone. 

873 The returned value must be hashable. 

874 """ 

875 return value 

876 

877 def _get_destinct_hash(self, skel: 'SkeletonInstance', name: str) -> t.Any: 

878 """ 

879 Returns a distinct hash value for this bone. 

880 The returned value must be hashable. 

881 """ 

882 values = [] 

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

884 values.append(self._get_single_destinct_hash(value)) 

885 

886 return tuple(values) 

887 

888 def _validate_multiple_contraints( 

889 self, 

890 constraints: MultipleConstraints, 

891 skel: 'SkeletonInstance', 

892 name: str 

893 ) -> list[ReadFromClientError]: 

894 """ 

895 Validates the value of a bone against its multiple constraints and returns a list of ReadFromClientError 

896 objects for each violation, such as too many items or duplicates. 

897 

898 :param constraints: The MultipleConstraints definition to apply. 

899 :param skel: A SkeletonInstance object where the values should be validated. 

900 :param name: A string representing the bone's name. 

901 :return: A list of ReadFromClientError objects for each constraint violation. 

902 """ 

903 res = [] 

904 value = self._get_destinct_hash(skel, name) 

905 

906 if constraints.min and len(value) < constraints.min: 

907 res.append( 

908 ReadFromClientError( 

909 ReadFromClientErrorSeverity.Invalid, 

910 i18n.translate("core.bones.error.toofewitems", "Too few items") 

911 ) 

912 ) 

913 

914 if constraints.max and len(value) > constraints.max: 

915 res.append( 

916 ReadFromClientError( 

917 ReadFromClientErrorSeverity.Invalid, 

918 i18n.translate("core.bones.error.toomanyitems", "Too many items") 

919 ) 

920 ) 

921 

922 if not constraints.duplicates: 

923 if len(set(value)) != len(value): 

924 res.append( 

925 ReadFromClientError( 

926 ReadFromClientErrorSeverity.Invalid, 

927 i18n.translate("core.bones.error.duplicateitems", "Duplicate items"), 

928 ) 

929 ) 

930 

931 return res 

932 

933 def singleValueSerialize(self, value, skel: 'SkeletonInstance', name: str, parentIndexed: bool): 

934 """ 

935 Serializes a single value of the bone for storage in the database. 

936 

937 Derived bone classes should overwrite this method to implement their own logic for serializing single 

938 values. 

939 The serialized value should be suitable for storage in the database. 

940 """ 

941 return value 

942 

943 def serialize(self, skel: 'SkeletonInstance', name: str, parentIndexed: bool) -> bool: 

944 """ 

945 Serializes this bone into a format that can be written into the datastore. 

946 

947 :param skel: A SkeletonInstance object containing the values to be serialized. 

948 :param name: A string representing the property name of the bone in its Skeleton (not the description). 

949 :param parentIndexed: A boolean indicating whether the parent bone is indexed. 

950 :return: A boolean indicating whether the serialization was successful. 

951 """ 

952 self.serialize_compute(skel, name) 

953 

954 if name in skel.accessedValues: 

955 empty_value = self.getEmptyValue() 

956 newVal = skel.accessedValues[name] 

957 

958 if self.languages and self.multiple: 958 ↛ 959line 958 didn't jump to line 959 because the condition on line 958 was never true

959 res = db.Entity() 

960 res["_viurLanguageWrapper_"] = True 

961 for language in self.languages: 

962 res[language] = [] 

963 if not self.indexed: 

964 res.exclude_from_indexes.add(language) 

965 if language in newVal: 

966 for singleValue in newVal[language]: 

967 value = self.singleValueSerialize(singleValue, skel, name, parentIndexed) 

968 if value != empty_value: 

969 res[language].append(value) 

970 

971 elif self.languages: 

972 res = db.Entity() 

973 res["_viurLanguageWrapper_"] = True 

974 for language in self.languages: 

975 res[language] = None 

976 if not self.indexed: 976 ↛ 977line 976 didn't jump to line 977 because the condition on line 976 was never true

977 res.exclude_from_indexes.add(language) 

978 if language in newVal: 978 ↛ 974line 978 didn't jump to line 974 because the condition on line 978 was always true

979 res[language] = self.singleValueSerialize(newVal[language], skel, name, parentIndexed) 

980 

981 elif self.multiple: 

982 res = [] 

983 

984 assert newVal is None or isinstance(newVal, (list, tuple)), \ 

985 f"Cannot handle {repr(newVal)} here. Expecting list or tuple." 

986 

987 for singleValue in (newVal or ()): 

988 value = self.singleValueSerialize(singleValue, skel, name, parentIndexed) 

989 if value != empty_value: 989 ↛ 987line 989 didn't jump to line 987 because the condition on line 989 was always true

990 res.append(value) 

991 

992 else: # No Languages, not Multiple 

993 res = self.singleValueSerialize(newVal, skel, name, parentIndexed) 

994 

995 skel.dbEntity[name] = res 

996 

997 # Ensure our indexed flag is up2date 

998 indexed = self.indexed and parentIndexed 

999 if indexed and name in skel.dbEntity.exclude_from_indexes: 999 ↛ 1000line 999 didn't jump to line 1000 because the condition on line 999 was never true

1000 skel.dbEntity.exclude_from_indexes.discard(name) 

1001 elif not indexed and name not in skel.dbEntity.exclude_from_indexes: 1001 ↛ 1002line 1001 didn't jump to line 1002 because the condition on line 1001 was never true

1002 skel.dbEntity.exclude_from_indexes.add(name) 

1003 return True 

1004 return False 

1005 

1006 def serialize_compute(self, skel: "SkeletonInstance", name: str) -> None: 

1007 """ 

1008 This function checks whether a bone is computed and if this is the case, it attempts to serialize the 

1009 value with the appropriate calculation method 

1010 

1011 :param skel: The SkeletonInstance where the current bone is located 

1012 :param name: The name of the bone in the Skeleton 

1013 """ 

1014 if not self.compute: 

1015 return None 

1016 

1017 match self.compute.interval.method: 

1018 case ComputeMethod.OnWrite: 

1019 skel.accessedValues[name] = self._compute(skel, name) 

1020 

1021 case ComputeMethod.Lifetime: 1021 ↛ 1022line 1021 didn't jump to line 1022 because the pattern on line 1021 never matched

1022 now = utils.utcNow() 

1023 

1024 last_update = \ 

1025 skel.accessedValues.get(f"_viur_compute_{name}_") \ 

1026 or skel.dbEntity.get(f"_viur_compute_{name}_") 

1027 

1028 if not last_update or last_update + self.compute.interval.lifetime < now: 

1029 skel.accessedValues[name] = self._compute(skel, name) 

1030 skel.dbEntity[f"_viur_compute_{name}_"] = now 

1031 

1032 case ComputeMethod.Once: 

1033 if name not in skel.dbEntity: 1033 ↛ exitline 1033 didn't return from function 'serialize_compute' because the condition on line 1033 was always true

1034 val = self._compute(skel, name) 

1035 if val is not None: 1035 ↛ exitline 1035 didn't return from function 'serialize_compute' because the condition on line 1035 was always true

1036 skel.accessedValues[name] = val 

1037 

1038 def singleValueUnserialize(self, val): 

1039 """ 

1040 Unserializes a single value of the bone from the stored database value. 

1041 

1042 Derived bone classes should overwrite this method to implement their own logic for unserializing 

1043 single values. The unserialized value should be suitable for use in the application logic. 

1044 """ 

1045 return val 

1046 

1047 def unserialize(self, skel: 'viur.core.skeleton.SkeletonInstance', name: str) -> bool: 

1048 """ 

1049 Deserialize bone data from the datastore and populate the bone with the deserialized values. 

1050 

1051 This function is the inverse of the serialize function. It converts data from the datastore 

1052 into a format that can be used by the bones in the skeleton. 

1053 

1054 :param skel: A SkeletonInstance object containing the values to be deserialized. 

1055 :param name: The property name of the bone in its Skeleton (not the description). 

1056 :returns: True if deserialization is successful, False otherwise. 

1057 """ 

1058 if name in skel.dbEntity: 

1059 loadVal = skel.dbEntity[name] 

1060 elif ( 

1061 # fixme: Remove this piece of sh*t at least with VIUR4 

1062 # We're importing from an old ViUR2 instance - there may only be keys prefixed with our name 

1063 conf.viur2import_blobsource and any(n.startswith(name + ".") for n in skel.dbEntity) 

1064 # ... or computed 

1065 or self.compute 

1066 ): 

1067 loadVal = None 

1068 else: 

1069 skel.accessedValues[name] = self.getDefaultValue(skel) 

1070 return False 

1071 

1072 if self.unserialize_compute(skel, name): 

1073 return True 

1074 

1075 # unserialize value to given config 

1076 if self.languages and self.multiple: 1076 ↛ 1077line 1076 didn't jump to line 1077 because the condition on line 1076 was never true

1077 res = {} 

1078 if isinstance(loadVal, dict) and "_viurLanguageWrapper_" in loadVal: 

1079 for language in self.languages: 

1080 res[language] = [] 

1081 if language in loadVal: 

1082 tmpVal = loadVal[language] 

1083 if not isinstance(tmpVal, list): 

1084 tmpVal = [tmpVal] 

1085 for singleValue in tmpVal: 

1086 res[language].append(self.singleValueUnserialize(singleValue)) 

1087 else: # We could not parse this, maybe it has been written before languages had been set? 

1088 for language in self.languages: 

1089 res[language] = [] 

1090 mainLang = self.languages[0] 

1091 if loadVal is None: 

1092 pass 

1093 elif isinstance(loadVal, list): 

1094 for singleValue in loadVal: 

1095 res[mainLang].append(self.singleValueUnserialize(singleValue)) 

1096 else: # Hopefully it's a value stored before languages and multiple has been set 

1097 res[mainLang].append(self.singleValueUnserialize(loadVal)) 

1098 elif self.languages: 

1099 res = {} 

1100 if isinstance(loadVal, dict) and "_viurLanguageWrapper_" in loadVal: 

1101 for language in self.languages: 

1102 res[language] = None 

1103 if language in loadVal: 1103 ↛ 1101line 1103 didn't jump to line 1101 because the condition on line 1103 was always true

1104 tmpVal = loadVal[language] 

1105 if isinstance(tmpVal, list) and tmpVal: 1105 ↛ 1106line 1105 didn't jump to line 1106 because the condition on line 1105 was never true

1106 tmpVal = tmpVal[0] 

1107 res[language] = self.singleValueUnserialize(tmpVal) 

1108 else: # We could not parse this, maybe it has been written before languages had been set? 

1109 for language in self.languages: 

1110 res[language] = None 

1111 oldKey = f"{name}.{language}" 

1112 if oldKey in skel.dbEntity and skel.dbEntity[oldKey]: 1112 ↛ 1113line 1112 didn't jump to line 1113 because the condition on line 1112 was never true

1113 res[language] = self.singleValueUnserialize(skel.dbEntity[oldKey]) 

1114 loadVal = None # Don't try to import later again, this format takes precedence 

1115 mainLang = self.languages[0] 

1116 if loadVal is None: 1116 ↛ 1117line 1116 didn't jump to line 1117 because the condition on line 1116 was never true

1117 pass 

1118 elif isinstance(loadVal, list) and loadVal: 1118 ↛ 1119line 1118 didn't jump to line 1119 because the condition on line 1118 was never true

1119 res[mainLang] = self.singleValueUnserialize(loadVal) 

1120 else: # Hopefully it's a value stored before languages and multiple has been set 

1121 res[mainLang] = self.singleValueUnserialize(loadVal) 

1122 elif self.multiple: 1122 ↛ 1123line 1122 didn't jump to line 1123 because the condition on line 1122 was never true

1123 res = [] 

1124 if isinstance(loadVal, dict) and "_viurLanguageWrapper_" in loadVal: 

1125 # Pick one language we'll use 

1126 if conf.i18n.default_language in loadVal: 

1127 loadVal = loadVal[conf.i18n.default_language] 

1128 else: 

1129 loadVal = [x for x in loadVal.values() if x is not True] 

1130 if loadVal and not isinstance(loadVal, list): 

1131 loadVal = [loadVal] 

1132 if loadVal: 

1133 for val in loadVal: 

1134 res.append(self.singleValueUnserialize(val)) 

1135 else: # Not multiple, no languages 

1136 res = None 

1137 if isinstance(loadVal, dict) and "_viurLanguageWrapper_" in loadVal: 1137 ↛ 1139line 1137 didn't jump to line 1139 because the condition on line 1137 was never true

1138 # Pick one language we'll use 

1139 if conf.i18n.default_language in loadVal: 

1140 loadVal = loadVal[conf.i18n.default_language] 

1141 else: 

1142 loadVal = [x for x in loadVal.values() if x is not True] 

1143 if loadVal and isinstance(loadVal, list): 1143 ↛ 1144line 1143 didn't jump to line 1144 because the condition on line 1143 was never true

1144 loadVal = loadVal[0] 

1145 if loadVal is not None: 1145 ↛ 1148line 1145 didn't jump to line 1148 because the condition on line 1145 was always true

1146 res = self.singleValueUnserialize(loadVal) 

1147 

1148 skel.accessedValues[name] = res 

1149 return True 

1150 

1151 def unserialize_compute(self, skel: "SkeletonInstance", name: str) -> bool: 

1152 """ 

1153 This function checks whether a bone is computed and if this is the case, it attempts to deserialise the 

1154 value with the appropriate calculation method 

1155 

1156 :param skel : The SkeletonInstance where the current Bone is located 

1157 :param name: The name of the Bone in the Skeleton 

1158 :return: True if the Bone was unserialized, False otherwise 

1159 """ 

1160 if ( 

1161 not self.compute 

1162 or self._prevent_compute # deprecated, still honoured for code outside of the core 

1163 or self.is_computing(skel, name) 

1164 or skel._cascade_deletion 

1165 ): 

1166 return False 

1167 

1168 match self.compute.interval.method: 

1169 # Computation is bound to a lifetime? 

1170 case ComputeMethod.Lifetime: 1170 ↛ 1171line 1170 didn't jump to line 1171 because the pattern on line 1170 never matched

1171 now = utils.utcNow() 

1172 from viur.core.skeleton import RefSkel # noqa: E402 # import works only here because circular imports 

1173 

1174 if skel["key"] and skel.dbEntity: 

1175 if issubclass(skel.skeletonCls, RefSkel): # we have a ref skel we must load the complete Entity 

1176 db_obj = db.get(skel["key"]) 

1177 last_update = db_obj.get(f"_viur_compute_{name}_") 

1178 else: 

1179 last_update = skel.dbEntity.get(f"_viur_compute_{name}_") 

1180 skel.accessedValues[f"_viur_compute_{name}_"] = last_update or now 

1181 

1182 if not last_update or last_update + self.compute.interval.lifetime <= now: 

1183 # if so, recompute and refresh updated value 

1184 skel.accessedValues[name] = value = self._compute(skel, name) 

1185 

1186 def transact(): 

1187 db_obj = db.get(skel["key"]) 

1188 db_obj[f"_viur_compute_{name}_"] = now 

1189 db_obj[name] = value 

1190 db.put(db_obj) 

1191 

1192 if db.is_in_transaction(): 

1193 transact() 

1194 else: 

1195 db.run_in_transaction(transact) 

1196 

1197 else: 

1198 return False 

1199 

1200 else: 

1201 # Run like ComputeMethod.Always on unwritten skeleton 

1202 skel.accessedValues[name] = self._compute(skel, name) 

1203 

1204 return True 

1205 

1206 # Compute on every deserialization 

1207 case ComputeMethod.Always: 1207 ↛ 1211line 1207 didn't jump to line 1211 because the pattern on line 1207 always matched

1208 skel.accessedValues[name] = self._compute(skel, name) 

1209 return True 

1210 

1211 return False 

1212 

1213 def delete(self, skel: 'viur.core.skeleton.SkeletonInstance', name: str): 

1214 """ 

1215 Like postDeletedHandler, but runs inside the transaction 

1216 """ 

1217 pass 

1218 

1219 def buildDBFilter(self, 

1220 name: str, 

1221 skel: 'viur.core.skeleton.SkeletonInstance', 

1222 dbFilter: db.Query, 

1223 rawFilter: dict, 

1224 prefix: t.Optional[str] = None) -> db.Query: 

1225 """ 

1226 Parses the searchfilter a client specified in his Request into 

1227 something understood by the datastore. 

1228 This function must: 

1229 

1230 * - Ignore all filters not targeting this bone 

1231 * - Safely handle malformed data in rawFilter (this parameter is directly controlled by the client) 

1232 

1233 :param name: The property-name this bone has in its Skeleton (not the description!) 

1234 :param skel: The :class:`viur.core.db.Query` this bone is part of 

1235 :param dbFilter: The current :class:`viur.core.db.Query` instance the filters should be applied to 

1236 :param rawFilter: The dictionary of filters the client wants to have applied 

1237 :returns: The modified :class:`viur.core.db.Query` 

1238 """ 

1239 myKeys = [key for key in rawFilter.keys() if (key == name or key.startswith(name + "$"))] 

1240 

1241 if len(myKeys) == 0: 

1242 return dbFilter 

1243 

1244 for key in myKeys: 

1245 value = rawFilter[key] 

1246 tmpdata = key.split("$") 

1247 

1248 if len(tmpdata) > 1: 

1249 if isinstance(value, list): 

1250 continue 

1251 if tmpdata[1] == "lt": 

1252 dbFilter.filter((prefix or "") + tmpdata[0] + " <", value) 

1253 elif tmpdata[1] == "le": 

1254 dbFilter.filter((prefix or "") + tmpdata[0] + " <=", value) 

1255 elif tmpdata[1] == "gt": 

1256 dbFilter.filter((prefix or "") + tmpdata[0] + " >", value) 

1257 elif tmpdata[1] == "ge": 

1258 dbFilter.filter((prefix or "") + tmpdata[0] + " >=", value) 

1259 elif tmpdata[1] == "lk": 

1260 dbFilter.filter((prefix or "") + tmpdata[0] + " =", value) 

1261 else: 

1262 dbFilter.filter((prefix or "") + tmpdata[0] + " =", value) 

1263 else: 

1264 if isinstance(value, list): 

1265 dbFilter.filter((prefix or "") + key + " IN", value) 

1266 else: 

1267 dbFilter.filter((prefix or "") + key + " =", value) 

1268 

1269 return dbFilter 

1270 

1271 def buildDBSort( 

1272 self, 

1273 name: str, 

1274 skel: "SkeletonInstance", 

1275 query: db.Query, 

1276 params: dict, 

1277 postfix: str = "", 

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

1279 """ 

1280 Same as buildDBFilter, but this time its not about filtering 

1281 the results, but by sorting them. 

1282 Again: query is controlled by the client, so you *must* expect and safely handle 

1283 malformed data! 

1284 

1285 :param name: The property-name this bone has in its Skeleton (not the description!) 

1286 :param skel: The :class:`viur.core.skeleton.Skeleton` instance this bone is part of 

1287 :param dbFilter: The current :class:`viur.core.db.Query` instance the filters should 

1288 be applied to 

1289 :param query: The dictionary of filters the client wants to have applied 

1290 :param postfix: Inherited classes may use this to add a postfix to the porperty name 

1291 :returns: The modified :class:`viur.core.db.Query`, 

1292 None if the query is unsatisfiable. 

1293 """ 

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

1295 if self.languages: 

1296 lang = None 

1297 prefix = f"{name}." 

1298 if orderby.startswith(prefix): 

1299 lng = orderby[len(prefix):] 

1300 if lng in self.languages: 

1301 lang = lng 

1302 

1303 if lang is None: 

1304 lang = current.language.get() 

1305 if not lang or lang not in self.languages: 

1306 lang = self.languages[0] 

1307 

1308 prop = f"{name}.{lang}" 

1309 else: 

1310 prop = name 

1311 

1312 # In case this is a multiple query, check if all filters are valid 

1313 if isinstance(query.queries, list): 

1314 in_eq_filter = None 

1315 

1316 for item in query.queries: 

1317 new_in_eq_filter = [ 

1318 key for key in item.filters.keys() 

1319 if key.rstrip().endswith(("<", ">", "!=")) 

1320 ] 

1321 if in_eq_filter and new_in_eq_filter and in_eq_filter != new_in_eq_filter: 

1322 raise NotImplementedError("Impossible ordering!") 

1323 

1324 in_eq_filter = new_in_eq_filter 

1325 

1326 else: 

1327 in_eq_filter = [ 

1328 key for key in query.queries.filters.keys() 

1329 if key.rstrip().endswith(("<", ">", "!=")) 

1330 ] 

1331 

1332 if in_eq_filter: 

1333 orderby_prop = in_eq_filter[0].split(" ", 1)[0] 

1334 if orderby_prop != prop: 

1335 logging.warning( 

1336 f"The query was rewritten; Impossible ordering changed from {prop!r} into {orderby_prop!r}" 

1337 ) 

1338 prop = orderby_prop 

1339 

1340 query.order((prop + postfix, db.SortOrder.from_str(params.get("orderdir")))) 

1341 

1342 return query 

1343 

1344 def _hashValueForUniquePropertyIndex( 

1345 self, 

1346 value: str | int | float | db.Key | list[str | int | float | db.Key], 

1347 ) -> list[str]: 

1348 """ 

1349 Generates a hash of the given value for creating unique property indexes. 

1350 

1351 This method is called by the framework to create a consistent hash representation of a value 

1352 for constructing unique property indexes. Derived bone classes should overwrite this method to 

1353 implement their own logic for hashing values. 

1354 

1355 :param value: The value(s) to be hashed. 

1356 

1357 :return: A list containing a string representation of the hashed value. If the bone is multiple, 

1358 the list may contain more than one hashed value. 

1359 """ 

1360 

1361 def hash_value(value: str | int | float | db.Key) -> str: 

1362 h = hashlib.sha256() 

1363 h.update(str(value).encode("UTF-8")) 

1364 res = h.hexdigest() 

1365 

1366 if isinstance(value, int | float): 1366 ↛ 1367line 1366 didn't jump to line 1367 because the condition on line 1366 was never true

1367 return f"I-{res}" 

1368 elif isinstance(value, str): 1368 ↛ 1370line 1368 didn't jump to line 1370 because the condition on line 1368 was always true

1369 return f"S-{res}" 

1370 elif isinstance(value, db.Key): 

1371 # We Hash the keys here by our self instead of relying on str() or to_legacy_urlsafe() 

1372 # as these may change in the future, which would invalidate all existing locks 

1373 def keyHash(key): 

1374 if key is None: 

1375 return "-" 

1376 return f"{hash_value(key.kind)}-{hash_value(key.id_or_name)}-<{keyHash(key.parent)}>" 

1377 

1378 return f"K-{keyHash(value)}" 

1379 

1380 raise NotImplementedError(f"Type {type(value)} can't be safely used in an uniquePropertyIndex") 

1381 

1382 # zero/empty string and these should not be locked 

1383 if not value and not self.unique.lockEmpty: 1383 ↛ 1384line 1383 didn't jump to line 1384 because the condition on line 1383 was never true

1384 return [] 

1385 

1386 # Always work with list of values 

1387 if not isinstance(value, list): 1387 ↛ 1388line 1387 didn't jump to line 1388 because the condition on line 1387 was never true

1388 value = [value] 

1389 

1390 values = [hash_value(val) for val in value] 

1391 

1392 if self.unique.method == UniqueLockMethod.SameValue: 

1393 # Lock each entry individually 

1394 return values 

1395 

1396 elif self.unique.method == UniqueLockMethod.SameSet: 1396 ↛ 1401line 1396 didn't jump to line 1401 because the condition on line 1396 was always true

1397 # Ignore the sort-order; so simply sort that list 

1398 values.sort() 

1399 

1400 # Lock the value for that specific list (equals to UniqueLockMethod.SameList) 

1401 return [hash_value(", ".join(values))] 

1402 

1403 def getUniquePropertyIndexValues(self, skel: 'viur.core.skeleton.SkeletonInstance', name: str) -> list[str]: 

1404 """ 

1405 Returns a list of hashes for the current value(s) of a bone in the skeleton, used for storing in the 

1406 unique property value index. 

1407 

1408 :param skel: A SkeletonInstance object representing the current skeleton. 

1409 :param name: The property-name of the bone in the skeleton for which the unique property index values 

1410 are required (not the description!). 

1411 

1412 :return: A list of strings representing the hashed values for the current bone value(s) in the skeleton. 

1413 If the bone has no value, an empty list is returned. 

1414 """ 

1415 if self.compute: 1415 ↛ 1416line 1415 didn't jump to line 1416 because the condition on line 1415 was never true

1416 self.serialize_compute(skel, name) 

1417 

1418 values = [value for _, _, value in self.iter_bone_value(skel, name) if value is not None] 

1419 return self._hashValueForUniquePropertyIndex(values) if values else [] 

1420 

1421 def getReferencedBlobs(self, skel: 'viur.core.skeleton.SkeletonInstance', name: str) -> set[str]: 

1422 """ 

1423 Returns a set of blob keys referenced from this bone 

1424 """ 

1425 return set() 

1426 

1427 def performMagic(self, valuesCache: dict, name: str, isAdd: bool): 

1428 """ 

1429 This function applies "magically" functionality which f.e. inserts the current Date 

1430 or the current user. 

1431 :param isAdd: Signals wherever this is an add or edit operation. 

1432 """ 

1433 pass # We do nothing by default 

1434 

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

1436 """ 

1437 Can be overridden to perform further actions after the main entity has been written. 

1438 

1439 :param boneName: Name of this bone 

1440 :param skel: The skeleton this bone belongs to 

1441 :param key: The (new?) Database Key we've written to. In case of a RelSkel the key is None. 

1442 """ 

1443 pass 

1444 

1445 def postDeletedHandler(self, skel: 'viur.core.skeleton.SkeletonInstance', boneName: str, key: str): 

1446 """ 

1447 Can be overridden to perform further actions after the main entity has been deleted. 

1448 

1449 :param skel: The skeleton this bone belongs to 

1450 :param boneName: Name of this bone 

1451 :param key: The old Database Key of the entity we've deleted 

1452 """ 

1453 pass 

1454 

1455 def after_from_client(self, skel: "SkeletonInstance", name: str, errors: list[ReadFromClientError]) -> None: 

1456 """ 

1457 Called at the end of :meth:`fromClient` after ``skel[name]`` has been set and all 

1458 validation (including multiple-constraints) has run. 

1459 

1460 Override to post-process or normalize ``skel[name]`` in-place, or to add/remove 

1461 entries from ``errors``. Always called when the field was part of the submitted data 

1462 (i.e. ``skel[name]`` has been written), regardless of whether errors occurred. 

1463 The ``NotSet`` early-return (field absent from request) is the only case where this 

1464 hook is *not* called. 

1465 

1466 :param skel: The skeleton instance whose bone value was just read. 

1467 :param name: The attribute name of this bone within the skeleton. 

1468 :param errors: Mutable list of :class:`ReadFromClientError` collected so far. 

1469 Changes here affect the return value of :meth:`fromClient`. 

1470 """ 

1471 pass 

1472 

1473 def clone_value(self, skel: "SkeletonInstance", src_skel: "SkeletonInstance", bone_name: str) -> None: 

1474 """Clone / Set the value for this bone depending on :attr:`clone_behavior`""" 

1475 match self.clone_behavior.strategy: 

1476 case CloneStrategy.COPY_VALUE: 

1477 try: 

1478 skel.accessedValues[bone_name] = copy.deepcopy(src_skel.accessedValues[bone_name]) 

1479 except KeyError: 

1480 pass # bone_name is not in accessedValues, cannot clone it 

1481 try: 

1482 skel.renderAccessedValues[bone_name] = copy.deepcopy(src_skel.renderAccessedValues[bone_name]) 

1483 except KeyError: 

1484 pass # bone_name is not in renderAccessedValues, cannot clone it 

1485 case CloneStrategy.SET_NULL: 

1486 skel.accessedValues[bone_name] = None 

1487 case CloneStrategy.SET_DEFAULT: 

1488 skel.accessedValues[bone_name] = self.getDefaultValue(skel) 

1489 case CloneStrategy.SET_EMPTY: 

1490 skel.accessedValues[bone_name] = self.getEmptyValue() 

1491 case CloneStrategy.CUSTOM: 

1492 skel.accessedValues[bone_name] = self.clone_behavior.custom_func(skel, src_skel, bone_name) 

1493 case other: 

1494 raise NotImplementedError(other) 

1495 

1496 def refresh(self, skel: 'viur.core.skeleton.SkeletonInstance', boneName: str) -> None: 

1497 """ 

1498 Refresh all values we might have cached from other entities. 

1499 """ 

1500 pass 

1501 

1502 def mergeFrom(self, valuesCache: dict, boneName: str, otherSkel: 'viur.core.skeleton.SkeletonInstance'): 

1503 """ 

1504 Merges the values from another skeleton instance into the current instance, given that the bone types match. 

1505 

1506 :param valuesCache: A dictionary containing the cached values for each bone in the skeleton. 

1507 :param boneName: The property-name of the bone in the skeleton whose values are to be merged. 

1508 :param otherSkel: A SkeletonInstance object representing the other skeleton from which the values \ 

1509 are to be merged. 

1510 

1511 This function clones the values from the specified bone in the other skeleton instance into the current 

1512 instance, provided that the bone types match. If the bone types do not match, a warning is logged, and the merge 

1513 is ignored. If the bone in the other skeleton has no value, the function returns without performing any merge 

1514 operation. 

1515 """ 

1516 if getattr(otherSkel, boneName) is None: 

1517 return 

1518 if not isinstance(getattr(otherSkel, boneName), type(self)): 

1519 logging.error(f"Ignoring values from conflicting boneType ({getattr(otherSkel, boneName)} is not a " 

1520 f"instance of {type(self)})!") 

1521 return 

1522 valuesCache[boneName] = copy.deepcopy(otherSkel.valuesCache.get(boneName, None)) 

1523 

1524 def setBoneValue(self, 

1525 skel: 'SkeletonInstance', 

1526 boneName: str, 

1527 value: t.Any, 

1528 append: bool, 

1529 language: None | str = None) -> bool: 

1530 """ 

1531 Sets the value of a bone in a skeleton instance, with optional support for appending and language-specific 

1532 values. Sanity checks are being performed. 

1533 

1534 :param skel: The SkeletonInstance object representing the skeleton to which the bone belongs. 

1535 :param boneName: The property-name of the bone in the skeleton whose value should be set or modified. 

1536 :param value: The value to be assigned. Its type depends on the type of the bone. 

1537 :param append: If True, the given value is appended to the bone's values instead of replacing it. \ 

1538 Only supported for bones with multiple=True. 

1539 :param language: The language code for which the value should be set or appended, \ 

1540 if the bone supports languages. 

1541 

1542 :return: A boolean indicating whether the operation was successful or not. 

1543 

1544 This function sets or modifies the value of a bone in a skeleton instance, performing sanity checks to ensure 

1545 the value is valid. If the value is invalid, no modification occurs. The function supports appending values to 

1546 bones with multiple=True and setting or appending language-specific values for bones that support languages. 

1547 """ 

1548 assert not (bool(self.languages) ^ bool(language)), f"language is required or not supported on {boneName!r}" 

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

1550 

1551 if not append and self.multiple: 

1552 # set multiple values at once 

1553 val = [] 

1554 errors = [] 

1555 for singleValue in value: 

1556 singleValue, singleError = self.singleValueFromClient(singleValue, skel, boneName, {boneName: value}) 

1557 val.append(singleValue) 

1558 if singleError: 1558 ↛ 1559line 1558 didn't jump to line 1559 because the condition on line 1558 was never true

1559 errors.extend(singleError) 

1560 else: 

1561 # set or append one value 

1562 val, errors = self.singleValueFromClient(value, skel, boneName, {boneName: value}) 

1563 

1564 if errors: 

1565 for e in errors: 1565 ↛ 1570line 1565 didn't jump to line 1570 because the loop on line 1565 didn't complete

1566 if e.severity in [ReadFromClientErrorSeverity.Invalid, ReadFromClientErrorSeverity.NotSet]: 1566 ↛ 1565line 1566 didn't jump to line 1565 because the condition on line 1566 was always true

1567 # If an invalid datatype (or a non-parseable structure) have been passed, abort the store 

1568 logging.error(e) 

1569 return False 

1570 if not append and not language: 

1571 skel[boneName] = val 

1572 elif append and language: 1572 ↛ 1573line 1572 didn't jump to line 1573 because the condition on line 1572 was never true

1573 if not language in skel[boneName] or not isinstance(skel[boneName][language], list): 

1574 skel[boneName][language] = [] 

1575 skel[boneName][language].append(val) 

1576 elif append: 1576 ↛ 1581line 1576 didn't jump to line 1581 because the condition on line 1576 was always true

1577 if not isinstance(skel[boneName], list): 1577 ↛ 1578line 1577 didn't jump to line 1578 because the condition on line 1577 was never true

1578 skel[boneName] = [] 

1579 skel[boneName].append(val) 

1580 else: # Just language 

1581 skel[boneName][language] = val 

1582 return True 

1583 

1584 def getSearchTags(self, skel: 'viur.core.skeleton.SkeletonInstance', name: str) -> set[str]: 

1585 """ 

1586 Returns a set of strings as search index for this bone. 

1587 

1588 This function extracts a set of search tags from the given bone's value in the skeleton 

1589 instance. The resulting set can be used for indexing or searching purposes. 

1590 

1591 :param skel: The skeleton instance where the values should be loaded from. This is an instance 

1592 of a class derived from `viur.core.skeleton.SkeletonInstance`. 

1593 :param name: The name of the bone, which is a string representing the key for the bone in 

1594 the skeleton. This should correspond to an existing bone in the skeleton instance. 

1595 :return: A set of strings, extracted from the bone value. If the bone value doesn't have 

1596 any searchable content, an empty set is returned. 

1597 """ 

1598 return set() 

1599 

1600 def iter_bone_value( 

1601 self, skel: 'viur.core.skeleton.SkeletonInstance', name: str 

1602 ) -> t.Iterator[tuple[t.Optional[int], t.Optional[str], t.Any]]: 

1603 """ 

1604 Yield all values from the Skeleton related to this bone instance. 

1605 

1606 This method handles multiple/languages cases, which could save a lot of if/elifs. 

1607 It always yields a triplet: index, language, value. 

1608 Where index is the index (int) of a value inside a multiple bone, 

1609 language is the language (str) of a multi-language-bone, 

1610 and value is the value inside this container. 

1611 index or language is None if the bone is single or not multi-lang. 

1612 

1613 This function can be used to conveniently iterate through all the values of a specific bone 

1614 in a skeleton instance, taking into account multiple and multi-language bones. 

1615 

1616 :param skel: The skeleton instance where the values should be loaded from. This is an instance 

1617 of a class derived from `viur.core.skeleton.SkeletonInstance`. 

1618 :param name: The name of the bone, which is a string representing the key for the bone in 

1619 the skeleton. This should correspond to an existing bone in the skeleton instance. 

1620 

1621 :return: A generator which yields triplets (index, language, value), where index is the index 

1622 of a value inside a multiple bone, language is the language of a multi-language bone, 

1623 and value is the value inside this container. index or language is None if the bone is 

1624 single or not multi-lang. 

1625 """ 

1626 value = skel[name] 

1627 if not value: 

1628 return None 

1629 

1630 if self.languages and isinstance(value, dict): 

1631 for idx, (lang, values) in enumerate(value.items()): 

1632 if self.multiple: 

1633 if not values: 

1634 continue 

1635 for val in values: 

1636 yield idx, lang, val 

1637 else: 

1638 yield None, lang, values 

1639 else: 

1640 if self.multiple: 

1641 for idx, val in enumerate(value): 

1642 yield idx, None, val 

1643 else: 

1644 yield None, None, value 

1645 

1646 def is_computing(self, skel: "SkeletonInstance", bone_name: str) -> bool: 

1647 """ 

1648 Is this bone already being computed on this skeleton, in this thread? 

1649 

1650 A compute function that reaches its own bone again would recurse endlessly -- 

1651 a :class:`~viur.core.bones.relational.RelationalBone` with ``raw=False`` 

1652 unserializes it a second time. :meth:`_compute` therefore marks the 

1653 computation while it runs and :meth:`unserialize_compute` steps aside for it. 

1654 

1655 The mark must not be stored on the bone. Bone instances are shared by every 

1656 skeleton of their class and by every thread -- ``SkeletonInstance`` copies the 

1657 bone *map*, not the bones -- so a flag there also stops a *concurrent* request 

1658 from computing the bone. That request then silently receives the stored value, 

1659 which for a computed bone is usually ``None``. 

1660 

1661 :param skel: The skeleton the bone is computed on. 

1662 :param bone_name: Name of the bone. 

1663 :return: True while a computation of this bone on this skeleton is running. 

1664 """ 

1665 return _computation_key(skel, bone_name) in getattr(_computing_bones, "keys", ()) 

1666 

1667 @contextlib.contextmanager 

1668 def computing(self, skel: "SkeletonInstance", bone_name: str) -> t.Iterator[None]: 

1669 """ 

1670 Mark this bone as being computed on *skel* for the duration of the block. 

1671 

1672 :param skel: The skeleton the bone is computed on. 

1673 :param bone_name: Name of the bone. 

1674 """ 

1675 try: 

1676 keys = _computing_bones.keys 

1677 except AttributeError: 

1678 keys = _computing_bones.keys = set() 

1679 key = _computation_key(skel, bone_name) 

1680 keys.add(key) 

1681 try: 

1682 yield 

1683 finally: 

1684 keys.discard(key) 

1685 

1686 def _compute(self, skel: 'viur.core.skeleton.SkeletonInstance', bone_name: str): 

1687 """Performs the evaluation of a bone configured as compute""" 

1688 from ..skeleton.utils import without_render_preparation 

1689 

1690 compute_fn_parameters = inspect.signature(self.compute.fn).parameters 

1691 compute_fn_args = {} 

1692 skel = without_render_preparation(skel) 

1693 

1694 if "skel" in compute_fn_parameters: 

1695 compute_fn_args["skel"] = skel 

1696 

1697 if "bone" in compute_fn_parameters: 1697 ↛ 1698line 1697 didn't jump to line 1698 because the condition on line 1697 was never true

1698 compute_fn_args["bone"] = getattr(skel, bone_name) 

1699 

1700 if "bone_name" in compute_fn_parameters: 1700 ↛ 1701line 1700 didn't jump to line 1701 because the condition on line 1700 was never true

1701 compute_fn_args["bone_name"] = bone_name 

1702 

1703 # Marked for exactly the regions the previous flag covered, and released on 

1704 # every exit -- an exception out of the compute function used to leave the 

1705 # flag set and quietly disable the bone for the rest of the process. 

1706 with self.computing(skel, bone_name): 

1707 ret = self.compute.fn(**compute_fn_args) 

1708 

1709 if self.compute.raw or ret is None: 1709 ↛ 1726line 1709 didn't jump to line 1726 because the condition on line 1709 was always true

1710 if ret is None: # exit on None 1710 ↛ 1711line 1710 didn't jump to line 1711 because the condition on line 1710 was never true

1711 return ret 

1712 

1713 def unserialize_raw_value(raw_value: list[dict] | dict | None): 

1714 if self.multiple: 

1715 return [self.singleValueUnserialize(inner_value) for inner_value in raw_value] 

1716 return self.singleValueUnserialize(raw_value) 

1717 

1718 if self.languages: 1718 ↛ 1719line 1718 didn't jump to line 1719 because the condition on line 1718 was never true

1719 return { 

1720 lang: unserialize_raw_value(ret.get(lang, [] if self.multiple else None)) 

1721 for lang in self.languages 

1722 } 

1723 

1724 return unserialize_raw_value(ret) 

1725 

1726 with self.computing(skel, bone_name): 

1727 errors = self.fromClient(skel, bone_name, {bone_name: ret}) 

1728 

1729 if errors: 

1730 raise ValueError(f"Computed value fromClient failed with {errors!r}") 

1731 

1732 return skel[bone_name] 

1733 

1734 def structure(self) -> dict: 

1735 """ 

1736 Describes the bone and its settings as an JSON-serializable dict. 

1737 This function has to be implemented for subsequent, specialized bone types. 

1738 """ 

1739 ret = { 

1740 "clone_behavior": { 

1741 "strategy": self.clone_behavior.strategy, 

1742 }, 

1743 "descr": self.descr, 

1744 "emptyvalue": self.getEmptyValue(), 

1745 "indexed": self.indexed, 

1746 "languages": self.languages, 

1747 "params": self.params, 

1748 "readonly": self.readOnly, 

1749 "required": self.required and not self.readOnly, 

1750 "tags": self.tags, 

1751 "type": self.type, 

1752 "unique": self.unique.method.value if self.unique else False, 

1753 "visible": self.visible, 

1754 } 

1755 

1756 # Provide a defaultvalue, if it's not a function. 

1757 if not callable(self.defaultValue) and self.defaultValue is not None: 1757 ↛ 1758line 1757 didn't jump to line 1758 because the condition on line 1757 was never true

1758 ret["defaultvalue"] = self.defaultValue 

1759 

1760 # Provide a multiple setting 

1761 if self.multiple and isinstance(self.multiple, MultipleConstraints): 1761 ↛ 1762line 1761 didn't jump to line 1762 because the condition on line 1761 was never true

1762 ret["multiple"] = { 

1763 "duplicates": self.multiple.duplicates, 

1764 "max": self.multiple.max, 

1765 "min": self.multiple.min, 

1766 } 

1767 else: 

1768 ret["multiple"] = self.multiple 

1769 

1770 # Provide compute information 

1771 if self.compute: 

1772 ret["compute"] = { 

1773 "method": self.compute.interval.method.name 

1774 } 

1775 

1776 if self.compute.interval.lifetime: 1776 ↛ 1777line 1776 didn't jump to line 1777 because the condition on line 1776 was never true

1777 ret["compute"]["lifetime"] = self.compute.interval.lifetime.total_seconds() 

1778 

1779 return ret 

1780 

1781 def dump(self, skel: "SkeletonInstance", bone_name: str) -> t.Any: 

1782 """ 

1783 Returns the value of a bone in a JSON-serializable format. 

1784 

1785 The function is not called "to_json()" because the JSON-serializable 

1786 format can be used for different purposes and renderings, not just 

1787 JSON. 

1788 

1789 :param skel: The SkeletonInstance that contains the bone. 

1790 :param bone_name: The name of the bone to in the skeleton. 

1791 

1792 :return: The value of the bone in a JSON-serializable version. 

1793 """ 

1794 ret = {} 

1795 bone_value = skel[bone_name] 

1796 if self.languages and self.multiple: 

1797 for language in self.languages: 

1798 if bone_value and language in bone_value and bone_value[language]: 

1799 ret[language] = [self._atomic_dump(value) for value in bone_value[language]] 

1800 else: 

1801 ret[language] = [] 

1802 elif self.languages: 

1803 for language in self.languages: 

1804 if bone_value and language in bone_value and bone_value[language]: 

1805 ret[language] = self._atomic_dump(bone_value[language]) 

1806 else: 

1807 ret[language] = None 

1808 elif self.multiple: 

1809 ret = [self._atomic_dump(value) for value in bone_value or ()] 

1810 

1811 else: 

1812 ret = self._atomic_dump(bone_value) 

1813 return ret 

1814 

1815 def _atomic_dump(self, value): 

1816 """ 

1817 One atomic value of the bone. 

1818 """ 

1819 return value