Source code for kobin.app

"""
Kobin class
===========

The Kobin instance are callable WSGI Application.

Usage
-----

.. code-block:: python

   from kobin import Kobin, Response
   app = Kobin()

   @app.route('/')
   def index() -> Response:
       return Response('Hello World')

"""
from importlib.machinery import SourceFileLoader
import logging
import os
import traceback
from urllib.parse import urljoin
import warnings

from .routes import Router
from .requests import request
from .responses import HTTPError


[docs]class Kobin: """ This class is a WSGI application implementation. Create a instance, and run using WSGI Server. """ def __init__(self, config=None): self.router = Router() self.config = load_config(config) self.before_request_callbacks = [] self.after_request_callbacks = [] self.logger = self.config.get('LOGGER') self._frozen = False def __call__(self, environ, start_response): """It is called when receive http request.""" if not self._frozen: self._frozen = True response = self._handle(environ) start_response(response.status, response.headerlist) return response.body def __setattr__(self, key, value): if self.frozen: warnings.warn("Cannot Change the state of started application!", stacklevel=2) else: super().__setattr__(key, value) def __delattr__(self, item): if self.frozen: warnings.warn("Cannot Delete the state of started application!", stacklevel=2) else: super().__setattr__(item) @property def frozen(self): if '_frozen' not in dir(self): return False return self._frozen def route(self, rule=None, method='GET', name=None, callback=None): def decorator(callback_func): self.router.add(rule, method, name, callback_func) return callback_func return decorator(callback) if callback else decorator def before_request(self, callback): def decorator(callback_func): self.before_request_callbacks.append(callback_func) return callback_func return decorator(callback) def after_request(self, callback): def decorator(callback_func): self.after_request_callbacks.append(callback_func) return callback_func return decorator(callback) def _handle(self, environ): environ['kobin.app'] = self request.bind(environ) try: for before_request_callback in self.before_request_callbacks: before_request_callback() method = environ['REQUEST_METHOD'] path = environ['PATH_INFO'] or '/' callback, kwargs = self.router.match(path, method) response = callback(**kwargs) if kwargs else callback() for after_request_callback in self.after_request_callbacks: wrapped_response = after_request_callback(response) if wrapped_response: response = wrapped_response except HTTPError as e: response = e except BaseException as e: error_message = _get_exception_message(e, self.config.get('DEBUG')) self.logger.debug(error_message) response = HTTPError(error_message, 500) return response
def _get_exception_message(e, debug): if debug: stacktrace = '\n'.join(traceback.format_tb(e.__traceback__)) message = f"500: Internal Server Error\n\n" \ f"Exception:\n {repr(e)}\n\n" \ f"Stacktrace:\n{stacktrace}\n" else: message = 'Internal Server Error' return message # Following configurations are optional: # # * DEBUG # * SECRET_KEY # * TEMPLATE_DIRS (default: './templates/') or TEMPLATE_ENVIRONMENT # * LOG_LEVEL # * LOG_HANDLER # def _current_app(): # This function exists for unittest.mock.patch. return request['kobin.app'] def template_router_reverse(name, with_host=False): url = _current_app().router.reverse(name) if with_host: url = urljoin(request.url, url) if url is None: return '' return url def load_jinja2_env(template_dirs, global_variables=None, global_filters=None, **envoptions): try: from jinja2 import Environment, FileSystemLoader env = Environment(loader=FileSystemLoader(template_dirs), **envoptions) if global_variables: env.globals.update(global_variables) if global_filters: env.filters.update(global_filters) return env except ImportError: pass def _get_default_logger(debug): logger = logging.getLogger(__name__) handler = logging.StreamHandler() if debug: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) logger.addHandler(handler) return logger def load_config(config=None): default_config = { 'BASE_DIR': os.path.abspath('.'), 'TEMPLATE_DIRS': [os.path.join(os.path.abspath('.'), 'templates')], 'DEBUG': False, } if config is not None: default_config.update(config) if 'TEMPLATE_ENVIRONMENT' not in default_config: env = load_jinja2_env(default_config['TEMPLATE_DIRS']) if env: default_config['TEMPLATE_ENVIRONMENT'] = env if 'LOGGER' not in default_config: default_config['LOGGER'] = _get_default_logger(default_config.get('DEBUG')) return default_config def load_config_from_module(module): config = {key: getattr(module, key) for key in dir(module) if key.isupper()} return load_config(config) def load_config_from_pyfile(filepath): module = SourceFileLoader('config', filepath).load_module() return load_config_from_module(module)
[docs]def current_config(key, default=None): """Get the configurations of your Kobin's application.""" return request['kobin.app'].config.get(key, default)