Source code for keg.app

import importlib

import flask
from werkzeug.datastructures import ImmutableDict

import keg.cli
import keg.config
from keg.ctx import KegRequestContext
import keg.logging
import keg.signals as signals
from keg.extensions import lazy_gettext as _
from keg.templating import _keg_default_template_ctx_processor, AssetsExtension
from keg.utils import classproperty, visit_modules, hybridmethod
import keg.web


class KegAppError(Exception):
    pass


[docs]class Keg(flask.Flask): import_name = None use_blueprints = () config_class = keg.config.Config logging_class = keg.logging.Logging _cli = None cli_loader_class = keg.cli.CLILoader db_enabled = False db_visit_modules = ['.model.entities'] db_manager = None jinja_options = ImmutableDict( extensions=[AssetsExtension] ) template_filters = ImmutableDict() template_globals = ImmutableDict() visit_modules = False _init_ran = False def __init__(self, import_name=None, *args, **kwargs): # flask requires an import name, so we should too. if import_name is None and self.import_name is None: raise KegAppError(_('Please set the "import_name" attribute on your app class or' ' pass it into the app instance.')) # passed in value takes precedence import_name = import_name or self.import_name self._init_config = kwargs.pop('config', {}) flask.Flask.__init__(self, import_name, *args, **kwargs)
[docs] def make_config(self, instance_relative=False): """ Needed for Flask <= 0.10.x so we can set the configuration class being used. Once 0.11 comes out, Flask supports setting the config_class on the app. """ root_path = self.root_path if instance_relative: root_path = self.instance_path return self.config_class(root_path, self.default_config)
def init(self, config_profile=None, use_test_profile=False, config=None): if self._init_ran: raise KegAppError(_('init() already called on this instance')) self._init_ran = True self.init_config(config_profile, use_test_profile, config) self.init_logging() self.init_error_handling() self.init_extensions() self.init_routes() self.init_blueprints() self.init_jinja() self.init_visit_modules() self.on_init_complete() signals.app_ready.send(self) signals.init_complete.send(self) # return self for easy chaining, i.e. app = MyKegApp().init() return self
[docs] def on_init_complete(self): """ For subclasses to override """ pass
def init_config(self, config_profile, use_test_profile, config): init_config = self._init_config.copy() init_config.update(config or {}) self.config.init_app(config_profile, self.import_name, self.root_path, use_test_profile) self.config.update(init_config) signals.config_ready.send(self) signals.config_complete.send(self) self.on_config_complete()
[docs] def on_config_complete(self): """ For subclasses to override """ pass
def init_extensions(self): self.init_db() # Keg components are essentially flask extensions, so they need to be set up during the # init process. Being extensions, it makes sense to trigger this here. But, they will best # be set up at the end of the init process, because other app-specific extensions may be # set up in `on_init_complete` for error handling/notifications and the like. signals.init_complete.connect(self.init_registered_components, sender=self) def db_manager_cls(self): from keg.db import DatabaseManager return DatabaseManager def init_db(self): if self.db_enabled: cls = self.db_manager_cls() self.db_manager = cls(self) def init_registered_components(self, app): # KEG_REGISTERED_COMPONENTS is presumed to be a set/list/iterable of dotted paths usable # for import. At the top level of the imported path, there should be a `__component__` # that takes the dotted path that was used for import (as an absolute parent for relative # imports) and has an init_app. Ideally, based on KegComponent. for comp_path in self.config.get('KEG_REGISTERED_COMPONENTS', set()): comp_module = importlib.import_module(comp_path) comp_object = getattr(comp_module, '__component__') comp_object.init_app(self, parent_path=comp_path) def init_blueprints(self): for blueprint in self.use_blueprints: self.register_blueprint(blueprint) def init_logging(self): self.logging = self.logging_class(self.config) self.logging.init_app() def init_error_handling(self): # handle status codes generic_errors = range(500, 506) for err in generic_errors: self.errorhandler(err)(self.handle_server_error) # utility to abort responses self.errorhandler(keg.web.ImmediateResponse)(keg.web.handle_immediate_response) def init_jinja(self): self.jinja_env.filters.update(self.template_filters) # template_context_processors is supposed to be functions that return dictionaries where # the key is the name of the template variable and the value is the value. # First, add Keg defaults self.template_context_processors[None].append(_keg_default_template_ctx_processor) self.template_context_processors[None].append(lambda: self.template_globals) def init_visit_modules(self): if self.visit_modules: visit_modules(self.visit_modules, self.import_name) def handle_server_error(self, error): # send_exception_email() return '500 SERVER ERROR<br/><br/>{}'.format(_('administrators notified'))
[docs] def request_context(self, environ): """Request context for the app.""" return KegRequestContext(self, environ)
def _cli_getter(cls): # noqa: first argument is not self in this context due to @classproperty if cls._cli is None: cal = cls.cli_loader_class(cls) cls._cli = cal.create_group() return cls._cli cli = classproperty(_cli_getter, ignore_set=True) @classmethod def environ_key(cls, key): # App names often have periods and it is not possibe to export an # environment variable with a period in it. name = cls.import_name.replace('.', '_').upper() return '{}_{}'.format(name, key.upper())
[docs] @classmethod def testing_prep(cls, **config): """ 1. Instantiate the app class. 2. Cache the app instance after creation so that it's only instantiated once per Python process. 3. Trigger `signal.testing_run_start` the first time this method is called for an app class. """ # For now, do the import here so we don't have a hard dependency on WebTest from keg.testing import ContextManager if cls is Keg: raise TypeError(_('Don\'t use testing_prep() on Keg. Create a subclass first.')) cm = ContextManager.get_for(cls) # If the context manager's app isn't ready, that means this will be the first time the app # is instantiated. That seems like a good indicator that tests are just beginning, so it's # safe to trigger the signal. We don't want the signal to fire every time b/c # testing_prep() can be called more than once per test run. if not cm.is_ready(): app = cm.make_ready(config) # Setup an app context so that DB operations will work without error. with app.app_context(): signals.testing_run_start.send(app) return cm.app
@property def logger(self): """Standard logger for the app.""" return self.logging.app_logger @hybridmethod def route(self, rule, **options): """ Same as Flask.route() and will be used when in an instance context. """ return super(Keg, self).route(rule, **options)
[docs] @route.classmethod def route(cls, rule, **options): # noqa """ Enable .route() to be used in a class context as well. E.g.:: KegApp.route('/something'): def view_something(): pass """ def decorator(f): if not hasattr(cls, '_routes'): cls._routes = [] cls._routes.append((f, rule, options)) return f return decorator
def init_routes(self): if not hasattr(self, '_routes'): return for func, rule, options in self._routes: # We follow the same logic here as Flask.route() decorator. endpoint = options.pop('endpoint', None) self.add_url_rule(rule, endpoint, func, **options)