Source code for kobin.routes

"""
Routing
=======

Kobin's routing system may be slightly distinctive.

Rule Syntax
-----------

Kobin use decorator based URL dispatch.

* Dynamic convert URL variables from Type Hints.

.. code-block:: python

   from kobin import Kobin, Response, RedirectResponse
   app = Kobin()

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

   @app.route('/users/{user_id}')
   def index(user_id: str) -> Response:
       return Response('User List')


Reverse Routing
---------------

`app.router.reverse` function returns URL.
The usage is like this:

.. code-block:: python

   from kobin import Kobin, Response
   app = Kobin()

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

   @app.route('/users/{user_id}', 'user-detail')
   def user_detail(user_id: int) -> Response:
       return Response('Hello User{}'.format(user_id))

   print(app.router.reverse('top-page'))
   # http://hostname/

   print(app.router.reverse('user-detail', user_id=1))
   # http://hostname/users/1


Reverse Routing and Redirecting
-------------------------------

:class:`RedirectResponse`
The usage is like this:

.. code-block:: python

   from kobin import Kobin, Response, RedirectResponse
   app = Kobin()

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

   @app.route('/404')
   def user_detail() -> Response:
       top_url = app.router.reverse('top-page')
       return RedirectResponse(top_url)

"""
from typing import get_type_hints
from .responses import HTTPError


def split_by_slash(path):
    stripped_path = path.lstrip('/').rstrip('/')
    return stripped_path.split('/')


[docs]def match_url_vars_type(url_vars, type_hints): """ Match types of url vars. >>> match_url_vars_type({'user_id': '1'}, {'user_id': int}) (True, {'user_id': 1}) >>> match_url_vars_type({'user_id': 'foo'}, {'user_id': int}) (False, {}) """ typed_url_vars = {} try: for k, v in url_vars.items(): arg_type = type_hints.get(k) if arg_type and arg_type != str: typed_url_vars[k] = arg_type(v) else: typed_url_vars[k] = v except ValueError: return False, {} return True, typed_url_vars
[docs]def match_path(rule, path): """ Match path. >>> match_path('/foo', '/foo') (True, {}) >>> match_path('/foo', '/bar') (False, {}) >>> match_path('/users/{user_id}', '/users/1') (True, {'user_id': '1'}) >>> match_path('/users/{user_id}', '/users/not-integer') (True, {'user_id': 'not-integer'}) """ split_rule = split_by_slash(rule) split_path = split_by_slash(path) url_vars = {} if len(split_rule) != len(split_path): return False, {} for r, p in zip(split_rule, split_path): if r.startswith('{') and r.endswith('}'): url_vars[r[1:-1]] = p continue if r != p: return False, {} return True, url_vars
class Router: def __init__(self) -> None: self.endpoints = [] def match(self, path, method): """ Get callback and url_vars. >>> from kobin import Response >>> r = Router() >>> def view(user_id: int) -> Response: ... return Response(f'You are {user_id}') ... >>> r.add('/users/{user_id}', 'GET', 'user-detail', view) >>> callback, url_vars = r.match('/users/1', 'GET') >>> url_vars {'user_id': 1} >>> response = callback(**url_vars) >>> response.body [b'You are 1'] >>> callback, url_vars = r.match('/notfound', 'GET') Traceback (most recent call last): ... kobin.responses.HTTPError """ if path != '/': path = path.rstrip('/') method = method.upper() status = 404 for p, n, m in self.endpoints: matched, url_vars = match_path(p, path) if not matched: # path: not matched continue if method not in m: # path: matched, method: not matched status = 405 raise HTTPError(status=status, body=f'Method not found: {path} {method}') # it has security issue?? callback, type_hints = m[method] type_matched, typed_url_vars = match_url_vars_type(url_vars, type_hints) if not type_matched: continue # path: not matched (types are different) return callback, typed_url_vars raise HTTPError(status=status, body=f'Not found: {path}') def add(self, rule, method, name, callback): """ Add a new rule or replace the target for an existing rule. >>> from kobin import Response >>> r = Router() >>> def view(user_id: int) -> Response: ... return Response(f'You are {user_id}') ... >>> r.add('/users/{user_id}', 'GET', 'user-detail', view) >>> path, name, methods = r.endpoints[0] >>> path '/users/{user_id}' >>> name 'user-detail' >>> callback, type_hints = methods['GET'] >>> view == callback True >>> type_hints['user_id'] == int True """ if rule != '/': rule = rule.rstrip('/') method = method.upper() for i, e in enumerate(self.endpoints): r, n, callbacks = e if r == rule: assert name == n and n is not None, ( "A same path should set a same name for reverse routing." ) callbacks[method] = (callback, get_type_hints(callback)) self.endpoints[i] = (r, name, callbacks) break else: e = (rule, name, {method: (callback, get_type_hints(callback))}) self.endpoints.append(e) def reverse(self, name, **kwargs): """ Reverse routing. >>> from kobin import Response >>> r = Router() >>> def view(user_id: int) -> Response: ... return Response(f'You are {user_id}') ... >>> r.add('/users/{user_id}', 'GET', 'user-detail', view) >>> r.reverse('user-detail', user_id=1) '/users/1' """ for p, n, _ in self.endpoints: if name == n: return p.format(**kwargs)