Source code for score.http._init
# Copyright © 2015-2017 STRG.AT GmbH, Vienna, Austria
#
# This file is part of the The SCORE Framework.
#
# The SCORE Framework and all its parts are free software: you can redistribute
# them and/or modify them under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation which is in the
# file named COPYING.LESSER.txt.
#
# The SCORE Framework and all its parts are distributed without any WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. For more details see the GNU Lesser General Public
# License.
#
# If you have not received a copy of the GNU Lesser General Public License see
# http://www.gnu.org/licenses/.
#
# The License-Agreement realised between you as Licensee and STRG.AT GmbH as
# Licenser including the issue of its valid conclusion and its pre- and
# post-contractual effects is governed by the laws of Austria. Any disputes
# concerning this License-Agreement including the issue of its valid conclusion
# and its pre- and post-contractual effects are exclusively decided by the
# competent court, in whose district STRG.AT GmbH has its registered seat, at
# the discretion of STRG.AT GmbH also the competent court, in whose district the
# Licensee has his registered seat, an establishment or assets.
from score.init import (
parse_list, parse_dotted_path, extract_conf, parse_bool, ConfigurationError)
import re
from score.init import ConfiguredModule
import inspect
import functools
from ._urltpl import MissingVariable, InvalidVariable
from webob import Request, Response
from webob.exc import (
HTTPMovedPermanently, HTTPFound, HTTPNotFound, HTTPException,
HTTPInternalServerError)
import logging
from collections import OrderedDict
import urllib
defaults = {
'debug': False,
'preroutes': [],
'urlbase': None,
'ctx.member.url': 'url',
'serve.ip': '0.0.0.0',
'serve.port': 8080,
'serve.threaded': False,
}
[docs]def init(confdict, ctx, orm=None, tpl=None):
"""
Initializes this module acoording to :ref:`our module initialization
guidelines <module_initialization>` with the following configuration keys:
:confkey:`router`
Path to the :class:`RouteConfiguration` containing the list of routes to
compile.
:confkey:`preroutes` :confdefault:`list()`
List of :term:`preroute` functions to call before invoking the actual
route. See :ref:`http_routing` for details.
:confkey:`handler.*`
Keys starting with "``handler.``" are interpreted as :ref:`error
handlers <http_error_handler>`.
:confkey:`debug` :confdefault:`False`
Setting this to `True` will enable the `werkzeug debugger`_ for your
application.
:confkey:`urlbase` :confdefault:`None`
This will be the prefix for all URLs generated by the module. The module
will create relative URLs by default (i.e. `/Sir%20Lancelot`), but you
can make it create absolute URLs by default by paassing this
configuration value.
If you configure this to be 'http://example.net/', your URL would be
'http://example.net/Sir%20Lancelot'.
Note that you can always decide, whether a *certain* URL should be
absolute or relative, by passing the appropriate argument to
:meth:`ConfiguredHttpModule.url`.
:confkey:`ctx.member.url` :confdefault:`url`
The name of the :term:`context member` function for generating URLs.
:confkey:`serve.ip` :confdefault:`0.0.0.0`
This will be the ip address your HTTP server will bind_ to, when using
:mod:`score.serve` to serve your application.
:confkey:`serve.port` :confdefault:`8080`
This will be the port of your HTTP server, when using
:mod:`score.serve` to serve your application.
:confkey:`serve.threaded` :confdefault:`False`
Setting this to `True` will make your HTTP server threaded, which should
increase its performance. Note that your application will need to be
thread-safe_, if you want to enable this feature.
.. _werkzeug debugger: http://werkzeug.pocoo.org/docs/0.11/debug/#using-the-debugger
.. _bind: http://www.xeams.com/bindtoaddress.htm
.. _thread-safe: https://en.wikipedia.org/wiki/Thread_safety
"""
conf = dict(defaults.items())
conf.update(confdict)
if 'router' not in conf:
import score.http
raise ConfigurationError(score.http, 'No router provided')
router = parse_dotted_path(conf['router'])
preroutes = list(map(parse_dotted_path, parse_list(conf['preroutes'])))
error_handlers = {}
exception_handlers = {}
for error, handler in extract_conf(conf, 'handler.').items():
if re.match('\d(\d\d|XX)', error):
error_handlers[error] = parse_dotted_path(handler)
else:
error = parse_dotted_path(error)
exception_handlers[error] = handler
debug = parse_bool(conf['debug'])
if not conf['urlbase']:
conf['urlbase'] = ''
http = ConfiguredHttpModule(
ctx, orm, tpl, router, preroutes, error_handlers, exception_handlers,
debug, conf['urlbase'], conf['serve.ip'], int(conf['serve.port']),
parse_bool(conf['serve.threaded']))
def constructor(ctx):
def url(*args, **kwargs):
return http.url(ctx, *args, **kwargs)
return url
ctx.register(conf['ctx.member.url'], constructor)
return http
log = logging.getLogger('score.http.router')
[docs]class Route:
"""
A :term:`route` representation.
"""
def __init__(self, conf, route):
self.conf = conf
self.name = route.name
self.urltpl = route.urltpl
if isinstance(self.urltpl, str):
self.urltpl = conf.url_class(self.urltpl)
self.tpl = route.tpl
self.callback = route.callback
self.preconditions = route.preconditions
self._match2vars = route._match2vars
self._vars2url = route._vars2url
self._vars2urlparts = route._vars2urlparts
@property
def callback(self):
return self._callback
@callback.setter
def callback(self, callback):
self._callback = callback
functools.update_wrapper(self, self.callback)
[docs] def url(self, ctx, *args, **kwargs):
"""
Creates the URL to this route with given arguments.
"""
urlbase = ''
absolute = True
if '_absolute' in kwargs:
absolute = kwargs['_absolute']
del kwargs['_absolute']
assert '_relative' not in kwargs
elif '_relative' in kwargs:
absolute = not kwargs['_relative']
del kwargs['_relative']
query = ''
if '_query' in kwargs:
if kwargs['_query']:
query = '?' + urllib.parse.urlencode(kwargs['_query'])
del kwargs['_query']
anchor = ''
if '_anchor' in kwargs:
if kwargs['_anchor']:
anchor = '#' + urllib.parse.quote(kwargs['_anchor'])
del kwargs['_anchor']
if absolute:
try:
urlbase = kwargs['_urlbase']
del kwargs['_urlbase']
except KeyError:
urlbase = self.conf.urlbase
if self._vars2url:
url = self._vars2url(ctx, *args, **kwargs)
else:
if self._vars2urlparts:
kwargs.update(self._vars2urlparts(ctx, *args, **kwargs))
self._args2kwargs(args, kwargs)
variables = self._kwargs2vars(kwargs)
url = self.urltpl.generate(**variables)
if urlbase:
url = urlbase + url
return url + query + anchor
def _args2kwargs(self, args, kwargs):
if not args:
return
params = inspect.signature(self.callback).parameters
for i, name in enumerate(params):
if name not in kwargs:
kwargs[name] = args[i - 1]
def _kwargs2vars(self, kwargs):
variables = {}
for name in self.urltpl.variables:
if name in kwargs:
variables[name] = kwargs[name]
continue
parts = name.split('.')
if parts[0] not in kwargs:
raise MissingVariable(parts[0])
current = kwargs[parts[0]]
for part in parts[1:]:
try:
current = getattr(current, part)
except AttributeError:
raise InvalidVariable(
'Could not retrieve "%s" from %s' %
('.'.join(parts[1:]), kwargs[parts[0]]))
variables[name] = current
return variables
def _call_match2vars(self, ctx, match):
variables = self.urltpl.match2vars(ctx, match)
if self._match2vars:
newvars = self._match2vars(ctx, variables)
if not newvars:
log.debug(' %s: registered match2vars() could not '
'convert variables (%s)' % (self.name, variables))
return None
variables = newvars
else:
# remove matches containing dots
variables = dict((k, v)
for (k, v) in variables.items() if '.' not in k)
for callback in self.preconditions:
if not callback(ctx, **variables):
log.debug(' %s: precondition failed (%s)' %
(self.name, callback))
return None
return variables
def can_handle(self, request):
match = self.urltpl.regex.match(urllib.parse.unquote(request.path))
if not match:
return False
ctx = self.conf.ctx.Context()
ctx.http = self.conf.create_ctx_member(request)
try:
variables = self._call_match2vars(ctx, match)
if variables is None:
return False
except HTTPException:
# the _match2vars function may raise an HTTPException, which implies
# that this route would indeed be responsible for the given request,
# but its implementation chose to handle it prematurely (i.e. before
# the route callback itself was executed)
pass
return True
def extract_variables(self, request):
match = self.urltpl.regex.match(urllib.parse.unquote(request.path))
if not match:
return None
ctx = self.conf.ctx.Context()
ctx.http = self.conf.create_ctx_member(request)
try:
return self._call_match2vars(ctx, match)
except HTTPException as exception:
# see can_handle() for the reason we're returning the exception here
return exception
def handle(self, ctx):
request = ctx.http.request
match = self.urltpl.regex.match(urllib.parse.unquote(request.path))
if not match:
log.debug(' %s: No regex match (%s)' %
(self.name, self.urltpl.regex.pattern))
return None
try:
variables = self._call_match2vars(ctx, match)
if variables is None:
return None
log.debug(' %s: SUCCESS, invoking callback' % (self.name))
ctx.http.route = self
ctx.http.route_vars = variables
for preroute in self.conf.preroutes:
preroute(ctx)
result = self.callback(ctx, **variables)
except HTTPException as response:
result = response
if isinstance(result, Response):
ctx.http.response = result
return result
if isinstance(result, str):
ctx.http.response.text = result
elif self.tpl:
if result is None:
result = {}
else:
assert isinstance(result, dict)
result['ctx'] = ctx
ctx.http.response.text = self.conf.tpl.render(self.tpl, result)
return ctx.http.response
[docs]class ConfiguredHttpModule(ConfiguredModule):
"""
This module's :class:`configuration class <score.init.ConfiguredModule>`.
"""
def __init__(self, ctx, orm, tpl, router, preroutes, error_handlers,
exception_handlers, debug, urlbase, host, port, threaded):
self.ctx = ctx
self.orm = orm
self.tpl = tpl
self.router = router.clone()
self.preroutes = preroutes
self.error_handlers = error_handlers
self.exception_handlers = exception_handlers
self.debug = debug
self.urlbase = urlbase
self.host = host
self.port = port
self.threaded = threaded
[docs] def route(self, name):
"""
Provides the :class:`Route` with given *name*.
"""
return self.routes[name]
def newroute(self, *args, **kwargs):
assert not self._finalized
return self.router.route(*args, **kwargs)
def _finalize(self):
self.routes = OrderedDict((route.name, Route(self, route))
for route in self.router.sorted_routes())
for name, route in self.routes.items():
if not route._match2vars and self.orm:
route._match2vars = self._mk_match2vars(route)
if not log.isEnabledFor(logging.DEBUG):
return
msg = 'Compiled routes:'
for name, route in self.routes.items():
msg += '\n - %s (%s)' % (name, route.urltpl)
log.debug(msg)
def _mk_match2vars(self, route):
param2clsid = {}
parameters = inspect.signature(route.callback).parameters
test_redirect = False
for i, (name, param) in enumerate(parameters.items()):
if i == 0:
continue
if param.annotation is inspect.Parameter.empty:
return
cls = param.annotation
if not issubclass(cls, self.orm.Base):
return
if ('%s.id' % name) in route.urltpl.variables:
idcol = 'id'
else:
table = cls.__table__
for var in route.urltpl.variables:
match = re.match('%s\.([^.]+)$' % name, var)
if not match:
continue
col = match.group(1)
# TODO: handle the case where the column is part of a parent
# table
if table.columns.get(col).unique:
idcol = col
break
else:
return
if not test_redirect:
for var in route.urltpl.variables:
if var == '%s.%s' % (name, idcol):
continue
if var.startswith('%s.' % name):
test_redirect = True
break
param2clsid[name] = (cls, idcol)
if not param2clsid:
return
def match2vars(ctx, matches):
result = {}
for var, (cls, idcol) in param2clsid.items():
id = matches['%s.%s' % (var, idcol)]
result[name] = self.orm.get_session(ctx).query(cls).\
filter(getattr(cls, idcol) == id).\
first()
if result[name] is None:
return
if test_redirect and ctx.http.request.method == 'GET':
realpath = urllib.parse.unquote(
route.url(ctx, _relative=True, **result))
if urllib.parse.unquote(ctx.http.request.path) != realpath:
# need to create the url a second time to incorporate the
# query string
ctx.http.redirect(route.url(
ctx, _query=ctx.http.request.GET, **result))
return result
return match2vars
[docs] def url(self, ctx, route, *args, **kwargs):
"""
Shortcut for ``route(route).url(ctx, *args, **kwargs)``.
"""
return self.route(route).url(ctx, *args, **kwargs)
def get_serve_runners(self):
if not hasattr(self, '_serve_runners'):
import score.serve
class Runner(score.serve.SocketServerRunner):
def _mkserver(runner):
from werkzeug.serving import make_server
return make_server(self.host, self.port, self.mkwsgi(),
threaded=self.threaded)
self._serve_runners = [Runner()]
return self._serve_runners
def score_serve_workers(self):
if not hasattr(self, '_score_serve_workers'):
import score.serve
import socket
from werkzeug.serving import BaseWSGIServer
class Server(BaseWSGIServer):
multithread = self.threaded
def server_bind(self):
self.socket.setsockopt(
socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
super().server_bind()
class Worker(score.serve.SocketServerWorker):
def _mkserver(runner):
return Server(self.host, self.port, self.mkwsgi())
self._score_serve_workers = Worker()
return self._score_serve_workers
[docs] def mkwsgi(self):
"""
Creates a WSGI_ application, that will route incoming requests to the
configured routes.
"""
if self.debug:
def app(env, start_response):
response = self.create_response(Request(env))
return response(env, start_response)
from werkzeug.debug import DebuggedApplication
app = DebuggedApplication(app, True)
else:
def app(env, start_response):
try:
request = Request(env)
except Exception as e:
log.critical(e)
response = HTTPInternalServerError()
else:
try:
response = self.create_response(request)
except Exception as e:
log.exception(e)
response = self.create_failsafe_response(request, e)
return response(env, start_response)
return app
def find_route_for(self, request_or_url):
if isinstance(request_or_url, Request):
request = request_or_url
else:
request = Request.blank(request_or_url)
for route in self.routes.values():
if route.can_handle(request):
return route
return None
def find_route_and_args_for(self, request_or_url):
if isinstance(request_or_url, Request):
request = request_or_url
else:
request = Request.blank(request_or_url)
for route in self.routes.values():
result = route.extract_variables(request)
if result is not None:
return route, result
return None, None
def create_ctx_member(self, request):
return Http(self, request)
def create_response(self, request):
ctx = self.ctx.Context()
ctx.http = self.create_ctx_member(request)
try:
log.debug('Received %s request for %s' %
(request.method, request.path))
for name, route in self.routes.items():
if route.handle(ctx):
break
else:
ctx.http.response = self.create_error_response(
ctx, HTTPNotFound())
except Exception as e:
for exc in self.exception_handlers:
# let's see if we have a dedicated exception handler for this
# kind of error
if isinstance(e, exc):
try:
self.exception_handlers[exc](ctx, e)
break
except HTTPException as response:
ctx.http.response = response
break
except Exception as e2:
ctx.destroy(e2)
raise
else:
ctx.destroy(e)
raise
response = ctx.http.response
ctx.destroy()
return response
def create_failsafe_response(self, request, error=None):
try:
with self.ctx.Context() as ctx:
ctx.tx.doom()
ctx.http = Http(self, request)
ctx.http.exc = ctx.http.exception = error
response = self.create_error_response(ctx, error)
return response
except Exception as e:
if ctx._active:
try:
ctx.destroy(e)
except Exception:
log.exception(e)
pass
log.critical(e)
return HTTPInternalServerError()
finally:
assert not ctx or not ctx._active
def create_error_response(self, ctx, error):
code = 500
if isinstance(error, HTTPException):
code = error.code
ctx.http.response = ctx.http.res = error
else:
ctx.http.response = ctx.http.res = HTTPInternalServerError()
handler = None
if str(code) in self.error_handlers:
handler = self.error_handlers[str(code)]
elif '%dXX' % (code % 100) in self.error_handlers:
handler = self.error_handlers['%dXX' % (code % 100)]
if not handler:
return ctx.http.response
try:
result = handler(ctx, error)
except HTTPException as response:
result = response
if isinstance(result, Response):
ctx.http.response = result
return result
if isinstance(result, str):
ctx.http.response.text = result
return ctx.http.response
class Http:
def __init__(self, conf, request):
self._conf = conf
self._response = None
self.req = self.request = request
self.url = conf.url
def redirect(self, url, permanent=False):
if not permanent:
raise HTTPFound(location=url)
else:
raise HTTPMovedPermanently(location=url)
@property
def response(self):
if self._response is None:
self._response = Response(conditional_response=True)
self._response.charset = 'utf-8'
return self._response
@response.setter
def response(self, value):
self._response = value
res = response