Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/skeleton/base.py: 21%
94 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-25 15:02 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-25 15:02 +0000
1import fnmatch
2import logging
3import typing as t
5from deprecated.sphinx import deprecated
7from .meta import MetaBaseSkel
8from ..bones.base import BaseBone, ReadFromClientErrorSeverity
9from ..config import conf
11if t.TYPE_CHECKING: 11 ↛ 12line 11 didn't jump to line 12 because the condition on line 11 was never true
12 from .instance import SkeletonInstance
15class BaseSkeleton(object, metaclass=MetaBaseSkel):
16 """
17 This is a container-object holding information about one database entity.
19 It has to be sub-classed with individual information about the kindName of the entities
20 and its specific data attributes, the so called bones.
21 The Skeleton stores its bones in an :class:`OrderedDict`-Instance, so the definition order of the
22 contained bones remains constant.
24 :ivar key: This bone stores the current database key of this entity. \
25 Assigning to this bones value is dangerous and does *not* affect the actual key its stored in.
27 :vartype key: server.bones.BaseBone
29 :ivar creationdate: The date and time where this entity has been created.
30 :vartype creationdate: server.bones.DateBone
32 :ivar changedate: The date and time of the last change to this entity.
33 :vartype changedate: server.bones.DateBone
34 """
35 __viurBaseSkeletonMarker__ = True
36 boneMap = None
38 @classmethod
39 @deprecated(
40 version="3.7.0",
41 reason="Function renamed. Use subskel function as alternative implementation.",
42 )
43 def subSkel(cls, *subskel_names, fullClone: bool = False, **kwargs) -> "SkeletonInstance":
44 return cls.subskel(*subskel_names, clone=fullClone) # FIXME: REMOVE WITH VIUR4
46 @classmethod
47 def subskel(
48 cls,
49 *names: str,
50 bones: t.Iterable[str] = (),
51 clone: bool = False,
52 ) -> "SkeletonInstance":
53 """
54 Creates a new sub-skeleton from the current skeleton.
56 A sub-skeleton is a copy of the original skeleton, containing only a subset of its bones.
58 Sub-skeletons can either be defined using the the subSkels property of the Skeleton object,
59 or freely by giving patterns for bone names which shall be part of the sub-skeleton.
61 1. Giving names as parameter merges the bones of all Skeleton.subSkels-configurations together.
62 This is the usual behavior. By passing multiple sub-skeleton names to this function, a sub-skeleton
63 with the union of all bones of the specified sub-skeletons is returned. If an entry called "*"
64 exists in the subSkels-dictionary, the bones listed in this entry will always be part of the
65 generated sub-skeleton.
66 2. Given the *bones* parameter allows to freely specify a sub-skeleton; One specialty here is,
67 that the order of the bones can also be changed in this mode. This mode is the new way of defining
68 sub-skeletons, and might become the primary way to define sub-skeletons in future.
69 3. Both modes (1 + 2) can be combined, but then the original order of the bones is kept.
70 4. The "key" bone is automatically available in each sub-skeleton.
71 5. An fnmatch-compatible wildcard pattern is allowed both in the subSkels-bone-list and the
72 free bone list.
74 Example (TodoSkel is the example skeleton from viur-base):
75 ```py
76 # legacy mode (see 1)
77 subskel = TodoSkel.subskel("add")
78 # creates subskel: key, firstname, lastname, subject
80 # free mode (see 2) allows to specify a different order!
81 subskel = TodoSkel.subskel(bones=("subject", "message", "*stname"))
82 # creates subskel: key, subject, message, firstname, lastname
84 # mixed mode (see 3)
85 subskel = TodoSkel.subskel("add", bones=("message", ))
86 # creates subskel: key, firstname, lastname, subject, message
87 ```
89 :param bones: Allows to specify an iterator of bone names (more precisely, fnmatch-wildards) which allow
90 to freely define a subskel. If *only* this parameter is given, the order of the specification also
91 defines, the order of the list. Otherwise, the original order as defined in the skeleton is kept.
92 :param clone: If set True, performs a cloning of the used bone map, to be entirely stand-alone.
94 :return: The sub-skeleton of the specified type.
95 """
96 from_subskel = False
97 bones = list(bones)
99 for name in names:
100 # a str refers to a subskel name from the cls.subSkel dict
101 if isinstance(name, str):
102 # add bones from "*" subskel once
103 if not from_subskel:
104 bones.extend(cls.subSkels.get("*") or ())
105 from_subskel = True
107 bones.extend(cls.subSkels.get(name) or ())
109 else:
110 raise ValueError(f"Invalid subskel definition: {name!r}")
112 if from_subskel:
113 # when from_subskel is True, create bone names based on the order of the bones in the original skeleton
114 bones = tuple(k for k in cls.__boneMap__.keys() if any(fnmatch.fnmatch(k, n) for n in bones))
116 if not bones:
117 raise ValueError("The given subskel definition doesn't contain any bones!")
119 return cls(bones=bones, clone=clone)
121 @classmethod
122 def setSystemInitialized(cls):
123 for attrName in dir(cls):
124 bone = getattr(cls, attrName)
125 if isinstance(bone, BaseBone):
126 bone.setSystemInitialized()
128 @classmethod
129 def setBoneValue(
130 cls,
131 skel: "SkeletonInstance",
132 boneName: str,
133 value: t.Any,
134 append: bool = False,
135 language: t.Optional[str] = None
136 ) -> bool:
137 """
138 Allows for setting a bones value without calling fromClient or assigning a value directly.
139 Sanity-Checks are performed; if the value is invalid, that bone flips back to its original
140 (default) value and false is returned.
142 :param boneName: The name of the bone to be modified
143 :param value: The value that should be assigned. It's type depends on the type of that bone
144 :param append: If True, the given value is appended to the values of that bone instead of
145 replacing it. Only supported on bones with multiple=True
146 :param language: Language to set
148 :return: Wherever that operation succeeded or not.
149 """
150 bone = getattr(skel, boneName, None)
152 if not isinstance(bone, BaseBone):
153 raise ValueError(f"{boneName!r} is no valid bone on this skeleton ({skel!r})")
155 if language:
156 if not bone.languages:
157 raise ValueError("The bone {boneName!r} has no language setting")
158 elif language not in bone.languages:
159 raise ValueError("The language {language!r} is not available for bone {boneName!r}")
161 if value is None:
162 if append:
163 raise ValueError("Cannot append None-value to bone {boneName!r}")
165 if language:
166 skel[boneName][language] = [] if bone.multiple else None
167 else:
168 skel[boneName] = [] if bone.multiple else None
170 return True
172 _ = skel[boneName] # ensure the bone is being unserialized first
173 return bone.setBoneValue(skel, boneName, value, append, language)
175 @classmethod
176 def fromClient(
177 cls,
178 skel: "SkeletonInstance",
179 data: dict[str, list[str] | str],
180 *,
181 amend: bool = False,
182 ignore: t.Optional[t.Iterable[str]] = None,
183 ) -> bool:
184 """
185 Load supplied *data* into Skeleton.
187 This function works similar to :func:`~viur.core.skeleton.Skeleton.setValues`, except that
188 the values retrieved from *data* are checked against the bones and their validity checks.
190 Even if this function returns False, all bones are guaranteed to be in a valid state.
191 The ones which have been read correctly are set to their valid values;
192 Bones with invalid values are set back to a safe default (None in most cases).
193 So its possible to call :func:`~viur.core.skeleton.Skeleton.write` afterwards even if reading
194 data with this function failed (through this might violates the assumed consistency-model).
196 :param skel: The skeleton instance to be filled.
197 :param data: Dictionary from which the data is read.
198 :param amend: Defines whether content of data may be incomplete to amend the skel,
199 which is useful for edit-actions.
200 :param ignore: optional list of bones to be ignored; Defaults to all readonly-bones when set to None.
202 :returns: True if all data was successfully read and complete. \
203 False otherwise (e.g. some required fields where missing or where invalid).
204 """
205 complete = True
206 skel.errors = []
208 for key, bone in skel.items():
209 if (ignore is None and bone.readOnly) or key in (ignore or ()):
210 continue
212 if errors := bone.fromClient(skel, key, data):
213 for error in errors:
214 # insert current bone name into error's fieldPath
215 error.fieldPath.insert(0, str(key))
217 # logging.info(f"{key=} {error=} {skel[key]=} {bone.getEmptyValue()=}")
219 incomplete = (
220 # always when something is invalid
221 error.severity == ReadFromClientErrorSeverity.Invalid
222 or (
223 # only when path is top-level
224 len(error.fieldPath) == 1
225 and (
226 # bone is generally required
227 bool(bone.required)
228 and (
229 # and value is either empty
230 error.severity == ReadFromClientErrorSeverity.Empty
231 # or not set, depending on amending mode
232 or (
233 error.severity == ReadFromClientErrorSeverity.NotSet
234 and (amend and bone.isEmpty(skel[key]))
235 or not amend
236 )
237 )
238 )
239 )
240 )
242 # in case there are language requirements, test additionally
243 if bone.languages and isinstance(bone.required, (list, tuple)):
244 incomplete &= any([key, lang] == error.fieldPath for lang in bone.required)
246 # logging.debug(f"BaseSkel.fromClient {incomplete=} {error.severity=} {bone.required=}")
248 if incomplete:
249 complete = False
251 if conf.debug.skeleton_from_client:
252 logging.error(
253 f"""{getattr(cls, "kindName", cls.__name__)}: {".".join(error.fieldPath)}: """
254 f"""({error.severity}) {error.errorMessage}"""
255 )
256 else:
257 errors.clear()
259 skel.errors += errors
261 return complete
263 @classmethod
264 def refresh(cls, skel: "SkeletonInstance[t.Self]"):
265 """
266 Refresh the bones current content.
268 This function causes a refresh of all relational bones and their associated
269 information.
270 """
271 logging.debug(f"""Refreshing {skel["key"]!r} ({skel.get("name")!r})""")
273 for key, bone in skel.items():
274 if not isinstance(bone, BaseBone):
275 continue
277 _ = skel[key] # Ensure value gets loaded
278 bone.refresh(skel, key)
280 @classmethod
281 def readonly(cls, skel: "SkeletonInstance"):
282 """
283 Set all bones to readonly in the Skeleton.
284 """
285 for bone in skel.values():
286 if not isinstance(bone, BaseBone):
287 continue
288 bone.readOnly = True
290 def __new__(cls, *args, **kwargs) -> "SkeletonInstance":
291 from .instance import SkeletonInstance
292 return SkeletonInstance(cls, *args, **kwargs)