sanic/sanic/router.py

158 lines
5.3 KiB
Python
Raw Normal View History

2016-10-15 20:59:00 +01:00
import re
2016-10-20 06:07:16 +01:00
from collections import defaultdict, namedtuple
2016-10-20 05:07:07 +01:00
from functools import lru_cache
from .config import Config
2016-10-15 20:59:00 +01:00
from .exceptions import NotFound, InvalidUsage
2016-10-20 04:56:23 +01:00
Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters'])
Parameter = namedtuple('Parameter', ['name', 'cast'])
2016-10-15 20:59:00 +01:00
2016-10-20 04:43:31 +01:00
REGEX_TYPES = {
2016-10-20 05:05:55 +01:00
'string': (str, r'[^/]+'),
2016-10-20 04:56:23 +01:00
'int': (int, r'\d+'),
'number': (float, r'[0-9\\.]+'),
2016-10-20 05:05:55 +01:00
'alpha': (str, r'[A-Za-z]+'),
2016-10-20 04:43:31 +01:00
}
2016-10-15 20:59:00 +01:00
2016-10-20 06:07:16 +01:00
def url_hash(url):
return url.count('/')
class RouteExists(Exception):
pass
2016-10-20 06:07:16 +01:00
2016-10-15 20:59:00 +01:00
class Router:
"""
Router supports basic routing with parameters and method checks
Usage:
@sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...])
def my_route(request, my_parameter):
do stuff...
2016-11-28 19:00:39 +00:00
or
2016-11-28 19:05:47 +00:00
@sanic.route('/my/url/<my_paramter>:type', methods['GET', 'POST', ...])
2016-11-28 19:00:39 +00:00
def my_route_with_type(request, my_parameter):
do stuff...
2016-10-15 20:59:00 +01:00
2016-10-16 14:01:59 +01:00
Parameters will be passed as keyword arguments to the request handling
2016-11-28 19:00:39 +00:00
function. Provided parameters can also have a type by appending :type to
the <parameter>. Given parameter must be able to be type-casted to this.
If no type is provided, a string is expected. A regular expression can
also be passed in as the type. The argument given to the function will
always be a string, independent of the type.
2016-10-15 20:59:00 +01:00
"""
routes_static = None
routes_dynamic = None
routes_always_check = None
2016-10-15 20:59:00 +01:00
def __init__(self):
self.routes_all = {}
self.routes_static = {}
self.routes_dynamic = defaultdict(list)
self.routes_always_check = []
2016-10-15 20:59:00 +01:00
def add(self, uri, methods, handler):
"""
Adds a handler to the route list
:param uri: Path to match
2016-10-16 14:01:59 +01:00
:param methods: Array of accepted method names.
If none are provided, any method is allowed
:param handler: Request handler function.
When executed, it should provide a response object.
2016-10-15 20:59:00 +01:00
:return: Nothing
"""
if uri in self.routes_all:
raise RouteExists("Route already registered: {}".format(uri))
2016-10-15 20:59:00 +01:00
# Dict for faster lookups of if method allowed
2016-10-16 14:01:59 +01:00
if methods:
2016-10-20 05:16:16 +01:00
methods = frozenset(methods)
2016-10-15 20:59:00 +01:00
parameters = []
properties = {"unhashable": None}
2016-10-15 20:59:00 +01:00
def add_parameter(match):
# We could receive NAME or NAME:PATTERN
2016-10-20 06:33:59 +01:00
name = match.group(1)
pattern = 'string'
if ':' in name:
name, pattern = name.split(':', 1)
2016-10-15 20:59:00 +01:00
2016-10-20 06:33:59 +01:00
default = (str, pattern)
2016-10-15 20:59:00 +01:00
# Pull from pre-configured types
2016-10-20 06:33:59 +01:00
_type, pattern = REGEX_TYPES.get(pattern, default)
parameter = Parameter(name=name, cast=_type)
parameters.append(parameter)
# Mark the whole route as unhashable if it has the hash key in it
if re.search('(^|[^^]){1}/', pattern):
properties['unhashable'] = True
# Mark the route as unhashable if it matches the hash key
elif re.search(pattern, '/'):
properties['unhashable'] = True
2016-10-20 06:33:59 +01:00
return '({})'.format(pattern)
2016-10-15 20:59:00 +01:00
2016-10-20 04:56:23 +01:00
pattern_string = re.sub(r'<(.+?)>', add_parameter, uri)
pattern = re.compile(r'^{}$'.format(pattern_string))
2016-10-15 20:59:00 +01:00
2016-10-16 14:01:59 +01:00
route = Route(
2016-10-20 05:16:16 +01:00
handler=handler, methods=methods, pattern=pattern,
2016-10-16 14:01:59 +01:00
parameters=parameters)
2016-10-20 06:07:16 +01:00
self.routes_all[uri] = route
if properties['unhashable']:
self.routes_always_check.append(route)
elif parameters:
self.routes_dynamic[url_hash(uri)].append(route)
else:
self.routes_static[uri] = route
2016-10-15 20:59:00 +01:00
def get(self, request):
"""
2016-10-16 14:01:59 +01:00
Gets a request handler based on the URL of the request, or raises an
error
2016-10-15 20:59:00 +01:00
:param request: Request object
:return: handler, arguments, keyword arguments
"""
return self._get(request.url, request.method)
@lru_cache(maxsize=Config.ROUTER_CACHE_SIZE)
def _get(self, url, method):
"""
Gets a request handler based on the URL of the request, or raises an
error. Internal method for caching.
:param url: Request URL
:param method: Request method
:return: handler, arguments, keyword arguments
"""
# Check against known static routes
route = self.routes_static.get(url)
if route:
2016-10-20 06:07:16 +01:00
match = route.pattern.match(url)
2016-10-15 20:59:00 +01:00
else:
# Move on to testing all regex routes
for route in self.routes_dynamic[url_hash(url)]:
2016-10-20 06:07:16 +01:00
match = route.pattern.match(url)
if match:
break
else:
# Lastly, check against all regex routes that cannot be hashed
for route in self.routes_always_check:
match = route.pattern.match(url)
if match:
break
else:
raise NotFound('Requested URL {} not found'.format(url))
if route.methods and method not in route.methods:
2016-10-20 05:05:55 +01:00
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
method, url), status_code=405)
2016-10-20 05:05:55 +01:00
2016-10-20 06:33:59 +01:00
kwargs = {p.name: p.cast(value)
for value, p
in zip(match.groups(1), route.parameters)}
2016-10-20 05:05:55 +01:00
return route.handler, [], kwargs