sanic/sanic/router.py
2016-10-16 08:01:59 -05:00

147 lines
4.9 KiB
Python

import re
from collections import namedtuple
from .exceptions import NotFound, InvalidUsage
Route = namedtuple("Route", ['handler', 'methods', 'pattern', 'parameters'])
Parameter = namedtuple("Parameter", ['name', 'cast'])
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...
Parameters will be passed as keyword arguments to the request handling
function provided Parameters can also have a type by appending :type to
the <parameter>. If no type is provided, a string is expected. A regular
expression can also be passed in as the type
TODO:
This probably needs optimization for larger sets of routes,
since it checks every route until it finds a match which is bad and
I should feel bad
"""
routes = None
regex_types = {
"string": (None, "\w+"),
"int": (int, "\d+"),
"number": (float, "[0-9\\.]+"),
"alpha": (None, "[A-Za-z]+"),
}
def __init__(self):
self.routes = []
def add(self, uri, methods, handler):
"""
Adds a handler to the route list
:param uri: Path to match
: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.
:return: Nothing
"""
# Dict for faster lookups of if method allowed
methods_dict = None
if methods:
methods_dict = {method: True for method in methods}
parameters = []
def add_parameter(match):
# We could receive NAME or NAME:PATTERN
parts = match.group(1).split(':')
if len(parts) == 2:
parameter_name, parameter_pattern = parts
else:
parameter_name = parts[0]
parameter_pattern = 'string'
# Pull from pre-configured types
parameter_regex = self.regex_types.get(parameter_pattern)
if parameter_regex:
parameter_type, parameter_pattern = parameter_regex
else:
parameter_type = None
parameter = Parameter(name=parameter_name, cast=parameter_type)
parameters.append(parameter)
return "({})".format(parameter_pattern)
pattern_string = re.sub("<(.+?)>", add_parameter, uri)
pattern = re.compile("^{}$".format(pattern_string))
route = Route(
handler=handler, methods=methods_dict, pattern=pattern,
parameters=parameters)
self.routes.append(route)
def get(self, request):
"""
Gets a request handler based on the URL of the request, or raises an
error
:param request: Request object
:return: handler, arguments, keyword arguments
"""
route = None
args = []
kwargs = {}
for _route in self.routes:
match = _route.pattern.match(request.url)
if match:
for index, parameter in enumerate(_route.parameters, start=1):
value = match.group(index)
if parameter.cast:
kwargs[parameter.name] = parameter.cast(value)
else:
kwargs[parameter.name] = value
route = _route
break
if route:
if route.methods and request.method not in route.methods:
raise InvalidUsage(
"Method {} not allowed for URL {}".format(
request.method, request.url), status_code=405)
return route.handler, args, kwargs
else:
raise NotFound("Requested URL {} not found".format(request.url))
class SimpleRouter:
"""
Simple router records and reads all routes from a dictionary
It does not support parameters in routes, but is very fast
"""
routes = None
def __init__(self):
self.routes = {}
def add(self, uri, methods, handler):
# Dict for faster lookups of method allowed
methods_dict = None
if methods:
methods_dict = {method: True for method in methods}
self.routes[uri] = Route(
handler=handler, methods=methods_dict, pattern=uri,
parameters=None)
def get(self, request):
route = self.routes.get(request.url)
if route:
if route.methods and request.method not in route.methods:
raise InvalidUsage(
"Method {} not allowed for URL {}".format(
request.method, request.url), status_code=405)
return route.handler, [], {}
else:
raise NotFound("Requested URL {} not found".format(request.url))