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