Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/__init__.py: 14%
151 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-03 12:27 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-03 12:27 +0000
1"""
2ViUR-core
3Copyright © 2025 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-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"
16from google.appengine.api import wrap_wsgi_app
17from types import ModuleType
18from viur.core import i18n, request, utils
19from viur.core.config import conf
20from viur.core.decorators import access, exposed, force_post, force_ssl, internal_exposed, skey
21from viur.core.i18n import translate
22from viur.core.module import Method, Module
23import inspect
24import typing as t
25import warnings
26from .tasks import (
27 callDeferred,
28 CallDeferred,
29 DeleteEntitiesIter,
30 PeriodicTask,
31 QueryIter,
32 retry_n_times,
33 runStartupTasks,
34 StartupTask,
35 TaskHandler,
36)
38if not sys.argv[0].endswith("viur-migrate"): # FIXME: What a "kinda hackish" solution... 38 ↛ 42line 38 didn't jump to line 42 because the condition on line 38 was always true
39 # noinspection PyUnresolvedReferences
40 from viur.core import logging as viurLogging # unused import, must exist, initializes request logging
42import logging # this import has to stay here, see #571
44__all__ = [
45 # basics from this __init__
46 "setDefaultLanguage",
47 "setDefaultDomainLanguage",
48 "setup",
49 # prototypes
50 "Module",
51 "Method",
52 # tasks
53 "DeleteEntitiesIter",
54 "QueryIter",
55 "retry_n_times",
56 "callDeferred",
57 "CallDeferred",
58 "StartupTask",
59 "PeriodicTask",
60 # Decorators
61 "access",
62 "exposed",
63 "force_post",
64 "force_ssl",
65 "internal_exposed",
66 "skey",
67 # others
68 "conf",
69 "translate",
70]
72# Show DeprecationWarning from the viur-core
73warnings.filterwarnings("once", category=DeprecationWarning)
74warnings.filterwarnings("ignore", category=DeprecationWarning, module=r"viur\.datastore.*",
75 message="'clonedBoneMap' was renamed into 'bone_map'")
77def setDefaultLanguage(lang: str):
78 """
79 Sets the default language used by ViUR to *lang*.
81 :param lang: Name of the language module to use by default.
82 """
83 conf.i18n.default_language = lang.lower()
86def setDefaultDomainLanguage(domain: str, lang: str):
87 """
88 If conf.i18n.language_method is set to "domain", this function allows setting the map of which domain
89 should use which language.
90 :param domain: The domain for which the language should be set
91 :param lang: The language to use (in ISO2 format, e.g. "DE")
92 """
93 host = domain.lower().strip(" /")
94 if host.startswith("www."):
95 host = host[4:]
96 conf.i18n.domain_language_mapping[host] = lang.lower()
99def __build_app(modules: ModuleType | object, renderers: ModuleType | object, default: str = None) -> Module:
100 """
101 Creates the application-context for the current instance.
103 This function converts the classes found in the *modules*-module,
104 and the given renders into the object found at ``conf.main_app``.
106 Every class found in *modules* becomes
108 - instanced
109 - get the corresponding renderer attached
110 - will be attached to ``conf.main_app``
112 :param modules: Usually the module provided as *modules* directory within the application.
113 :param renderers: Usually the module *viur.core.renders*, or a dictionary renderName => renderClass.
114 :param default: Name of the renderer, which will form the root of the application.
115 This will be the renderer, which wont get a prefix, usually html.
116 (=> /user instead of /html/user)
117 """
118 if not isinstance(renderers, dict):
119 # build up the dict from viur.core.render
120 renderers, mod = {}, renderers
122 from viur.core.render.abstract import AbstractRenderer
124 for render_name, render_mod in vars(mod).items():
125 if inspect.ismodule(render_mod):
126 for render_clsname, render_cls in vars(render_mod).items():
127 # this is "kinda hackish..." because ViUR 3's current renderer concept is pure bulls*t...
128 if render_clsname == "DefaultRender":
129 continue
131 if (
132 # test for a renderer
133 (inspect.isclass(render_cls) and issubclass(render_cls, AbstractRenderer))
134 # bullsh*t, this must be entirely reworked!
135 or render_clsname == "_postProcessAppObj"
136 ):
137 renderers.setdefault(render_name, {})
138 renderers[render_name][render_clsname] = render_cls
140 # assign ViUR system modules
141 from viur.core.modules.moduleconf import ModuleConf # noqa: E402 # import works only here because circular imports
142 from viur.core.modules.script import Script # noqa: E402 # import works only here because circular imports
143 from viur.core.modules.translation import Translation # noqa: E402 # import works only here because circular imports
144 from viur.core.prototypes.instanced_module import InstancedModule # noqa: E402 # import works only here because circular imports
146 for name, cls in {
147 "_tasks": TaskHandler,
148 "_moduleconf": ModuleConf,
149 "_translation": Translation,
150 "script": Script,
151 }.items():
152 # Check whether name is contained in modules so that it can be overwritten
153 if name not in vars(modules):
154 setattr(modules, name, cls)
156 assert issubclass(getattr(modules, name), cls)
158 # Resolver defines the URL mapping
159 resolver = {}
161 # Index is mapping all module instances for global access
162 index = (modules.index if hasattr(modules, "index") else Module)("index", "")
163 index.register(resolver, renderers[default]["default"](parent=index))
165 for module_name, module_cls in vars(modules).items(): # iterate over all modules
166 if module_name == "index":
167 continue # ignore index, as it has been processed before!
169 if module_name in renderers:
170 raise NameError(f"Cannot name module {module_name!r}, as it is a reserved render's name")
172 if not ( # we define the cases we want to use and then negate them all
173 (inspect.isclass(module_cls) and issubclass(module_cls, Module) # is a normal Module class
174 and not issubclass(module_cls, InstancedModule)) # but not a "instantiable" Module
175 or isinstance(module_cls, InstancedModule) # is an already instanced Module
176 ):
177 continue
179 # remember module_instance for default renderer.
180 module_instance = default_module_instance = None
182 for render_name, render in renderers.items(): # look, if a particular renderer should be built
183 # Only continue when module_cls is configured for this render
184 # todo: VIUR4 this is for legacy reasons, can be done better!
185 if not getattr(module_cls, render_name, False):
186 continue
188 # Create a new module instance
189 module_instance = module_cls(
190 module_name, ("/" + render_name if render_name != default else "") + "/" + module_name
191 )
193 # Attach the module-specific or the default render
194 if render_name == default: # default or render (sub)namespace?
195 default_module_instance = module_instance
196 target = resolver
197 else:
198 if getattr(index, render_name, True) is True:
199 # Render is not build yet, or it is just the simple marker that a given render should be build
200 setattr(index, render_name, Module(render_name, "/" + render_name))
202 # Attach the module to the given renderer node
203 setattr(getattr(index, render_name), module_name, module_instance)
204 target = resolver.setdefault(render_name, {})
206 module_instance.register(target, render.get(module_name, render["default"])(parent=module_instance))
208 # Apply Renderers postProcess Filters
209 if "_postProcessAppObj" in render: # todo: This is ugly!
210 render["_postProcessAppObj"](target)
212 # Ugly solution, but there is no better way to do it in ViUR 3:
213 # Allow that any module can be accessed by `conf.main_app.<modulename>`,
214 # either with default render or the last created render.
215 # This behavior does NOT influence the routing.
216 if default_module_instance or module_instance:
217 setattr(index, module_name, default_module_instance or module_instance)
219 # fixme: Below is also ugly...
220 if default in renderers and hasattr(renderers[default]["default"], "renderEmail"):
221 conf.emailRenderer = renderers[default]["default"]().renderEmail
222 elif "html" in renderers:
223 conf.emailRenderer = renderers["html"]["default"]().renderEmail
225 # This might be useful for debugging, please keep it for now.
226 if conf.debug.trace:
227 import pprint
228 logging.debug(pprint.pformat(resolver))
230 conf.main_resolver = resolver
231 conf.main_app = index
234def setup(modules: ModuleType | object, render: ModuleType | object = None, default: str = "html"):
235 """
236 Define whats going to be served by this instance.
238 :param modules: Usually the module provided as *modules* directory within the application.
239 :param render: Usually the module *viur.core.renders*, or a dictionary renderName => renderClass.
240 :param default: Name of the renderer, which will form the root of the application.\
241 This will be the renderer, which wont get a prefix, usually html. \
242 (=> /user instead of /html/user)
243 """
244 from viur.core.bones.base import setSystemInitialized
245 # noinspection PyUnresolvedReferences
246 import skeletons # This import is not used here but _must_ remain to ensure that the
247 # application's data models are explicitly imported at some place!
248 if conf.instance.project_id not in conf.valid_application_ids:
249 raise RuntimeError(
250 f"""Refusing to start, {conf.instance.project_id=} is not in {conf.valid_application_ids=}""")
251 if not render:
252 import viur.core.render
253 render = viur.core.render
255 __build_app(modules, render, default)
257 # Send warning email in case trace is activated in a cloud environment
258 if ((conf.debug.trace
259 or conf.debug.trace_external_call_routing
260 or conf.debug.trace_internal_call_routing)
261 and (not conf.instance.is_dev_server or conf.debug.dev_server_cloud_logging)):
262 from viur.core import email
263 try:
264 email.send_email_to_admins(
265 "Debug mode enabled",
266 "ViUR just started a new Instance with call tracing enabled! This might log sensitive information!"
267 )
268 except Exception as exc: # OverQuota, whatever
269 logging.exception(exc)
270 # Ensure that our Content Security Policy Header Cache gets build
271 from viur.core import securityheaders
272 securityheaders._rebuildCspHeaderCache()
273 securityheaders._rebuildPermissionHeaderCache()
274 setSystemInitialized()
275 # Assert that all security related headers are in a sane state
276 if conf.security.content_security_policy and conf.security.content_security_policy["_headerCache"]:
277 for k in conf.security.content_security_policy["_headerCache"]:
278 if not k.startswith("Content-Security-Policy"):
279 raise AssertionError("Got unexpected header in "
280 "conf.security.content_security_policy['_headerCache']")
281 if conf.security.strict_transport_security:
282 if not conf.security.strict_transport_security.startswith("max-age"):
283 raise AssertionError("Got unexpected header in conf.security.strict_transport_security")
284 crossDomainPolicies = {None, "none", "master-only", "by-content-type", "all"}
285 if conf.security.x_permitted_cross_domain_policies not in crossDomainPolicies:
286 raise AssertionError("conf.security.x_permitted_cross_domain_policies "
287 f"must be one of {crossDomainPolicies!r}")
288 if conf.security.x_frame_options is not None and isinstance(conf.security.x_frame_options, tuple):
289 mode, uri = conf.security.x_frame_options
290 assert mode in ["deny", "sameorigin", "allow-from"]
291 if mode == "allow-from":
292 assert uri is not None and (uri.lower().startswith("https://") or uri.lower().startswith("http://"))
293 runStartupTasks() # Add a deferred call to run all queued startup tasks
294 i18n.initializeTranslations()
295 if conf.file_hmac_key is None:
296 from viur.core import db
297 key = db.Key("viur-conf", "viur-conf")
298 if not (obj := db.Get(key)): # create a new "viur-conf"?
299 logging.info("Creating new viur-conf")
300 obj = db.Entity(key)
302 if "hmacKey" not in obj: # create a new hmacKey
303 logging.info("Creating new hmacKey")
304 obj["hmacKey"] = utils.string.random(length=20)
305 db.Put(obj)
307 conf.file_hmac_key = bytes(obj["hmacKey"], "utf-8")
309 if conf.instance.is_dev_server:
310 WIDTH = 80 # defines the standard width
311 FILL = "#" # define sthe fill char (must be len(1)!)
312 PYTHON_VERSION = (sys.version_info.major, sys.version_info.minor, sys.version_info.micro)
314 # define lines to show
315 lines = (
316 " LOCAL DEVELOPMENT SERVER IS UP AND RUNNING ", # title line
317 f"""project = \033[1;31m{conf.instance.project_id}\033[0m""",
318 f"""python = \033[1;32m{".".join((str(i) for i in PYTHON_VERSION))}\033[0m""",
319 f"""viur = \033[1;32m{".".join((str(i) for i in conf.version))}\033[0m""",
320 "" # empty line
321 )
323 # first and last line are shown with a cool line made of FILL
324 first_last = (0, len(lines) - 1)
326 # dump to console
327 for i, line in enumerate(lines):
328 print(
329 f"""\033[0m{FILL}{line:{
330 FILL if i in first_last else " "}^{(WIDTH - 2) + (11 if i not in first_last else 0)
331 }}{FILL}"""
332 )
334 return wrap_wsgi_app(app)
337def app(environ: dict, start_response: t.Callable):
338 return request.Router(environ).response(environ, start_response)
341# DEPRECATED ATTRIBUTES HANDLING
343__DEPRECATED_DECORATORS = {
344 # stuff prior viur-core < 3.5
345 "forcePost": ("force_post", force_post),
346 "forceSSL": ("force_ssl", force_ssl),
347 "internalExposed": ("internal_exposed", internal_exposed)
348}
351def __getattr__(attr: str) -> object:
352 if entry := __DEPRECATED_DECORATORS.get(attr): 352 ↛ 353line 352 didn't jump to line 353 because the condition on line 352 was never true
353 func = entry[1]
354 msg = f"@{attr} was replaced by @{entry[0]}"
355 warnings.warn(msg, DeprecationWarning, stacklevel=2)
356 logging.warning(msg, stacklevel=2)
357 return func
359 return super(__import__(__name__).__class__).__getattr__(attr)