1import re
2from urllib.parse import unquote
3
4RE_URL = re.compile(r'^(?P<scheme>.*)://(?P<path>[^\?]*)(\?(?P<query>.*))?$', flags=re.IGNORECASE)
5
6
7def get_url_params(url):
8    params = re.search(RE_URL, url)
9    query = params.group('query')
10    if query:
11        pairs = list(map(lambda kv: kv.split('='), query.split('&')))
12        query = {k: unquote(v) for k, v in pairs}
13    return {
14        'scheme': params.group('scheme'),
15        'path': params.group('path'),
16        'query': query or None
17    }
18
19
20class Router:
21    """
22    Usage:
23
24    >>> rt = Router()
25    >>>
26    >>> class App:
27    >>>
28    >>>     @rt.route('get/user')
29    >>>     def get_user(self, url_params):
30    >>>         ...
31    >>>         return userObject
32    >>>
33    >>>     def on_request(self, url):
34    >>>         result = rt.dispatch(self, url) # will return userObject for /get/user path
35    """
36
37    _callbacks = None
38
39    def __init__(self):
40        self._callbacks = {}
41
42    def dispatch(self, context, url):
43        url_params = get_url_params(url)
44        try:
45            callback = self._callbacks[url_params['path'].strip('/')]
46        except KeyError as e:
47            raise RouteNotFound('Route not found for path %s' % url_params['path']) from e
48
49        return callback(context, url_params)
50
51    def route(self, path):
52        if not path:
53            raise RoutePathEmpty()
54
55        def decorator(callback):
56            self._callbacks[path.strip('/')] = callback
57            return callback
58
59        return decorator
60
61
62class RouterError(RuntimeError):
63    pass
64
65
66class RoutePathEmpty(RouterError):
67    pass
68
69
70class RouteNotFound(RouterError):
71    pass
72