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
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-16 22:16 +0000
1"""
2ViUR-core
3Copyright © 2024 Mausbrand Informationssysteme GmbH
5https://core.docs.viur.dev
6Licensed under the MIT license. See LICENSE for more information.
7"""
9import os
10import sys
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"
16import inspect
17import warnings
18from types import ModuleType
19import typing as t
20from google.appengine.api import wrap_wsgi_app
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)
33# noinspection PyUnresolvedReferences
34from viur.core import logging as viurLogging # unused import, must exist, initializes request logging
36import logging # this import has to stay here, see #571
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]
65# Show DeprecationWarning from the viur-core
66warnings.filterwarnings("always", category=DeprecationWarning, module=r"viur\.core.*")
69def setDefaultLanguage(lang: str):
70 """
71 Sets the default language used by ViUR to *lang*.
73 :param lang: Name of the language module to use by default.
74 """
75 conf.i18n.default_language = lang.lower()
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()
91def __build_app(modules: ModuleType | object, renderers: ModuleType | object, default: str = None) -> Module:
92 """
93 Creates the application-context for the current instance.
95 This function converts the classes found in the *modules*-module,
96 and the given renders into the object found at ``conf.main_app``.
98 Every class found in *modules* becomes
100 - instanced
101 - get the corresponding renderer attached
102 - will be attached to ``conf.main_app``
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
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
127 modules._tasks = TaskHandler
128 modules._moduleconf = ModuleConf
129 modules._translation = Translation
130 modules.script = Script
132 # Resolver defines the URL mapping
133 resolver = {}
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))
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!
143 if module_name in renderers:
144 raise NameError(f"Cannot name module {module_name!r}, as it is a reserved render's name")
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
153 # remember module_instance for default renderer.
154 module_instance = default_module_instance = None
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
162 # Create a new module instance
163 module_instance = module_cls(
164 module_name, ("/" + render_name if render_name != default else "") + "/" + module_name
165 )
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))
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, {})
180 module_instance.register(target, render.get(module_name, render["default"])(parent=module_instance))
182 # Apply Renderers postProcess Filters
183 if "_postProcessAppObj" in render: # todo: This is ugly!
184 render["_postProcessAppObj"](target)
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)
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
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))
204 conf.main_resolver = resolver
205 conf.main_app = index
208def setup(modules: ModuleType | object, render: ModuleType | object = None, default: str = "html"):
209 """
210 Define whats going to be served by this instance.
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
229 __build_app(modules, render, default)
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)
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)
281 conf.file_hmac_key = bytes(obj["hmacKey"], "utf-8")
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)
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 )
297 # first and last line are shown with a cool line made of FILL
298 first_last = (0, len(lines) - 1)
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 )
308 return wrap_wsgi_app(app)
311def app(environ: dict, start_response: t.Callable):
312 return request.Router(environ).response(environ, start_response)
315# DEPRECATED ATTRIBUTES HANDLING
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}
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
333 return super(__import__(__name__).__class__).__getattr__(attr)