Source code for doctor.routing

import functools
import inspect
from typing import Any, Callable, List, Sequence, Tuple

from doctor.utils import copy_func, get_params_from_func, get_valid_class_name


[docs]class HTTPMethod(object): """Represents and HTTP method and it's configuration. When instantiated the logic attribute will have 3 attributes added to it: - `_doctor_allowed_exceptions` - A list of excpetions that are allowed to be re-reaised if encountered during a request. - `_doctor_params` - A :class:`~doctor.utils.Params` instance. - `_doctor_signature` - The parsed function Signature. - `_doctor_title` - The title that should be used in api documentation. :param method: The HTTP method. One of: (delete, get, post, put). :param logic: The logic function to be called for the http method. :param allowed_exceptions: If specified, these exception classes will be re-raised instead of turning them into 500 errors. :param title: An optional title for the http method. This will be used when generating api documentation. :param req_obj_type: A doctor :class:`~doctor.types.Object` type that the request body should be converted to. """ def __init__(self, method: str, logic: Callable, allowed_exceptions: List = None, title: str = None, req_obj_type: Callable = None): self.method = method logic = copy_func(logic) # Add doctor attributes to logic. We do a check to ensure some # attributes aren't already set in the event that # doctor.utils.add_param_annotations was used to add additional # request parameters to the logic function that aren't part of it's # signature. logic._doctor_req_obj_type = req_obj_type if not hasattr(logic, '_doctor_signature'): logic._doctor_signature = inspect.signature(logic) if not hasattr(logic, '_doctor_params'): logic._doctor_params = get_params_from_func(logic) logic._doctor_allowed_exceptions = allowed_exceptions logic._doctor_title = title self.logic = logic
[docs]def delete(func: Callable, allowed_exceptions: List = None, title: str = None, req_obj_type: Callable = None) -> HTTPMethod: """Returns a HTTPMethod instance to create a DELETE route. :see: :class:`~doctor.routing.HTTPMethod` """ return HTTPMethod('delete', func, allowed_exceptions=allowed_exceptions, title=title, req_obj_type=req_obj_type)
[docs]def get(func: Callable, allowed_exceptions: List = None, title: str = None, req_obj_type: Callable = None) -> HTTPMethod: """Returns a HTTPMethod instance to create a GET route. :see: :class:`~doctor.routing.HTTPMethod` """ return HTTPMethod('get', func, allowed_exceptions=allowed_exceptions, title=title, req_obj_type=req_obj_type)
[docs]def post(func: Callable, allowed_exceptions: List = None, title: str = None, req_obj_type: Callable = None) -> HTTPMethod: """Returns a HTTPMethod instance to create a POST route. :see: :class:`~doctor.routing.HTTPMethod` """ return HTTPMethod('post', func, allowed_exceptions=allowed_exceptions, title=title, req_obj_type=req_obj_type)
[docs]def put(func: Callable, allowed_exceptions: List = None, title: str = None, req_obj_type: Callable = None) -> HTTPMethod: """Returns a HTTPMethod instance to create a PUT route. :see: :class:`~doctor.routing.HTTPMethod` """ return HTTPMethod('put', func, allowed_exceptions=allowed_exceptions, title=title, req_obj_type=req_obj_type)
[docs]def create_http_method(logic: Callable, http_method: str, handle_http: Callable, before: Callable = None, after: Callable = None) -> Callable: """Create a handler method to be used in a handler class. :param callable logic: The underlying function to execute with the parsed and validated parameters. :param str http_method: HTTP method this will handle. :param handle_http: The HTTP handler function that should be used to wrap the logic functions. :param before: A function to be called before the logic function associated with the route. :param after: A function to be called after the logic function associated with the route. :returns: A handler function. """ @functools.wraps(logic) def fn(handler, *args, **kwargs): if before is not None and callable(before): before() result = handle_http(handler, args, kwargs, logic) if after is not None and callable(after): after(result) return result return fn
[docs]class Route(object): """Represents a route. :param route: The route path, e.g. `r'^/foo/<int:foo_id>/?$'` :param methods: A tuple of defined HTTPMethods for the route. :param heading: An optional heading that this route should be grouped under in the api documentation. :param base_handler_class: The base handler class to use. :param handler_name: The name that should be given to the handler class. :param before: A function to be called before the logic function associated with the route. :param after: A function to be called after the logic function associated with the route. """ def __init__(self, route: str, methods: Sequence[HTTPMethod], heading: str = 'API', base_handler_class = None, handler_name: str = None, before: Callable = None, after: Callable = None): self.after = after self.base_handler_class = base_handler_class self.before = before self.handler_name = handler_name self.heading = heading self.methods = methods self.route = route
[docs]def get_handler_name(route: Route, logic: Callable) -> str: """Gets the handler name. :param route: A Route instance. :param logic: The logic function. :returns: A handler class name. """ if route.handler_name is not None: return route.handler_name if any(m for m in route.methods if m.method.lower() == 'post'): # A list endpoint if route.heading != 'API': return '{}ListHandler'.format(get_valid_class_name(route.heading)) return '{}ListHandler'.format(get_valid_class_name(logic.__name__)) if route.heading != 'API': return '{}Handler'.format(get_valid_class_name(route.heading)) return '{}Handler'.format(get_valid_class_name(logic.__name__))
[docs]def create_routes(routes: Sequence[HTTPMethod], handle_http: Callable, default_base_handler_class: Any) -> List[Tuple[str, Any]]: """Creates handler routes from the provided routes. :param routes: A tuple containing the route and another tuple with all http methods allowed for the route. :param handle_http: The HTTP handler function that should be used to wrap the logic functions. :param default_base_handler_class: The default base handler class that should be used. :returns: A list of tuples containing the route and generated handler. """ created_routes = [] all_handler_names = [] for r in routes: handler = None if r.base_handler_class is not None: base_handler_class = r.base_handler_class else: base_handler_class = default_base_handler_class # Define the handler name. To prevent issues where auto-generated # handler names conflict with existing, appending a number to the # end of the hanlder name if it already exists. handler_name = get_handler_name(r, r.methods[0].logic) if handler_name in all_handler_names: handler_name = '{}{}'.format( handler_name, len(all_handler_names)) all_handler_names.append(handler_name) for method in r.methods: logic = method.logic http_method = method.method http_func = create_http_method(logic, http_method, handle_http, before=r.before, after=r.after) handler_methods_and_properties = { '__name__': handler_name, '_doctor_heading': r.heading, 'methods': set([http_method.upper()]), http_method: http_func, } if handler is None: handler = type( handler_name, (base_handler_class,), handler_methods_and_properties) else: setattr(handler, http_method, http_func) # This is specific to Flask. Its MethodView class # initializes the methods attribute in __new__ so we # need to add all the other http methods we are defining # on the handler after it gets created by type. if hasattr(handler, 'methods'): handler.methods.add(http_method.upper()) created_routes.append((r.route, handler)) return created_routes