Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/prototypes/skelmodule.py: 0%

101 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-26 11:31 +0000

1import os 

2import yaml 

3import logging 

4from viur.core import Module, db, current, errors 

5from viur.core.decorators import * 

6from viur.core.config import conf 

7from viur.core.skeleton import skeletonByKind, Skeleton, SkeletonInstance 

8import typing as t 

9 

10 

11SINGLE_ORDER_TYPE = str | tuple[str, db.SortOrder] 

12""" 

13Type for exactly one sort order definitions. 

14""" 

15 

16ORDER_TYPE = SINGLE_ORDER_TYPE | tuple[SINGLE_ORDER_TYPE] | list[SINGLE_ORDER_TYPE] | dict[str, str | int] | None 

17""" 

18Type for sort order definitions (any amount of single order definitions). 

19""" 

20 

21DEFAULT_ORDER_TYPE = ORDER_TYPE | t.Callable[[db.Query], ORDER_TYPE] 

22""" 

23Type for default sort order definitions. 

24""" 

25 

26 

27def __load_indexes_from_file() -> dict[str, list]: 

28 """ 

29 Loads all indexes from the index.yaml and stores it in a dictionary sorted by the module(kind) 

30 :return A dictionary of indexes per module 

31 """ 

32 indexes_dict = {} 

33 try: 

34 with open(os.path.join(conf.instance.project_base_path, "index.yaml"), "r") as file: 

35 indexes = yaml.safe_load(file) 

36 indexes = indexes.get("indexes", []) 

37 for index in indexes or (): 

38 index["properties"] = [_property["name"] for _property in index["properties"]] 

39 indexes_dict.setdefault(index["kind"], []).append(index) 

40 

41 except FileNotFoundError: 

42 logging.warning("index.yaml not found") 

43 return {} 

44 

45 return indexes_dict 

46 

47 

48DATASTORE_INDEXES = __load_indexes_from_file() 

49 

50X_VIUR_BONELIST: t.Final[str] = "X-VIUR-BONELIST" 

51"""Defines the header parameter that might contain a client-defined bone list.""" 

52 

53 

54class SkelModule(Module): 

55 """ 

56 This is the extended module prototype used by any other ViUR module prototype. 

57 It a prototype which generally is bound to some database model abstracted by the ViUR skeleton system. 

58 """ 

59 

60 kindName: str = None 

61 """ 

62 Name of the datastore kind that is handled by this module. 

63 

64 This information is used to bind a specific :class:`viur.core.skeleton.Skeleton`-class to this 

65 prototype. By default, it is automatically determined from the module's class name, so a module named 

66 `Animal` refers to a Skeleton named `AnimalSkel` and its kindName is `animal`. 

67 

68 For more information, refer to the function :func:`~_resolveSkelCls`. 

69 """ 

70 

71 default_order: DEFAULT_ORDER_TYPE = None 

72 """ 

73 Allows to specify a default order for this module, which is applied when no other order is specified. 

74 

75 Setting a default_order might result in the requirement of additional indexes, which are being raised 

76 and must be specified. 

77 """ 

78 

79 def __init__(self, *args, **kwargs): 

80 super().__init__(*args, **kwargs) 

81 

82 # automatically determine kindName when not set 

83 if self.kindName is None: 

84 self.kindName = str(type(self).__name__).lower() 

85 

86 # assign index descriptions from index.yaml 

87 self.indexes = DATASTORE_INDEXES.get(self.kindName, []) 

88 

89 def _resolveSkelCls(self, *args, **kwargs) -> t.Type[Skeleton]: 

90 """ 

91 Retrieve the generally associated :class:`viur.core.skeleton.Skeleton` that is used by 

92 the application. 

93 

94 This is either be defined by the member variable *kindName* or by a Skeleton named like the 

95 application class in lower-case order. 

96 

97 If this behavior is not wanted, it can be definitely overridden by defining module-specific 

98 :func:`~viewSkel`, :func:`~addSkel`, or :func:`~editSkel` functions, or by overriding this 

99 function in general. 

100 

101 :return: Returns a Skeleton class that matches the application. 

102 """ 

103 return skeletonByKind(self.kindName) 

104 

105 def baseSkel(self, *args, **kwargs) -> SkeletonInstance: 

106 """ 

107 Returns an instance of an unmodified base skeleton for this module. 

108 

109 This function should only be used in cases where a full, unmodified skeleton of the module is required, e.g. 

110 for administrative or maintenance purposes. 

111 

112 By default, baseSkel is used by :func:`~viewSkel`, :func:`~addSkel`, and :func:`~editSkel`. 

113 """ 

114 return self.skel(**kwargs) 

115 

116 def skel( 

117 self, 

118 *, 

119 allow_client_defined: bool = False, 

120 bones: tuple[str, ...] | t.List[str] = (), 

121 exclude_bones: tuple[str, ...] | t.List[str] = (), 

122 **kwargs, 

123 ) -> SkeletonInstance: 

124 """ 

125 Retrieve module-specific skeleton, optionally as subskel. 

126 

127 :param allow_client_defined: Evaluates header X-VIUR-BONELIST to contain a comma-separated list of bones. 

128 Using this parameter enforces that the Skeleton class has a subskel named "*" for required bones that 

129 must exist. 

130 :param bones: Allows to specify a list of bones to form a subskel. 

131 :param exclude_bones: Provide a list of bones which are always excluded. 

132 

133 The parameters `bones` and `allow_client_defined` can be combined. 

134 """ 

135 skel_cls = self._resolveSkelCls(**kwargs) 

136 bones = set(bones) if bones else set() 

137 

138 if allow_client_defined: 

139 # if bonelist := current.request.get().kwargs.get(X_VIUR_BONELIST.lower()): # DEBUG 

140 if bonelist := current.request.get().request.headers.get(X_VIUR_BONELIST): 

141 if "*" not in skel_cls.subSkels: # a named star-subskel "*"" must exist! 

142 raise errors.BadRequest(f"Use of {X_VIUR_BONELIST!r} requires a defined star-subskel") 

143 

144 bones |= {bone.strip() for bone in bonelist.split(",")} 

145 else: 

146 allow_client_defined = False # is not client-defined! 

147 

148 bones.difference_update(exclude_bones) 

149 

150 # Return a subskel? 

151 if bones: 

152 # When coming from outside of a request, "*" is always involved. 

153 if allow_client_defined: 

154 current.request.get().response.vary = (X_VIUR_BONELIST, *(current.request.get().response.vary or ())) 

155 return skel_cls.subskel("*", bones=bones) 

156 

157 return skel_cls(bones=bones) 

158 

159 elif exclude_bones: 

160 # Return full skel, without generally excluded bones 

161 bones.update(skel_cls.__boneMap__.keys()) 

162 bones.difference_update(exclude_bones) 

163 return skel_cls(bones=bones) 

164 

165 # Otherwise, return full skeleton 

166 return skel_cls() 

167 

168 def _apply_default_order(self, query: db.Query): 

169 """ 

170 Apply the setting from `default_order` to a given db.Query. 

171 

172 The `default_order` will only be applied when the query has no other order, or is on a multquery. 

173 """ 

174 

175 # Apply default_order when possible! 

176 if ( 

177 self.default_order 

178 and query.queries 

179 and not isinstance(query.queries, list) 

180 and not query.queries.orders 

181 and not current.request.get().kwargs.get("search") 

182 ): 

183 if callable(default_order := self.default_order): 

184 default_order = default_order(query) 

185 

186 if isinstance(default_order, dict): 

187 logging.debug(f"Applying filter {default_order=}") 

188 query.mergeExternalFilter(default_order) 

189 

190 elif default_order: 

191 logging.debug(f"Applying {default_order=}") 

192 

193 # FIXME: This ugly test can be removed when there is type that abstracts SortOrders 

194 if ( 

195 isinstance(default_order, str) 

196 or ( 

197 isinstance(default_order, tuple) 

198 and len(default_order) == 2 

199 and isinstance(default_order[0], str) 

200 and isinstance(default_order[1], db.SortOrder) 

201 ) 

202 ): 

203 query.order(default_order) 

204 else: 

205 query.order(*default_order) 

206 

207 @force_ssl 

208 @force_post 

209 @exposed 

210 @skey 

211 @access("root") 

212 def add_or_edit(self, key: db.Key | int | str, **kwargs) -> t.Any: 

213 """ 

214 This function is intended to be used by importers. 

215 Only "root"-users are allowed to use it. 

216 """ 

217 

218 # Adjust key 

219 db_key = db.keyHelper(key, targetKind=self.kindName, adjust_kind=True) 

220 

221 # Retrieve and verify existing entry 

222 db_entity = db.Get(db_key) 

223 is_add = not bool(db_entity) 

224 

225 # Instanciate relevant skeleton 

226 if is_add: 

227 skel = self.addSkel() 

228 else: 

229 skel = self.editSkel() 

230 skel.dbEntity = db_entity # assign existing entity 

231 

232 skel["key"] = db_key 

233 

234 if ( 

235 not kwargs # no data supplied 

236 or not skel.fromClient(kwargs) # failure on reading into the bones 

237 ): 

238 # render the skeleton in the version it could as far as it could be read. 

239 return self.render.render("add_or_edit", skel) 

240 

241 if is_add: 

242 self.onAdd(skel) 

243 else: 

244 self.onEdit(skel) 

245 

246 skel.write() 

247 

248 if is_add: 

249 self.onAdded(skel) 

250 return self.render.addSuccess(skel) 

251 

252 self.onEdited(skel) 

253 return self.render.editSuccess(skel)