Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/__init__.py: 14%

168 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-12 02:43 +0000

1""" 

2ViUR-core 

3Copyright © 2026 Mausbrand Informationssysteme GmbH 

4 

5https://core.docs.viur.dev 

6Licensed under the MIT license. See LICENSE for more information. 

7""" 

8import fnmatch 

9import os 

10import sys 

11 

12# Set a dummy project id to survive API Client initializations 

13if sys.argv[0].endswith("viur-migrate"): # FIXME: What a "kinda hackish" solution... 13 ↛ 14line 13 didn't jump to line 14 because the condition on line 13 was never true

14 os.environ["GOOGLE_CLOUD_PROJECT"] = "dummy" 

15 

16from google.appengine.api import wrap_wsgi_app 

17from google.appengine.runtime import initialize as gae_initialize 

18from types import ModuleType 

19from viur.core import i18n, request, utils 

20from viur.core.config import conf 

21from viur.core.decorators import access, exposed, force_post, force_ssl, internal_exposed, skey 

22from viur.core.request import before_request, after_request 

23from viur.core.i18n import translate 

24from viur.core.module import Method, Module 

25import inspect 

26import typing as t 

27import warnings 

28from .tasks import ( 

29 callDeferred, 

30 CallDeferred, 

31 DeleteEntitiesIter, 

32 PeriodicTask, 

33 QueryIter, 

34 retry_n_times, 

35 runStartupTasks, 

36 StartupTask, 

37 TaskHandler, 

38) 

39 

40if not sys.argv[0].endswith("viur-migrate"): # FIXME: What a "kinda hackish" solution... 40 ↛ 44line 40 didn't jump to line 44 because the condition on line 40 was always true

41 # noinspection PyUnresolvedReferences 

42 from viur.core import logging as viurLogging # unused import, must exist, initializes request logging 

43 

44import logging # this import has to stay here, see #571 

45from deprecated.sphinx import deprecated 

46 

47__all__ = [ 

48 # basics from this __init__ 

49 "setDefaultLanguage", 

50 "setDefaultDomainLanguage", 

51 "setup", 

52 # prototypes 

53 "Module", 

54 "Method", 

55 # tasks 

56 "DeleteEntitiesIter", 

57 "QueryIter", 

58 "retry_n_times", 

59 "callDeferred", 

60 "CallDeferred", 

61 "StartupTask", 

62 "PeriodicTask", 

63 # Decorators 

64 "access", 

65 "after_request", 

66 "before_request", 

67 "exposed", 

68 "force_post", 

69 "force_ssl", 

70 "internal_exposed", 

71 "skey", 

72 # others 

73 "conf", 

74 "translate", 

75] 

76 

77# Show DeprecationWarning from the viur-core 

78warnings.filterwarnings("once", category=DeprecationWarning) 

79warnings.filterwarnings("ignore", category=DeprecationWarning, module=r"viur\.datastore.*", 

80 message="'clonedBoneMap' was renamed into 'bone_map'") 

81 

82 

83@deprecated( 

84 version="3.8.0", 

85 reason="Simply set `conf.i18n.default_language` to the desired language." 

86) 

87def setDefaultLanguage(lang: str): 

88 """ 

89 Sets the default language used by ViUR to *lang*. 

90 

91 :param lang: Name of the language module to use by default. 

92 """ 

93 msg = f"`setDefaultLanguage(\"{lang}\")` is deprecated; " \ 

94 f"Replace the call by `conf.i18n.default_language = \"{lang.lower()}\"`" 

95 warnings.warn(msg, DeprecationWarning, stacklevel=2) 

96 logging.warning(msg) 

97 

98 conf.i18n.default_language = lang.lower() 

99 

100 

101def setDefaultDomainLanguage(domain: str, lang: str): 

102 """ 

103 If conf.i18n.language_method is set to "domain", this function allows setting the map of which domain 

104 should use which language. 

105 :param domain: The domain for which the language should be set 

106 :param lang: The language to use (in ISO2 format, e.g. "DE") 

107 """ 

108 host = domain.lower().strip(" /") 

109 if host.startswith("www."): 

110 host = host[4:] 

111 conf.i18n.domain_language_mapping[host] = lang.lower() 

112 

113 

114def __build_app(modules: ModuleType | object, renderers: ModuleType | object, default: str = None) -> Module: 

115 """ 

116 Creates the application-context for the current instance. 

117 

118 This function converts the classes found in the *modules*-module, 

119 and the given renders into the object found at ``conf.main_app``. 

120 

121 Every class found in *modules* becomes 

122 

123 - instanced 

124 - get the corresponding renderer attached 

125 - will be attached to ``conf.main_app`` 

126 

127 :param modules: Usually the module provided as *modules* directory within the application. 

128 :param renderers: Usually the module *viur.core.renders*, or a dictionary renderName => renderClass. 

129 :param default: Name of the renderer, which will form the root of the application. 

130 This will be the renderer, which wont get a prefix, usually html. 

131 (=> /user instead of /html/user) 

132 """ 

133 if not isinstance(renderers, dict): 

134 # build up the dict from viur.core.render 

135 renderers, mod = {}, renderers 

136 

137 from viur.core.render.abstract import AbstractRenderer 

138 

139 for render_name, render_mod in vars(mod).items(): 

140 if inspect.ismodule(render_mod): 

141 for render_clsname, render_cls in vars(render_mod).items(): 

142 # this is "kinda hackish..." because ViUR 3's current renderer concept is pure bulls*t... 

143 if render_clsname == "DefaultRender": 

144 continue 

145 

146 if ( 

147 # test for a renderer 

148 (inspect.isclass(render_cls) and issubclass(render_cls, AbstractRenderer)) 

149 # bullsh*t, this must be entirely reworked! 

150 or render_clsname == "_postProcessAppObj" 

151 ): 

152 renderers.setdefault(render_name, {}) 

153 renderers[render_name][render_clsname] = render_cls 

154 

155 # assign ViUR system modules 

156 from viur.core.modules.moduleconf import ModuleConf # noqa: E402 # import works only here because circular imports 

157 from viur.core.modules.script import Script # noqa: E402 # import works only here because circular imports 

158 from viur.core.modules.translation import Translation # noqa: E402 # import works only here because circular imports 

159 from viur.core.prototypes.instanced_module import InstancedModule # noqa: E402 # import works only here because circular imports 

160 

161 for name, cls in { 

162 "_tasks": TaskHandler, 

163 "_moduleconf": ModuleConf, 

164 "_translation": Translation, 

165 "script": Script, 

166 }.items(): 

167 # Check whether name is contained in modules so that it can be overwritten 

168 if name not in vars(modules): 

169 setattr(modules, name, cls) 

170 

171 assert issubclass(getattr(modules, name), cls) 

172 

173 # Resolver defines the URL mapping 

174 resolver = {} 

175 

176 # Index is mapping all module instances for global access 

177 index = (modules.index if hasattr(modules, "index") else Module)("index", "") 

178 index.register(resolver, renderers[default]["default"](parent=index)) 

179 

180 for module_name, module_cls in vars(modules).items(): # iterate over all modules 

181 if module_name == "index": 

182 continue # ignore index, as it has been processed before! 

183 

184 if module_name in renderers: 

185 raise NameError(f"Cannot name module {module_name!r}, as it is a reserved render's name") 

186 

187 if not ( # we define the cases we want to use and then negate them all 

188 (inspect.isclass(module_cls) and issubclass(module_cls, Module) # is a normal Module class 

189 and not issubclass(module_cls, InstancedModule)) # but not a "instantiable" Module 

190 or isinstance(module_cls, InstancedModule) # is an already instanced Module 

191 ): 

192 continue 

193 

194 # remember module_instance for default renderer. 

195 module_instance = default_module_instance = None 

196 

197 for render_name, render in renderers.items(): # look, if a particular renderer should be built 

198 # Only continue when module_cls is configured for this render 

199 # todo: VIUR4 this is for legacy reasons, can be done better! 

200 if not getattr(module_cls, render_name, False): 

201 continue 

202 

203 # Create a new module instance 

204 module_instance = module_cls( 

205 module_name, ("/" + render_name if render_name != default else "") + "/" + module_name 

206 ) 

207 

208 # Attach the module-specific or the default render 

209 if render_name == default: # default or render (sub)namespace? 

210 default_module_instance = module_instance 

211 target = resolver 

212 else: 

213 if getattr(index, render_name, True) is True: 

214 # Render is not build yet, or it is just the simple marker that a given render should be build 

215 setattr(index, render_name, Module(render_name, "/" + render_name)) 

216 

217 # Attach the module to the given renderer node 

218 setattr(getattr(index, render_name), module_name, module_instance) 

219 target = resolver.setdefault(render_name, {}) 

220 

221 module_instance.register(target, render.get(module_name, render["default"])(parent=module_instance)) 

222 

223 # Apply Renderers postProcess Filters 

224 if "_postProcessAppObj" in render: # todo: This is ugly! 

225 render["_postProcessAppObj"](target) 

226 

227 # Ugly solution, but there is no better way to do it in ViUR 3: 

228 # Allow that any module can be accessed by `conf.main_app.<modulename>`, 

229 # either with default render or the last created render. 

230 # This behavior does NOT influence the routing. 

231 if default_module_instance or module_instance: 

232 setattr(index, module_name, default_module_instance or module_instance) 

233 

234 # fixme: Below is also ugly... 

235 if default in renderers and hasattr(renderers[default]["default"], "renderEmail"): 

236 conf.emailRenderer = renderers[default]["default"]().renderEmail 

237 elif "html" in renderers: 

238 conf.emailRenderer = renderers["html"]["default"]().renderEmail 

239 

240 # This might be useful for debugging, please keep it for now. 

241 if conf.debug.trace: 

242 import pprint 

243 logging.debug(pprint.pformat(resolver)) 

244 

245 conf.main_resolver = resolver 

246 conf.main_app = index 

247 

248 

249def setup(modules: ModuleType | object, render: ModuleType | object = None, default: str = "html"): 

250 """ 

251 Define whats going to be served by this instance. 

252 

253 :param modules: Usually the module provided as *modules* directory within the application. 

254 :param render: Usually the module *viur.core.renders*, or a dictionary renderName => renderClass. 

255 :param default: Name of the renderer, which will form the root of the application.\ 

256 This will be the renderer, which wont get a prefix, usually html. \ 

257 (=> /user instead of /html/user) 

258 """ 

259 # The App Engine SDK patches the threading APIs in `wrap_wsgi_app()` and thus relies on it running before 

260 # anything starts a thread. `setup()` breaks that order: it calls `wrap_wsgi_app()` only at its very end, 

261 # after `runStartupTasks()`, whose App Engine API calls may already start threads of the API thread pool. 

262 # The patch reloads `threading` and `concurrent.futures.thread`; threads started before that drop out of 

263 # their registries, are never woken up on shutdown and block the interpreter's exit. 

264 # So patch first; the call in `wrap_wsgi_app()` is then a no-op. 

265 gae_initialize.InitializeThreadingApis() 

266 

267 from viur.core.bones.base import setSystemInitialized 

268 # noinspection PyUnresolvedReferences 

269 import skeletons # This import is not used here but _must_ remain to ensure that the 

270 # application's data models are explicitly imported at some place! 

271 for application_id in conf.valid_application_ids: 

272 if fnmatch.fnmatch(conf.instance.project_id, application_id): 

273 break 

274 else: 

275 raise RuntimeError( 

276 f"""Refusing to start, {conf.instance.project_id=} is not in {conf.valid_application_ids=}""") 

277 if not render: 

278 import viur.core.render 

279 render = viur.core.render 

280 

281 __build_app(modules, render, default) 

282 

283 # Send warning email in case trace is activated in a cloud environment 

284 if ((conf.debug.trace 

285 or conf.debug.trace_external_call_routing 

286 or conf.debug.trace_internal_call_routing) 

287 and (not conf.instance.is_dev_server or conf.debug.dev_server_cloud_logging)): 

288 from viur.core import email 

289 try: 

290 email.send_email_to_admins( 

291 "Debug mode enabled", 

292 "ViUR just started a new Instance with call tracing enabled! This might log sensitive information!" 

293 ) 

294 except Exception as exc: # OverQuota, whatever 

295 logging.exception(exc) 

296 # Ensure that our Content Security Policy Header Cache gets build 

297 from viur.core import securityheaders 

298 securityheaders._rebuildCspHeaderCache() 

299 securityheaders._rebuildPermissionHeaderCache() 

300 setSystemInitialized() 

301 # Assert that all security related headers are in a sane state 

302 if conf.security.content_security_policy and conf.security.content_security_policy["_headerCache"]: 

303 for k in conf.security.content_security_policy["_headerCache"]: 

304 if not k.startswith("Content-Security-Policy"): 

305 raise AssertionError("Got unexpected header in " 

306 "conf.security.content_security_policy['_headerCache']") 

307 if conf.security.strict_transport_security: 

308 if not conf.security.strict_transport_security.startswith("max-age"): 

309 raise AssertionError("Got unexpected header in conf.security.strict_transport_security") 

310 securityheaders._validate_reporting_config() 

311 crossDomainPolicies = {None, "none", "master-only", "by-content-type", "all"} 

312 if conf.security.x_permitted_cross_domain_policies not in crossDomainPolicies: 

313 raise AssertionError("conf.security.x_permitted_cross_domain_policies " 

314 f"must be one of {crossDomainPolicies!r}") 

315 if conf.security.x_frame_options is not None and isinstance(conf.security.x_frame_options, tuple): 

316 mode, uri = conf.security.x_frame_options 

317 assert mode in ["deny", "sameorigin", "allow-from"] 

318 if mode == "allow-from": 

319 assert uri is not None and (uri.lower().startswith("https://") or uri.lower().startswith("http://")) 

320 runStartupTasks() # Add a deferred call to run all queued startup tasks 

321 i18n.initializeTranslations() 

322 if conf.file_hmac_key is None: 

323 from viur.core import db 

324 key = db.Key("viur-conf", "viur-conf") 

325 if not (obj := db.get(key)): # create a new "viur-conf"? 

326 logging.info("Creating new viur-conf") 

327 obj = db.Entity(key) 

328 

329 if "hmacKey" not in obj: # create a new hmacKey 

330 logging.info("Creating new hmacKey") 

331 obj["hmacKey"] = utils.string.random(length=20) 

332 db.put(obj) 

333 

334 conf.file_hmac_key = bytes(obj["hmacKey"], "utf-8") 

335 

336 if conf.instance.is_dev_server: 

337 WIDTH = 80 # defines the standard width 

338 FILL = "#" # define sthe fill char (must be len(1)!) 

339 PYTHON_VERSION = (sys.version_info.major, sys.version_info.minor, sys.version_info.micro) 

340 

341 # extra banner lines for a non-default datastore target 

342 datastore_lines = [] 

343 if conf.db.name: 

344 datastore_lines.append(f"""database = \033[1;33m{conf.db.name}\033[0m""") 

345 if conf.db.namespace: 

346 datastore_lines.append(f"""namespace = \033[1;33m{conf.db.namespace}\033[0m""") 

347 

348 # define lines to show 

349 lines = ( 

350 " LOCAL DEVELOPMENT SERVER IS UP AND RUNNING ", # title line 

351 f"""project = \033[1;31m{conf.instance.project_id}\033[0m""", 

352 f"""python = \033[1;32m{".".join((str(i) for i in PYTHON_VERSION))}\033[0m""", 

353 f"""viur = \033[1;32m{".".join((str(i) for i in conf.version))}\033[0m""", 

354 *datastore_lines, # only when a non-default db/namespace is set 

355 "" # empty line 

356 ) 

357 

358 # first and last line are shown with a cool line made of FILL 

359 first_last = (0, len(lines) - 1) 

360 

361 # dump to console 

362 for i, line in enumerate(lines): 

363 print( 

364 f"""\033[0m{FILL}{line:{ 

365 FILL if i in first_last else " "}^{(WIDTH - 2) + (11 if i not in first_last else 0) 

366 }}{FILL}""" 

367 ) 

368 

369 return wrap_wsgi_app(app) 

370 

371 

372def app(environ: dict, start_response: t.Callable): 

373 return request.Router(environ).response(environ, start_response) 

374 

375 

376# DEPRECATED ATTRIBUTES HANDLING 

377 

378__DEPRECATED_DECORATORS = { 

379 # stuff prior viur-core < 3.5 

380 "forcePost": ("force_post", force_post), 

381 "forceSSL": ("force_ssl", force_ssl), 

382 "internalExposed": ("internal_exposed", internal_exposed) 

383} 

384 

385 

386def __getattr__(attr: str) -> object: 

387 if entry := __DEPRECATED_DECORATORS.get(attr): 387 ↛ 388line 387 didn't jump to line 388 because the condition on line 387 was never true

388 func = entry[1] 

389 msg = f"@{attr} was replaced by @{entry[0]}" 

390 warnings.warn(msg, DeprecationWarning, stacklevel=2) 

391 logging.warning(msg, stacklevel=2) 

392 return func 

393 

394 return super(__import__(__name__).__class__).__getattr__(attr)