• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hivesolutions / flask-quorum / 1493

pending completion
1493

push

travis-ci-com

joamag
fix: initial fix fo removal of the app stack
The strategy revolves arround the direct usage of the `_app_ctx` push and pop method.
The strategy takes into consideration retro compatibility.

16 of 16 new or added lines in 1 file covered. (100.0%)

6002 of 8464 relevant lines covered (70.91%)

5.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.42
/src/quorum/base.py
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
3

4
# Hive Flask Quorum
5
# Copyright (c) 2008-2022 Hive Solutions Lda.
6
#
7
# This file is part of Hive Flask Quorum.
8
#
9
# Hive Flask Quorum is free software: you can redistribute it and/or modify
10
# it under the terms of the Apache License as published by the Apache
11
# Foundation, either version 2.0 of the License, or (at your option) any
12
# later version.
13
#
14
# Hive Flask Quorum is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
# Apache License for more details.
18
#
19
# You should have received a copy of the Apache License along with
20
# Hive Flask Quorum. If not, see <http://www.apache.org/licenses/>.
21

22
__author__ = "João Magalhães <joamag@hive.pt>"
8✔
23
""" The author(s) of the module """
24

25
__version__ = "1.0.0"
8✔
26
""" The version of the module """
27

28
__revision__ = "$LastChangedRevision$"
8✔
29
""" The revision number of the module """
30

31
__date__ = "$LastChangedDate$"
8✔
32
""" The last change date of the module """
33

34
__copyright__ = "Copyright (c) 2008-2022 Hive Solutions Lda."
8✔
35
""" The copyright for the module """
36

37
__license__ = "Apache License, Version 2.0"
8✔
38
""" The license for the module """
39

40
import os
8✔
41
import sys
8✔
42
import json
8✔
43
import time
8✔
44
import flask
8✔
45
import atexit
8✔
46
import socket
8✔
47
import logging
8✔
48
import inspect
8✔
49
import datetime
8✔
50
import functools
8✔
51

52
import werkzeug.debug
8✔
53

54
from . import acl
8✔
55
from . import log
8✔
56
from . import amqp
8✔
57
from . import util
8✔
58
from . import data
8✔
59
from . import mail
8✔
60
from . import info
8✔
61
from . import route
8✔
62
from . import model
8✔
63
from . import legacy
8✔
64
from . import amazon
8✔
65
from . import extras
8✔
66
from . import config
8✔
67
from . import session
8✔
68
from . import redisdb
8✔
69
from . import mongodb
8✔
70
from . import pusherc
8✔
71
from . import request
8✔
72
from . import template
8✔
73
from . import execution
8✔
74
from . import exceptions
8✔
75

76
APP = None
8✔
77
""" The reference to the top level application
78
that is being handled by quorum """
79

80
RUN_CALLED = False
8✔
81
""" Flag that controls if the on run methods have already
82
been called for the current execution environment """
83

84
RUN_F = {}
8✔
85
""" The map that will contain the various functions that
86
will be called upon the start of the main run loop """
87

88
ALLOW_ORIGIN = "*"
8✔
89
""" The default value to be used in the "Access-Control-Allow-Origin"
90
header value, this should not be too restrictive """
91

92
ALLOW_HEADERS = "*, X-Requested-With"
8✔
93
""" The default value to be used in the "Access-Control-Allow-Headers"
94
header value, this should not be too restrictive """
95

96
ALLOW_METHODS = "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS"
8✔
97
""" The default value to be used in the "Access-Control-Allow-Methods"
98
header value, this should not be too restrictive """
99

100
CONTENT_SECURITY = "default-src * ws://* wss://* data: blob:; script-src * 'unsafe-inline' 'unsafe-eval'; style-src * 'unsafe-inline';"
8✔
101
""" The default value to be used in the "Content-Security-Policy"
102
header value, this should not be too restrictive """
103

104
FRAME_OPTIONS = "SAMEORIGIN"
8✔
105
""" The value to be as the default/original for the "X-Frame-Options"
106
header, this should ensure that the same origin is always used when
107
trying to embed a dynamic content into a web page """
108

109
XSS_PROTECTION = "1; mode=block"
8✔
110
""" Value to be used as the original one for the "X-XSS-Protection"
111
header value, should provide a way of preventing XSS attach under the
112
internet explorer browser """
113

114
CONTENT_OPTIONS = "nosniff"
8✔
115
""" Default "X-Content-Type-Options" header value to be used to prevent
116
the sniffing of content type values, ensuring that the browser sticks to
117
value of content type provided by the server """
118

119
ESCAPE_EXTENSIONS = (
8✔
120
    ".xml",
121
    ".html",
122
    ".xhtml",
123
    ".xml.tpl",
124
    ".html.tpl",
125
    ".xhtml.tpl"
126
)
127
""" The sequence containing the various extensions
128
for which the autoescape mode will be enabled  by
129
default as expected by the end developer """
130

131
PLATFORM = "%s %d.%d.%d.%s %s" % (
8✔
132
    sys.subversion[0] if hasattr(sys, "subversion") else "CPython",
133
    sys.version_info[0],
134
    sys.version_info[1],
135
    sys.version_info[2],
136
    sys.version_info[3],
137
    sys.platform
138
)
139
""" Extra system information containing some of the details
140
of the technical platform that is running the system, this
141
string should be exposed carefully to avoid extra information
142
from being exposed to outside agents """
143

144
class Quorum(flask.Flask):
8✔
145
    """
146
    The top level application class that inherits from the
147
    flask based one. Should be responsible for the implementation
148
    of some of the modifications to the original application
149
    object infra-structure.
150
    """
151

152
    def select_jinja_autoescape(self, filename):
8✔
153
        if filename == None: return False
×
154
        if filename.endswith(ESCAPE_EXTENSIONS): return True
×
155
        return flask.Flask.select_jinja_autoescape(self, filename)
×
156

157
def monkey():
8✔
158
    """
159
    Runs the dirty job of monkey patching the flask module
160
    so that some of its functions get redirected to new
161
    quorum functions that add extra behavior.
162

163
    This is an internal function and should be changed only
164
    when handling the internals of the quorum framework.
165
    """
166

167
    flask._render_template = flask.render_template
8✔
168
    flask.render_template = template.render_template
8✔
169
    json._default_encoder = util.JSONEncoder()
8✔
170

171
def call_run():
8✔
172
    global RUN_CALLED
173
    if RUN_CALLED: return
8✔
174
    for _fname, f in RUN_F.items(): f()
8✔
175
    RUN_CALLED = True
8✔
176

177
def run(server = None, fallback = "base"):
8✔
178
    if not APP: raise exceptions.BaseError("Application not set or runnable")
×
179

180
    APP.logger.info(
×
181
        "Booting %s %s (flask %s) (%s) ..." % (
182
            info.NAME,
183
            info.VERSION,
184
            flask.__version__,
185
            PLATFORM
186
        )
187
    )
188

189
    server = config.conf("SERVER", server) or "base"
×
190
    runner = globals().get("run_" + server, None)
×
191
    runner_f = globals().get("run_" + fallback, None)
×
192
    if not runner: raise exceptions.BaseError("Server '%s' not found" % server)
×
193

194
    call_run()
×
195

196
    try: runner()
×
197
    except exceptions.ServerInitError as error:
×
198
        APP.logger.warning(
×
199
            "Server '%s' failed to start (%s) falling back to '%s'" % (
200
                server, legacy.UNICODE(error), fallback
201
            )
202
        )
203
        runner_f and runner_f()
×
204

205
def prepare_app():
8✔
206
    """
207
    Prepares the global application object encapsulating
208
    it with the proper decorators so that it is enabled
209
    with the proper features (eg: debugging capabilities).
210

211
    This method should be called and the returned application
212
    object used instead of the global one.
213

214
    :rtype: Application
215
    :return: The decorated application object with the proper\
216
    capabilities enabled, this object should be used for the\
217
    serving operations instead of the global one.
218
    """
219

220
    app = APP
×
221
    if app.debug: app = werkzeug.debug.DebuggedApplication(app, True)
×
222
    return app
×
223

224
def run_base():
8✔
225
    debug = config.conf("DEBUG", False, cast = bool)
×
226
    reloader = config.conf("RELOADER", False, cast = bool)
×
227
    host = config.conf("HOST", "127.0.0.1")
×
228
    port = int(config.conf("PORT", 5000))
×
229
    APP.run(
×
230
        use_debugger = debug,
231
        debug = debug,
232
        use_reloader = reloader,
233
        host = host,
234
        port = port
235
    )
236

237
def run_netius():
8✔
238
    try: import netius.servers
×
239
    except Exception as exception:
×
240
        raise exceptions.ServerInitError(
×
241
            legacy.UNICODE(exception),
242
            server = "netius"
243
        )
244

245
    kwargs = dict()
×
246
    host = config.conf("HOST", "127.0.0.1")
×
247
    port = int(config.conf("PORT", 5000))
×
248
    ipv6 = config.conf("IPV6", False, cast = bool)
×
249
    ssl = int(config.conf("SSL", 0)) and True or False
×
250
    key_file = config.conf("KEY_FILE", None)
×
251
    cer_file = config.conf("CER_FILE", None)
×
252
    servers = config.conf_prefix("SERVER_")
×
253
    for name, value in servers.items():
×
254
        name_s = name.lower()[7:]
×
255
        kwargs[name_s] = value
×
256
    kwargs["handlers"] = get_handlers()
×
257
    kwargs["level"] = get_level()
×
258

259
    # prepares the application object so that it becomes ready
260
    # to be executed by the server in the proper way
261
    app = prepare_app()
×
262

263
    # creates the netius wsgi server reference using the provided
264
    # application object and the constructed keyword arguments
265
    # and then call the serve method starting the event loop with
266
    # the proper network configuration
267
    server = netius.servers.WSGIServer(app, **kwargs)
×
268
    server.serve(
×
269
        host = host,
270
        port = port,
271
        ipv6 = ipv6,
272
        ssl = ssl,
273
        key_file = key_file,
274
        cer_file = cer_file
275
    )
276

277
def run_waitress():
8✔
278
    try: import waitress
×
279
    except Exception as exception:
×
280
        raise exceptions.ServerInitError(
×
281
            legacy.UNICODE(exception),
282
            server = "waitress"
283
        )
284

285
    host = config.conf("HOST", "127.0.0.1")
×
286
    port = int(config.conf("PORT", 5000))
×
287

288
    # prepares the application object so that it becomes ready
289
    # to be executed by the server in the proper way
290
    app = prepare_app()
×
291

292
    # starts the serving process for the waitress server with
293
    # the proper network configuration values, note that no ssl
294
    # support is currently available for waitress
295
    waitress.serve(
×
296
        app,
297
        host = host,
298
        port = port
299
    )
300

301
def load(
8✔
302
    app = None,
303
    name = None,
304
    locales = ("en_us",),
305
    secret_key = None,
306
    execution = True,
307
    redis_session = False,
308
    mongo_database = None,
309
    logger = None,
310
    models = None,
311
    safe = False,
312
    **kwargs
313
):
314
    """
315
    Initial loader function responsible for the overriding of
316
    the flask loading system and for the loading of configuration.
317

318
    .. note::
319

320
        This function should be called inside you main app file failure
321
        to do so may result in unexpected behavior.
322

323
    :type app: Application
324
    :param app: The optional flask application object to be used\
325
    in the loading of quorum (useful for self managed apps).
326
    :type name: String
327
    :param name: The name to be used to describe the application\
328
    for the management of internal values.
329
    :type locales: List
330
    :param locales: The list containing the various locale strings for\
331
    the locales available for the application to be loaded.
332
    :type secret_key: String
333
    :param secret_key: The secret seed value to be used for cryptographic\
334
    operations under flask (eg: client side sessions) this value should\
335
    be shared among all the pre-fork instances.
336
    :type execution: bool
337
    :param execution: Flag indicating if the (background) execution thread\
338
    should be started providing the required support for background tasks.
339
    :type redis_session: bool
340
    :param redis_session: If the session management for the flask infra-structure\
341
    should be managed using a server side session with support from redis.
342
    :type mongo_database: String
343
    :param mongo_database: The default name of the database to be used when\
344
    using the MongoDB infra-structure.
345
    :type logger: String
346
    :param logger: The name to be used as identification for possible logger\
347
    files created by the logging sub-module.
348
    :type models: Module
349
    :param models: The module containing the complete set of model classes to\
350
    be used by the data infra-structure (eg: ``mongo``).
351
    :type safe: bool
352
    :param safe: If the application should be run in a safe mode meaning that\
353
    extra validations will be done to ensure proper execution, typically these\
354
    kind of validations have a performance impacts (not recommended).
355
    :rtype: Application
356
    :return: The application that is used by the loaded quorum environment in\
357
    case one was provided that is retrieved, otherwise the newly created one is\
358
    returned instead.
359
    """
360

361
    global APP
362
    if APP: return APP
8✔
363

364
    # runs the initial loading of the configuration from all
365
    # the currently available sources (eg: file, environment, etc.)
366
    load_all()
8✔
367

368
    # retrieves the value of all the configuration considered
369
    # to be base and that are going to be used in the loading
370
    # of the current application (with default values)
371
    debug = config.conf("DEBUG", False, cast = bool)
8✔
372
    reloader = config.conf("RELOADER", False, cast = bool)
8✔
373
    level_s = config.conf("LEVEL", "WARNING")
8✔
374
    adapter_s = config.conf("ADAPTER", "mongo")
8✔
375
    name = config.conf("NAME", name)
8✔
376
    instance = config.conf("INSTANCE", None)
8✔
377
    force_ssl = config.conf("FORCE_SSL", False)
8✔
378
    redis_url = config.conf("REDISTOGO_URL", None)
8✔
379
    mongo_url = config.conf("MONGOHQ_URL", None)
8✔
380
    amqp_url = config.conf("AMQP_URL", None)
8✔
381
    amazon_id = config.conf("AMAZON_ID", None)
8✔
382
    amazon_secret = config.conf("AMAZON_SECRET", None)
8✔
383
    amazon_bucket = config.conf("AMAZON_BUCKET", None)
8✔
384
    pusher_app_id = config.conf("PUSHER_APP_ID", None)
8✔
385
    pusher_key = config.conf("PUSHER_KEY", None)
8✔
386
    pusher_secret = config.conf("PUSHER_SECRET", None)
8✔
387
    pusher_cluster = config.conf("PUSHER_CLUSTER", None)
8✔
388
    smtp_host = config.conf("SMTP_HOST", None)
8✔
389
    smtp_user = config.conf("SMTP_USER", None)
8✔
390
    smtp_password = config.conf("SMTP_PASSWORD", None)
8✔
391
    redis_url = config.conf("REDIS_URL", redis_url)
8✔
392
    mongo_url = config.conf("MONGOLAB_URI", mongo_url)
8✔
393
    mongo_url = config.conf("MONGO_URL", mongo_url)
8✔
394
    amqp_url = config.conf("CLOUDAMQP_URL", amqp_url)
8✔
395
    amqp_url = config.conf("RABBITMQ_URL", amqp_url)
8✔
396

397
    # retrieves the possible base URL configuration value and uses it
398
    # as the basis for the creation of the static URL values to be used
399
    # by the Flask infra-structure when using _external parameter
400
    base_url = config.conf("BASE_URL", None)
8✔
401
    if base_url:
8✔
402
        base_parse = legacy.urlparse(base_url)
×
403
        if base_parse.port: server_name = "%s:%d" % (base_parse.hostname, base_parse.port)
×
404
        else: server_name = "%s" % base_parse.hostname
×
405
        url_scheme = base_parse.scheme
×
406
        application_root = base_parse.path
×
407
    else:
408
        server_name, url_scheme, application_root = None, None, None
8✔
409

410
    # tries to retrieve the more specific URL related configuration values
411
    # defaulting to the base URL calculated ones in case they do not exist
412
    server_name = config.conf("SERVER_NAME", server_name)
8✔
413
    application_root = config.conf("APPLICATION_ROOT", application_root)
8✔
414
    url_scheme = config.conf("URL_SCHEME", url_scheme)
8✔
415
    url_scheme = config.conf("PREFERRED_URL_SCHEME", url_scheme)
8✔
416

417
    # re-sets the URL related configuration values, making sure that they
418
    # are properly set in the current configuration environment as they
419
    # are critical for proper external URL value generation
420
    config.confs("SERVER_NAME", server_name)
8✔
421
    config.confs("APPLICATION_ROOT", application_root)
8✔
422
    config.confs("PREFERRED_URL_SCHEME", url_scheme)
8✔
423

424
    # retrieves some internal configuration value related with
425
    # the way that the sessions are going to be handled
426
    session_cookie_path = config.conf("SESSION_COOKIE_PATH", "/")
8✔
427
    session_cookie_secure = config.conf("SESSION_COOKIE_SECURE", False, cast = bool)
8✔
428
    session_refresh_request = config.conf("SESSION_REFRESH_EACH_REQUEST", False, cast = bool)
8✔
429

430
    # re-sets a series of session related values according to quorum
431
    # predefined structure, this effectively enables sessions on
432
    # the client side to be handled in a predictable manner
433
    config.confs("SESSION_COOKIE_PATH", session_cookie_path)
8✔
434
    config.confs("SESSION_COOKIE_SECURE", session_cookie_secure)
8✔
435
    config.confs("SESSION_REFRESH_EACH_REQUEST", session_refresh_request)
8✔
436

437
    # creates the proper values according to the currently provided
438
    # ones so that they match the ones that are expected
439
    name = name + "-" + instance if instance else name
8✔
440
    prefix = instance + "-" if instance else ""
8✔
441
    suffix = "-" + instance if instance else ""
8✔
442
    level = logging.DEBUG if debug else _level(level_s)
8✔
443
    logger = logger and prefix + logger
8✔
444

445
    # retrieves the last stack element as the previous element and
446
    # uses it to retrieve the module that has triggered the loading
447
    previous = inspect.stack()[1]
8✔
448
    module = inspect.getmodule(previous[0])
8✔
449

450
    # uses the module to retrieve the base path for the execution of
451
    # the app, this is going to be used to calculate relative paths
452
    path = os.path.dirname(module.__file__)
8✔
453

454
    # creates the initial app reference using the provided one or
455
    # creates a new one from the provided/computed name, then sets
456
    # the current app reference as the current global one
457
    app = app or Quorum(name)
8✔
458
    APP = app
8✔
459

460
    # loads the app configuration from the provided keyword arguments
461
    # map and from the current dictionary of configuration values
462
    # (value propagation) and then starts the logging process with the
463
    # requested logger and provided "verbosity" level
464
    load_app_config(app, kwargs)
8✔
465
    load_app_config(app, config.confd())
8✔
466
    start_log(app, name = logger, level = level)
8✔
467

468
    # loads the various paths associated with the application into the
469
    # current environment to reduce the amount of issues related with
470
    # the importing of modules and other resources
471
    load_paths(app)
8✔
472

473
    # loads the complete set of bundle localized in the proper path into
474
    # the current app environment, this is a blocking operation and may
475
    # take some time to be performed completely
476
    load_bundles(app)
8✔
477

478
    # converts the naming of the adapter into a capital case one and
479
    # then tries to retrieve the associated class for proper instantiation
480
    # in case the class is not found the default value is set instead
481
    adapter_s = adapter_s.capitalize() + "Adapter"
8✔
482
    if not hasattr(data, adapter_s): adapter_s = "MongoAdapter"
8✔
483
    app.adapter = getattr(data, adapter_s)()
8✔
484

485
    # sets the various eval context filters as such by setting their eval
486
    # context filter flag to true the jinja infra-structure will handle
487
    # the rest of the operations so that it's properly used
488
    util.nl_to_br_jinja.evalcontextfilter = True
8✔
489
    util.nl_to_br_jinja.environmentfilter = True
8✔
490
    util.sp_to_nbsp_jinja.evalcontextfilter = True
8✔
491
    util.sp_to_nbsp_jinja.environmentfilter = True
8✔
492

493
    # updates the base app reference object with a series of operations
494
    # that should transform it's base execution and behavior
495
    app.before_request(before_request)
8✔
496
    app.after_request(after_request)
8✔
497
    app.context_processor(context_processor)
8✔
498
    app.template_filter("locale")(util.to_locale)
8✔
499
    app.template_filter("nl_to_br")(util.nl_to_br_jinja)
8✔
500
    app.template_filter("sp_to_nbsp")(util.sp_to_nbsp_jinja)
8✔
501
    app.template_filter("unset")(util.unset)
8✔
502
    app.request_class = request.Request
8✔
503
    app.locales = locales
8✔
504
    app.safe = safe
8✔
505
    app.debug = debug
8✔
506
    app.use_debugger = debug
8✔
507
    app.use_reloader = reloader
8✔
508
    app.secure_headers = True
8✔
509
    app.allow_origin = ALLOW_ORIGIN
8✔
510
    app.allow_headers = ALLOW_HEADERS
8✔
511
    app.allow_methods = ALLOW_METHODS
8✔
512
    app.content_security = CONTENT_SECURITY
8✔
513
    app.frame_options = FRAME_OPTIONS
8✔
514
    app.xss_protection = XSS_PROTECTION
8✔
515
    app.content_options = CONTENT_OPTIONS
8✔
516
    app.models = models
8✔
517
    app.module = module
8✔
518
    app.path = path
8✔
519
    app.secret_key = secret_key
8✔
520
    app.old_route = app.route
8✔
521
    app.route = route.route
8✔
522
    app.jinja_options = dict(
8✔
523
        finalize = finalize
524
    )
525
    app._locale_d = locales[0]
8✔
526

527
    # takes a snapshot of the current timestamp as it's
528
    # considered to be the start time of the application
529
    app.start_time = time.time()
8✔
530

531
    # sets a series of conditional based attributes in both
532
    # the associated modules and the base app object (as expected)
533
    if redis_url: redisdb.url = redis_url
8✔
534
    if mongo_url: mongodb.url = mongo_url
8✔
535
    if amqp_url: amqp.url = amqp_url
8✔
536
    if amazon_id: amazon.id = amazon_id
8✔
537
    if amazon_secret: amazon.secret = amazon_secret
8✔
538
    if amazon_bucket: amazon.bucket_name = amazon_bucket
8✔
539
    if pusher_app_id: pusherc.app_id = pusher_app_id
8✔
540
    if pusher_key: pusherc.key = pusher_key
8✔
541
    if pusher_secret: pusherc.secret = pusher_secret
8✔
542
    if pusher_cluster: pusherc.cluster = pusher_cluster
8✔
543
    if smtp_host: mail.SMTP_HOST = smtp_host
8✔
544
    if smtp_user: mail.SMTP_USER = smtp_user
8✔
545
    if smtp_password: mail.SMTP_PASSWORD = smtp_password
8✔
546
    if execution: start_execution()
8✔
547
    if redis_session: app.session_interface = session.RedisSessionInterface(url = redis_url)
8✔
548
    if mongo_database: mongodb.database = mongo_database + suffix
8✔
549
    if models: setup_models(models)
8✔
550
    if force_ssl: extras.SSLify(app)
8✔
551

552
    # verifies if the module that has called the method is not
553
    # of type main and in case it's not calls the runner methods
554
    # immediately so that the proper initialization is done, then
555
    # returns the app reference object to the caller method
556
    if not module.__name__ == "__main__": call_run()
8✔
557
    return app
8✔
558

559
def unload():
8✔
560
    """
561
    Unloads the current quorum instance, after this call
562
    no more access to the quorum facilities is allowed.
563

564
    A normal setup of the application would never require
565
    this function to be called directly.
566

567
    .. warning::
568

569
        Use this function with care as it may result in unexpected
570
        behavior from a developer point of view.
571

572
    .. note::
573

574
        After the call to this method most of the functionally of quorum
575
        will become unavailable until further call to :func:`quorum.load`.
576
    """
577

578
    global APP
579
    if not APP: return
8✔
580

581
    if APP.models: teardown_models(APP.models)
8✔
582

583
    APP = None
8✔
584

585
def load_all(path = None):
8✔
586
    load_config(3)
8✔
587
    config.load_file(path = path)
8✔
588
    config.load_env()
8✔
589

590
def load_config(offset = 1, encoding = "utf-8"):
8✔
591
    element = inspect.stack()[offset]
8✔
592
    module = inspect.getmodule(element[0])
8✔
593
    base_folder = os.path.dirname(module.__file__)
8✔
594
    config.load(path = base_folder, encoding = encoding)
8✔
595

596
def load_app_config(app, configs):
8✔
597
    for name, value in configs.items():
8✔
598
        app.config[name] = value
8✔
599

600
def load_paths(app):
8✔
601
    if not app.root_path in sys.path: sys.path.insert(0, app.root_path)
8✔
602

603
def load_bundles(app, offset = 2):
8✔
604
    # creates the base dictionary that will handle all the loaded
605
    # bundle information and sets it in the current application
606
    # object reference so that may be used latter on
607
    bundles = dict()
8✔
608
    app.bundles = bundles
8✔
609

610
    # inspects the current stack to obtain the reference to the base
611
    # application module and then uses it to calculate the base path
612
    # for the application, from there re-constructs the path to the
613
    # bundle file and verifies its own existence
614
    element = inspect.stack()[offset]
8✔
615
    module = inspect.getmodule(element[0])
8✔
616
    base_folder = os.path.dirname(module.__file__)
8✔
617
    bundles_path = os.path.join(base_folder, "bundles")
8✔
618
    if not os.path.exists(bundles_path): return
8✔
619

620
    # list the bundles directory files and iterates over each of the
621
    # files to load its own contents into the bundles "registry"
622
    paths = os.listdir(bundles_path)
×
623
    for path in paths:
×
624
        # joins the current (base) bundles path with the current path
625
        # in iteration to create the full path to the file and opens
626
        # it trying to read its JSON based contents
627
        path_f = os.path.join(bundles_path, path)
×
628
        file = open(path_f, "rb")
×
629
        try: data_j = json.load(file)
×
630
        except Exception: continue
×
631
        finally: file.close()
×
632

633
        # unpacks the current path in iteration into the base name,
634
        # locale string and file extension to be used in the registration
635
        # of the data in the bundles registry
636
        try: _base, locale, _extension = path.split(".", 2)
×
637
        except Exception: continue
×
638

639
        # retrieves a possible existing map for the current locale in the
640
        # registry and updates such map with the loaded data, then re-updates
641
        # the reference to the locale in the current bundle registry
642
        bundle = bundles.get(locale, {})
×
643
        bundle.update(data_j)
×
644
        bundles[locale] = bundle
×
645

646
def start_log(
8✔
647
    app,
648
    name = None,
649
    level = logging.WARN,
650
    format_base = log.LOGGING_FORMAT,
651
    format_tid = log.LOGGING_FORMAT_TID
652
):
653
    # tries to retrieve some of the default configuration values
654
    # that are going to be used in the logger startup
655
    format = config.conf("LOGGING_FORMAT", None)
8✔
656
    file_log = config.conf("FILE_LOG", False, cast = bool)
8✔
657
    stream_log = config.conf("STREAM_LOG", True, cast = bool)
8✔
658
    memory_log = config.conf("MEMORY_LOG", True, cast = bool)
8✔
659
    syslog_host = config.conf("SYSLOG_HOST", None)
8✔
660
    syslog_port = config.conf("SYSLOG_PORT", None, cast = int)
8✔
661
    syslog_proto = config.conf("SYSLOG_PROTO", "udp")
8✔
662
    syslog_kwargs = dict(socktype = socket.SOCK_STREAM) if\
8✔
663
        syslog_proto in ("tcp",) else dict()
664
    syslog_log = True if syslog_host else False
8✔
665

666
    # tries to determine the default syslog port in case no port
667
    # is defined and syslog logging is enabled
668
    if not syslog_port and syslog_log:
8✔
669
        syslog_port = log.SYSLOG_PORTS.get(syslog_proto)
×
670

671
    # "resolves" the proper logger file path taking into account
672
    # the currently defined operative system, should uses the system
673
    # level path in case the operative system is unix based
674
    if os.name == "nt": path_t = "%s"
8✔
675
    else: path_t = "/var/log/%s"
8✔
676
    path = name and path_t % name
8✔
677

678
    # creates the map that is going to be used to store the
679
    # various handlers registered for the logger
680
    app.handlers = dict()
8✔
681

682
    # creates the formatter object from the provided string
683
    # so that it may be used in the various handlers
684
    formatter = log.ThreadFormatter(format or format_base)
8✔
685
    formatter.set_tid(format or format_tid)
8✔
686

687
    # retrieves the reference to the logger object currently
688
    # associated with the app and disable the parent in it,
689
    # then removes the complete set of associated handlers and
690
    # sets the proper debug level, note that the logger is
691
    # going to be shared between quorum and flask (common logger)
692
    logger = app.logger if hasattr(app, "logger") else logging.getLogger("quorum")
8✔
693
    logger.parent = None
8✔
694
    logger.handlers = []
8✔
695
    logger.setLevel(level)
8✔
696

697
    # creates both the stream and the memory based handlers that
698
    # are going to be used for the current logger
699
    stream_handler = logging.StreamHandler() if stream_log else None
8✔
700
    memory_handler = log.MemoryHandler() if memory_log else None
8✔
701

702
    try:
8✔
703
        # tries to create the file handler for the logger with the
704
        # resolve path (operation may fail for permissions)
705
        file_handler = path and file_log and logging.FileHandler(path)
8✔
706
    except Exception:
×
707
        # in case there's an error creating the file handler for
708
        # the logger prints an error message indicating the problem
709
        sys.stderr.write("Problem starting logging for file '%s'\n" % path)
×
710
        file_handler = None
×
711

712
    # adds the various created handler to the current logger so that
713
    # they are going to be used when using the logger for output
714
    if stream_handler: logger.addHandler(stream_handler)
8✔
715
    if memory_handler: logger.addHandler(memory_handler)
8✔
716
    if file_handler: logger.addHandler(file_handler)
8✔
717

718
    # for each of the handlers adds them to the handlers map in case
719
    # they are valid and defined (no problem in construction)
720
    if stream_handler: app.handlers["stream"] = stream_handler
8✔
721
    if memory_handler: app.handlers["memory"] = memory_handler
8✔
722
    if file_handler: app.handlers["file"] = file_handler
8✔
723

724
    # iterates over the complete set of handlers currently
725
    # registered in the logger to properly set the formatter
726
    # and the level for all of them (as specified)
727
    for handler in logger.handlers:
8✔
728
        handler.setFormatter(formatter)
8✔
729
        handler.setLevel(level)
8✔
730

731
    # determines if the creation of the syslog handler is required and
732
    # it that the case created it setting the appropriate formatter to it
733
    syslog_handler = logging.handlers.SysLogHandler(
8✔
734
        (syslog_host, syslog_port), **syslog_kwargs
735
    ) if syslog_log else None if syslog_log else None
736

737
    # in case the syslog handler has been created creates the appropriate
738
    # formatter for it, sets the level and adds it to the logger
739
    if syslog_handler:
8✔
740
        syslog_formatter = log.BaseFormatter(
×
741
            log.LOGGIGN_SYSLOG % APP.name if APP else "quorum",
742
            datefmt = "%Y-%m-%dT%H:%M:%S.000000+00:00",
743
            wrap = True
744
        )
745
        syslog_handler.setLevel(level)
×
746
        syslog_handler.setFormatter(syslog_formatter)
×
747
        logger.addHandler(syslog_handler)
×
748
        app.handlers["syslog"] = syslog_handler
×
749

750
    # runs the extra logging step for the current state, meaning that
751
    # some more handlers may be created according to the logging config
752
    extra_logging(logger, level, formatter)
8✔
753

754
    # sets the current logger in the top level app value so that
755
    # this logger is going to be used as the quorum logger
756
    app.logger_q = logger
8✔
757

758
def extra_logging(logger, level, formatter):
8✔
759
    """
760
    Loads the complete set of logging handlers defined in the
761
    current logging value, should be a map of definitions.
762

763
    This handlers will latter be used for piping the various
764
    logging messages to certain output channels.
765

766
    The creation of the handler is done using a special keyword
767
    arguments strategy so that python and configuration files
768
    are properly set as compatible.
769

770
    :type logger Logger
771
    :param logger: The logger currently in use for where the new\
772
    handlers that are going to be created will be added.
773
    :type level: String/int
774
    :param level: The base severity level for which the new handler\
775
    will be configured in case no extra level definition is set.
776
    :type formatter: Formatter
777
    :param formatter: The logging formatter instance to be set in\
778
    the handler for formatting messages to the output.
779
    """
780

781
    # verifies if the logging attribute of the current instance is
782
    # defined and in case it's not returns immediately
783
    logging = config.conf("LOGGING", None)
8✔
784
    if not logging: return
8✔
785

786
    # iterates over the complete set of handler configuration in the
787
    # logging to create the associated handler instances
788
    for _config in logging:
×
789
        # gathers the base information on the current handler configuration
790
        # running also the appropriate transformation on the level
791
        name = _config.get("name", None)
×
792
        __level = _config.get("level", level)
×
793
        __level = _level(__level)
×
794

795
        # "clones" the configuration dictionary and then removes the base
796
        # values so that they do not interfere with the building
797
        _config = dict(_config)
×
798
        if "level" in _config: del _config["level"]
×
799
        if "name" in _config: del _config["name"]
×
800

801
        # retrieves the proper building, skipping the current loop in case
802
        # it does not exits and then builds the new handler instance, setting
803
        # the proper level and formatter and then adding it to the logger
804
        if not hasattr(log, name + "_handler"): continue
×
805
        builder = getattr(log, name + "_handler")
×
806
        handler = builder(**_config)
×
807
        handler.setLevel(__level)
×
808
        handler.setFormatter(formatter)
×
809
        logger.addHandler(handler)
×
810

811
def get_app(app = None):
8✔
812
    return app or APP
8✔
813

814
def get_adapter(app = None):
8✔
815
    return APP and APP.adapter
8✔
816

817
def get_log(app = None):
8✔
818
    app = app or APP
8✔
819
    if not app: return None
8✔
820
    is_custom = hasattr(app, "logger_q")
×
821
    return app.logger_q if is_custom else app.logger
×
822

823
def get_level(app = None):
8✔
824
    logger = get_log(app = app)
8✔
825
    if not logger: return None
8✔
826
    return logger.level
×
827

828
def get_handlers(app = None):
8✔
829
    logger = get_log(app = app)
×
830
    if not logger: return None
×
831
    return logger.handlers
×
832

833
def get_handler(name, app = None):
8✔
834
    app = app or APP
×
835
    if not app: return None
×
836
    return app.handlers.get(name, None)
×
837

838
def get_bundle(name, app = None, split = True):
8✔
839
    app = app or APP
8✔
840
    if not app: return None
8✔
841
    bundle = app.bundles.get(name, None)
8✔
842
    if bundle: return bundle
8✔
843
    if split and name:
8✔
844
        base = name.split("_", 1)[0]
8✔
845
        bundle = app.bundles.get(base, None)
8✔
846
        if bundle: return bundle
8✔
847
    name = _best_locale(name)
8✔
848
    return app.bundles.get(name, None)
8✔
849

850
def is_devel(app = None):
8✔
851
    level = get_level(app = app)
8✔
852
    if not level: return False
8✔
853
    return level < logging.INFO
×
854

855
def finalize(value):
8✔
856
    # returns an empty string as value representation
857
    # for unset values, this is the default representation
858
    # to be used in the template engine
859
    if value == None: return ""
×
860
    return value
×
861

862
def before_request():
8✔
863
    flask.request.args_s = util.load_form(flask.request.args)
8✔
864
    flask.request.form_s = util.load_form(flask.request.form)
8✔
865
    flask.request.locale = util.load_locale(APP.locales)
8✔
866
    util.set_locale()
8✔
867

868
def after_request(response):
8✔
869
    if APP.safe: util.reset_locale()
8✔
870
    util.anotate_async(response)
8✔
871
    util.anotate_secure(response)
8✔
872
    return response
8✔
873

874
def context_processor():
8✔
875
    return dict(
×
876
        acl = acl.check_login,
877
        conf = config.conf,
878
        locale = util.to_locale,
879
        nl_to_br = util.nl_to_br,
880
        sp_to_nbsp = util.sp_to_nbsp,
881
        date_time = util.date_time,
882
        time = time,
883
        datetime = datetime,
884
        zip = zip
885
    )
886

887
def start_execution():
8✔
888
    # creates the thread that it's going to be used to
889
    # execute the various background tasks and starts
890
    # it, providing the mechanism for execution
891
    execution.background_t = execution.ExecutionThread()
8✔
892
    background_t = execution.background_t
8✔
893
    background_t.start()
8✔
894

895
@atexit.register
8✔
896
def stop_execution():
897
    # stop the execution thread so that it's possible to
898
    # the process to return the calling
899
    background_t = execution.background_t
×
900
    background_t and background_t.stop()
×
901

902
def setup_models(models):
8✔
903
    _models_c = models_c(models = models)
8✔
904
    for model_c in _models_c: model_c.setup()
8✔
905

906
def teardown_models(models):
8✔
907
    _models_c = models_c(models = models)
8✔
908
    for model_c in _models_c: model_c.teardown()
8✔
909

910
def models_c(models = None):
8✔
911
    # retrieves the proper models defaulting to the current
912
    # application models in case they are not defined, note
913
    # that in case the model is not defined an empty list is
914
    # going to be returned (fallback process)
915
    models = models or APP.models
8✔
916
    if not models: return []
8✔
917

918
    # creates the list that will hold the various model
919
    # class discovered through module analysis
920
    models_c = []
8✔
921

922
    # iterates over the complete set of items in the models
923
    # modules to find the ones that inherit from the base
924
    # model class for those are the real models
925
    for _name, value in models.__dict__.items():
8✔
926
        # verifies if the current value in iteration inherits
927
        # from the top level model in case it does not continues
928
        # the loop as there's nothing to be done
929
        try: is_valid = issubclass(value, model.Model)
8✔
930
        except Exception: is_valid = False
8✔
931
        if not is_valid: continue
8✔
932

933
        # adds the current value in iteration as a new class
934
        # to the list that hold the various model classes
935
        models_c.append(value)
8✔
936

937
    # returns the list containing the various model classes
938
    # to the caller method as expected by definition
939
    return models_c
8✔
940

941
def resolve(identifier = "_id", counters = True):
8✔
942
    # creates the list that will hold the definition of the current
943
    # model classes with a sequence of name and identifier values
944
    entities = []
8✔
945

946
    # retrieves the complete set of model classes registered
947
    # for the current application and for each of them retrieves
948
    # the name of it and creates a tuple with the name and the
949
    # identifier attribute name adding then the tuple to the
950
    # list of entities tuples (resolution list)
951
    _models_c = models_c()
8✔
952
    for model_c in _models_c:
8✔
953
        name = model_c._name()
×
954
        tuple = (name, identifier)
×
955
        entities.append(tuple)
×
956

957
    # in case the counters flag is defined the counters tuple containing
958
    # the counters table name and identifier is added to the entities list
959
    if counters: entities.append(("counters", identifier))
8✔
960

961
    # returns the resolution list to the caller method as requested
962
    # by the call to this method
963
    return entities
8✔
964

965
def templates_path():
8✔
966
    return os.path.join(APP.root_path, APP.template_folder)
×
967

968
def bundles_path():
8✔
969
    return os.path.join(APP.root_path, "bundles")
×
970

971
def base_path(*args, **kwargs):
8✔
972
    return os.path.join(APP.root_path, *args)
×
973

974
def has_context():
8✔
975
    if hasattr(flask, "has_app_context"):
8✔
976
        return flask.has_app_context()
8✔
977

978
    if not hasattr(flask, "_app_ctx_stack"): return False
×
979
    if not hasattr(flask._app_ctx_stack, "top"): return False
×
980
    if not flask._app_ctx_stack.top: return False
×
981
    return True
×
982

983
def ensure_context(function):
8✔
984
    """
985
    Decorator that makes sure that the underlying execution
986
    method/function is run inside a valid flask app context.
987

988
    In case there's currently no app context defined it uses
989
    the global Application reference to create a new one.
990

991
    :type function: Function
992
    :param function: The function that is going to be decorated
993
    by the current ensure decorator.
994
    :rtype: Decorator
995
    :return: The decorator that should be used for the ensuring
996
    of the app context in the current execution environment.
997
    """
998

999
    @functools.wraps(function)
8✔
1000
    def interceptor(*args, **kwargs):
1001
        _ctx = has_context()
8✔
1002
        _app_ctx = APP.app_context() if APP else None
8✔
1003
        _ensure = True if not _ctx and _app_ctx else False
8✔
1004
        try:
8✔
1005
            if _ensure:
8✔
1006
                # verifies if the old version of app context
1007
                # stack management is the one that is currently
1008
                # in use and uses the appropriate push method
1009
                if hasattr(flask, "_app_ctx_stack") and\
8✔
1010
                    hasattr(flask._app_ctx_stack, "push"):
1011
                    flask._app_ctx_stack.push(_app_ctx)
8✔
1012
                elif _app_ctx:
×
1013
                    _app_ctx.push()
×
1014
            result = function(*args, **kwargs)
8✔
1015
        finally:
1016
            if _ensure:
8✔
1017
                # verifies if the old version of app context
1018
                # stack management is the one that is currently
1019
                # in use and uses the appropriate pop method
1020
                if hasattr(flask, "_app_ctx_stack") and\
8✔
1021
                    hasattr(flask._app_ctx_stack, "pop"):
1022
                    flask._app_ctx_stack.pop()
8✔
1023
                elif _app_ctx:
×
1024
                    _app_ctx.pop()
×
1025
        return result
8✔
1026

1027
    return interceptor
8✔
1028

1029
def onrun(function):
8✔
1030
    fname = function.__name__
×
1031
    if fname in RUN_F: return
×
1032
    RUN_F[fname] = function
×
1033
    return function
×
1034

1035
def _level(level):
8✔
1036
    """
1037
    Converts the provided logging level value into the best
1038
    representation of it, so that it may be used to update
1039
    a logger's level of representation.
1040

1041
    This method takes into account the current interpreter
1042
    version so that no problem occur.
1043

1044
    :type level: String/int
1045
    :param level: The level value that is meant to be converted
1046
    into the best representation possible.
1047
    :rtype: int
1048
    :return: The best representation of the level so that it may
1049
    be used freely for the setting of logging levels under the
1050
    current running interpreter.
1051
    """
1052

1053
    level_t = type(level)
8✔
1054
    if level_t == int: return level
8✔
1055
    if level == None: return level
8✔
1056
    if level == "SILENT": return log.SILENT
8✔
1057
    if hasattr(logging, "_checkLevel"):
8✔
1058
        return logging._checkLevel(level)
8✔
1059
    return logging.getLevelName(level)
×
1060

1061
def _best_locale(locale, app = None):
8✔
1062
    if not locale: return locale
8✔
1063
    app = app or APP
8✔
1064
    for _locale in app.locales:
8✔
1065
        is_valid = _locale.startswith(locale)
8✔
1066
        if not is_valid: continue
8✔
1067
        return _locale
8✔
1068
    return locale
8✔
1069

1070
# runs the monkey patching of the flask module so that it
1071
# may be used according to the quorum specification, this
1072
# is used in order to avoid minimal effort in the conversion
1073
# of old flask based applications (reverse compatibility)
1074
monkey()
8✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc