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

150 statements  

« prev     ^ index     » next       coverage.py v7.6.3, created at 2024-10-16 22:16 +0000

1""" 

2ViUR-core 

3Copyright © 2024 Mausbrand Informationssysteme GmbH 

4 

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

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

7""" 

8 

9import os 

10import sys 

11 

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

13if sys.argv[0].endswith("viur-core-migrate-config"): 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 

16import inspect 

17import warnings 

18from types import ModuleType 

19import typing as t 

20from google.appengine.api import wrap_wsgi_app 

21 

22from viur.core import i18n, request, utils 

23from viur.core.config import conf 

24from viur.core.decorators import * 

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

26from viur.core.module import Method, Module 

27from viur.core.module import Module, Method 

28from viur.core.tasks import TaskHandler, runStartupTasks 

29from .i18n import translate 

30from .tasks import (DeleteEntitiesIter, PeriodicTask, QueryIter, StartupTask, 

31 TaskHandler, callDeferred, retry_n_times, runStartupTasks) 

32 

33# noinspection PyUnresolvedReferences 

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

35 

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

37 

38__all__ = [ 

39 # basics from this __init__ 

40 "setDefaultLanguage", 

41 "setDefaultDomainLanguage", 

42 "setup", 

43 # prototypes 

44 "Module", 

45 "Method", 

46 # tasks 

47 "DeleteEntitiesIter", 

48 "QueryIter", 

49 "retry_n_times", 

50 "callDeferred", 

51 "StartupTask", 

52 "PeriodicTask", 

53 # Decorators 

54 "access", 

55 "exposed", 

56 "force_post", 

57 "force_ssl", 

58 "internal_exposed", 

59 "skey", 

60 # others 

61 "conf", 

62 "translate", 

63] 

64 

65# Show DeprecationWarning from the viur-core 

66warnings.filterwarnings("always", category=DeprecationWarning, module=r"viur\.core.*") 

67 

68 

69def setDefaultLanguage(lang: str): 

70 """ 

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

72 

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

74 """ 

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

76 

77 

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

79 """ 

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

81 should use which language. 

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

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

84 """ 

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

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

87 host = host[4:] 

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

89 

90 

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

92 """ 

93 Creates the application-context for the current instance. 

94 

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

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

97 

98 Every class found in *modules* becomes 

99 

100 - instanced 

101 - get the corresponding renderer attached 

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

103 

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

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

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

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

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

109 """ 

110 if not isinstance(renderers, dict): 

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

112 renderers, renderers_root = {}, renderers 

113 for key, module in vars(renderers_root).items(): 

114 if "__" not in key: 

115 renderers[key] = {} 

116 for subkey, render in vars(module).items(): 

117 if "__" not in subkey: 

118 renderers[key][subkey] = render 

119 del renderers_root 

120 

121 # assign ViUR system modules 

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

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

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

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

126 

127 modules._tasks = TaskHandler 

128 modules._moduleconf = ModuleConf 

129 modules._translation = Translation 

130 modules.script = Script 

131 

132 # Resolver defines the URL mapping 

133 resolver = {} 

134 

135 # Index is mapping all module instances for global access 

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

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

138 

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

140 if module_name == "index": 

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

142 

143 if module_name in renderers: 

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

145 

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

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

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

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

150 ): 

151 continue 

152 

153 # remember module_instance for default renderer. 

154 module_instance = default_module_instance = None 

155 

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

157 # Only continue when module_cls is configured for this render 

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

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

160 continue 

161 

162 # Create a new module instance 

163 module_instance = module_cls( 

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

165 ) 

166 

167 # Attach the module-specific or the default render 

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

169 default_module_instance = module_instance 

170 target = resolver 

171 else: 

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

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

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

175 

176 # Attach the module to the given renderer node 

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

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

179 

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

181 

182 # Apply Renderers postProcess Filters 

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

184 render["_postProcessAppObj"](target) 

185 

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

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

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

189 # This behavior does NOT influence the routing. 

190 if default_module_instance or module_instance: 

191 setattr(index, module_name, default_module_instance or module_instance) 

192 

193 # fixme: Below is also ugly... 

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

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

196 elif "html" in renderers: 

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

198 

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

200 if conf.debug.trace: 

201 import pprint 

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

203 

204 conf.main_resolver = resolver 

205 conf.main_app = index 

206 

207 

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

209 """ 

210 Define whats going to be served by this instance. 

211 

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

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

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

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

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

217 """ 

218 from viur.core.bones.base import setSystemInitialized 

219 # noinspection PyUnresolvedReferences 

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

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

222 if conf.instance.project_id not in conf.valid_application_ids: 

223 raise RuntimeError( 

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

225 if not render: 

226 import viur.core.render 

227 render = viur.core.render 

228 

229 __build_app(modules, render, default) 

230 

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

232 if ((conf.debug.trace 

233 or conf.debug.trace_external_call_routing 

234 or conf.debug.trace_internal_call_routing) 

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

236 from viur.core import email 

237 try: 

238 email.sendEMailToAdmins( 

239 "Debug mode enabled", 

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

241 ) 

242 except Exception as exc: # OverQuota, whatever 

243 logging.exception(exc) 

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

245 from viur.core import securityheaders 

246 securityheaders._rebuildCspHeaderCache() 

247 securityheaders._rebuildPermissionHeaderCache() 

248 setSystemInitialized() 

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

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

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

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

253 raise AssertionError("Got unexpected header in " 

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

255 if conf.security.strict_transport_security: 

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

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

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

259 if conf.security.x_permitted_cross_domain_policies not in crossDomainPolicies: 

260 raise AssertionError("conf.security.x_permitted_cross_domain_policies " 

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

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

263 mode, uri = conf.security.x_frame_options 

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

265 if mode == "allow-from": 

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

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

268 i18n.initializeTranslations() 

269 if conf.file_hmac_key is None: 

270 from viur.core import db 

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

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

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

274 obj = db.Entity(key) 

275 

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

277 logging.info("Creating new hmacKey") 

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

279 db.Put(obj) 

280 

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

282 

283 if conf.instance.is_dev_server: 

284 WIDTH = 80 # defines the standard width 

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

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

287 

288 # define lines to show 

289 lines = ( 

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

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

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

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

294 "" # empty line 

295 ) 

296 

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

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

299 

300 # dump to console 

301 for i, line in enumerate(lines): 

302 print( 

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

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

305 }}{FILL}""" 

306 ) 

307 

308 return wrap_wsgi_app(app) 

309 

310 

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

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

313 

314 

315# DEPRECATED ATTRIBUTES HANDLING 

316 

317__DEPRECATED_DECORATORS = { 

318 # stuff prior viur-core < 3.5 

319 "forcePost": ("force_post", force_post), 

320 "forceSSL": ("force_ssl", force_ssl), 

321 "internalExposed": ("internal_exposed", internal_exposed) 

322} 

323 

324 

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

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

327 func = entry[1] 

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

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

330 logging.warning(msg, stacklevel=2) 

331 return func 

332 

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