2021-08-30 18:04:44 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2016-10-20 05:07:07 +01:00
|
|
|
from functools import lru_cache
|
2021-08-30 18:04:44 +01:00
|
|
|
from inspect import signature
|
2021-02-15 15:20:07 +00:00
|
|
|
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
2021-08-30 18:04:44 +01:00
|
|
|
from uuid import UUID
|
2016-12-25 09:43:45 +00:00
|
|
|
|
2022-06-27 09:19:26 +01:00
|
|
|
from sanic_routing import BaseRouter
|
|
|
|
from sanic_routing.exceptions import NoMethod
|
|
|
|
from sanic_routing.exceptions import NotFound as RoutingNotFound
|
|
|
|
from sanic_routing.route import Route
|
2016-12-25 09:43:45 +00:00
|
|
|
|
2021-01-26 06:47:16 +00:00
|
|
|
from sanic.constants import HTTP_METHODS
|
2021-09-29 21:53:49 +01:00
|
|
|
from sanic.errorpages import check_error_format
|
2022-05-12 18:39:35 +01:00
|
|
|
from sanic.exceptions import MethodNotAllowed, NotFound, SanicException
|
2021-03-07 12:54:45 +00:00
|
|
|
from sanic.models.handler_types import RouteHandler
|
2016-12-25 09:43:45 +00:00
|
|
|
|
2016-10-15 20:59:00 +01:00
|
|
|
|
2021-02-09 10:25:08 +00:00
|
|
|
ROUTER_CACHE_SIZE = 1024
|
2021-03-01 13:30:52 +00:00
|
|
|
ALLOWED_LABELS = ("__file_uri__",)
|
2021-02-09 10:25:08 +00:00
|
|
|
|
|
|
|
|
2021-01-26 06:47:16 +00:00
|
|
|
class Router(BaseRouter):
|
2021-01-31 10:30:37 +00:00
|
|
|
"""
|
|
|
|
The router implementation responsible for routing a :class:`Request` object
|
|
|
|
to the appropriate handler.
|
|
|
|
"""
|
|
|
|
|
2021-01-26 06:47:16 +00:00
|
|
|
DEFAULT_METHOD = "GET"
|
|
|
|
ALLOWED_METHODS = HTTP_METHODS
|
2018-10-14 01:55:33 +01:00
|
|
|
|
2021-02-15 15:20:07 +00:00
|
|
|
def _get(
|
2021-03-21 07:47:21 +00:00
|
|
|
self, path: str, method: str, host: Optional[str]
|
2021-03-01 13:30:52 +00:00
|
|
|
) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
|
2021-02-01 07:56:58 +00:00
|
|
|
try:
|
2021-03-21 07:47:21 +00:00
|
|
|
return self.resolve(
|
2021-02-09 10:25:08 +00:00
|
|
|
path=path,
|
|
|
|
method=method,
|
2021-04-19 22:53:42 +01:00
|
|
|
extra={"host": host} if host else None,
|
2021-02-01 07:56:58 +00:00
|
|
|
)
|
|
|
|
except RoutingNotFound as e:
|
2023-03-06 19:24:12 +00:00
|
|
|
raise NotFound(f"Requested URL {e.path} not found") from None
|
2021-02-01 07:56:58 +00:00
|
|
|
except NoMethod as e:
|
2022-05-12 18:39:35 +01:00
|
|
|
raise MethodNotAllowed(
|
2023-03-06 19:24:12 +00:00
|
|
|
f"Method {method} not allowed for URL {path}",
|
2021-02-09 10:25:08 +00:00
|
|
|
method=method,
|
2023-03-26 13:24:08 +01:00
|
|
|
allowed_methods=tuple(e.allowed_methods)
|
|
|
|
if e.allowed_methods
|
|
|
|
else None,
|
2023-03-06 19:24:12 +00:00
|
|
|
) from None
|
2017-02-02 17:21:14 +00:00
|
|
|
|
2021-03-21 07:47:21 +00:00
|
|
|
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
2021-03-01 13:30:52 +00:00
|
|
|
def get( # type: ignore
|
2021-03-21 07:47:21 +00:00
|
|
|
self, path: str, method: str, host: Optional[str]
|
2021-03-01 13:30:52 +00:00
|
|
|
) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
|
2021-01-31 10:30:37 +00:00
|
|
|
"""
|
2021-12-06 07:17:01 +00:00
|
|
|
Retrieve a `Route` object containing the details about how to handle
|
2021-01-31 10:30:37 +00:00
|
|
|
a response for a given request
|
|
|
|
|
|
|
|
:param request: the incoming request object
|
|
|
|
:type request: Request
|
|
|
|
:return: details needed for handling the request and returning the
|
|
|
|
correct response
|
2021-03-01 13:30:52 +00:00
|
|
|
:rtype: Tuple[ Route, RouteHandler, Dict[str, Any]]
|
2021-01-31 10:30:37 +00:00
|
|
|
"""
|
2023-03-06 19:24:12 +00:00
|
|
|
__tracebackhide__ = True
|
2021-03-21 07:47:21 +00:00
|
|
|
return self._get(path, method, host)
|
2017-02-02 17:21:14 +00:00
|
|
|
|
2021-03-01 13:30:52 +00:00
|
|
|
def add( # type: ignore
|
2018-10-14 01:55:33 +01:00
|
|
|
self,
|
2021-01-31 10:30:37 +00:00
|
|
|
uri: str,
|
|
|
|
methods: Iterable[str],
|
|
|
|
handler: RouteHandler,
|
2021-02-15 08:47:16 +00:00
|
|
|
host: Optional[Union[str, Iterable[str]]] = None,
|
2021-01-31 10:30:37 +00:00
|
|
|
strict_slashes: bool = False,
|
|
|
|
stream: bool = False,
|
|
|
|
ignore_body: bool = False,
|
|
|
|
version: Union[str, float, int] = None,
|
|
|
|
name: Optional[str] = None,
|
2021-02-07 09:38:37 +00:00
|
|
|
unquote: bool = False,
|
|
|
|
static: bool = False,
|
2021-05-19 11:32:40 +01:00
|
|
|
version_prefix: str = "/v",
|
2021-09-29 21:53:49 +01:00
|
|
|
error_format: Optional[str] = None,
|
2021-02-03 20:36:44 +00:00
|
|
|
) -> Union[Route, List[Route]]:
|
2021-01-31 10:30:37 +00:00
|
|
|
"""
|
|
|
|
Add a handler to the router
|
|
|
|
|
|
|
|
:param uri: the path of the route
|
|
|
|
:type uri: str
|
|
|
|
:param methods: the types of HTTP methods that should be attached,
|
|
|
|
example: ``["GET", "POST", "OPTIONS"]``
|
|
|
|
:type methods: Iterable[str]
|
|
|
|
:param handler: the sync or async function to be executed
|
|
|
|
:type handler: RouteHandler
|
|
|
|
:param host: host that the route should be on, defaults to None
|
|
|
|
:type host: Optional[str], optional
|
|
|
|
:param strict_slashes: whether to apply strict slashes, defaults
|
|
|
|
to False
|
|
|
|
:type strict_slashes: bool, optional
|
|
|
|
:param stream: whether to stream the response, defaults to False
|
|
|
|
:type stream: bool, optional
|
|
|
|
:param ignore_body: whether the incoming request body should be read,
|
|
|
|
defaults to False
|
|
|
|
:type ignore_body: bool, optional
|
|
|
|
:param version: a version modifier for the uri, defaults to None
|
|
|
|
:type version: Union[str, float, int], optional
|
|
|
|
:param name: an identifying name of the route, defaults to None
|
|
|
|
:type name: Optional[str], optional
|
|
|
|
:return: the route object
|
|
|
|
:rtype: Route
|
|
|
|
"""
|
2021-01-26 07:24:38 +00:00
|
|
|
if version is not None:
|
|
|
|
version = str(version).strip("/").lstrip("v")
|
2021-05-19 11:32:40 +01:00
|
|
|
uri = "/".join([f"{version_prefix}{version}", uri.lstrip("/")])
|
2021-01-26 07:24:38 +00:00
|
|
|
|
2021-08-30 18:04:44 +01:00
|
|
|
uri = self._normalize(uri, handler)
|
|
|
|
|
2021-02-03 20:36:44 +00:00
|
|
|
params = dict(
|
2021-02-01 07:56:58 +00:00
|
|
|
path=uri,
|
|
|
|
handler=handler,
|
2021-06-16 13:13:55 +01:00
|
|
|
methods=frozenset(map(str, methods)) if methods else None,
|
2021-02-01 07:56:58 +00:00
|
|
|
name=name,
|
|
|
|
strict=strict_slashes,
|
2021-02-07 09:38:37 +00:00
|
|
|
unquote=unquote,
|
2021-01-26 07:24:38 +00:00
|
|
|
)
|
|
|
|
|
2021-02-03 20:36:44 +00:00
|
|
|
if isinstance(host, str):
|
|
|
|
hosts = [host]
|
|
|
|
else:
|
2021-02-15 08:47:16 +00:00
|
|
|
hosts = host or [None] # type: ignore
|
2021-02-03 20:36:44 +00:00
|
|
|
|
|
|
|
routes = []
|
|
|
|
|
|
|
|
for host in hosts:
|
|
|
|
if host:
|
|
|
|
params.update({"requirements": {"host": host}})
|
|
|
|
|
2023-03-26 13:24:08 +01:00
|
|
|
ident = name
|
|
|
|
if len(hosts) > 1:
|
|
|
|
ident = (
|
|
|
|
f"{name}_{host.replace('.', '_')}"
|
|
|
|
if name
|
|
|
|
else "__unnamed__"
|
|
|
|
)
|
|
|
|
|
2021-03-01 13:30:52 +00:00
|
|
|
route = super().add(**params) # type: ignore
|
2023-03-26 13:24:08 +01:00
|
|
|
route.extra.ident = ident
|
2022-09-28 23:07:09 +01:00
|
|
|
route.extra.ignore_body = ignore_body
|
|
|
|
route.extra.stream = stream
|
|
|
|
route.extra.hosts = hosts
|
|
|
|
route.extra.static = static
|
|
|
|
route.extra.error_format = error_format
|
2021-09-29 21:53:49 +01:00
|
|
|
|
2021-11-16 11:07:33 +00:00
|
|
|
if error_format:
|
2022-09-28 23:07:09 +01:00
|
|
|
check_error_format(route.extra.error_format)
|
2021-02-03 20:36:44 +00:00
|
|
|
|
|
|
|
routes.append(route)
|
|
|
|
|
|
|
|
if len(routes) == 1:
|
|
|
|
return routes[0]
|
|
|
|
return routes
|
2016-10-15 20:59:00 +01:00
|
|
|
|
2021-02-09 10:25:08 +00:00
|
|
|
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
2021-02-03 22:42:24 +00:00
|
|
|
def find_route_by_view_name(self, view_name, name=None):
|
|
|
|
"""
|
|
|
|
Find a route in the router based on the specified view name.
|
|
|
|
|
|
|
|
:param view_name: string of view name to search by
|
|
|
|
:param kwargs: additional params, usually for static files
|
|
|
|
:return: tuple containing (uri, Route)
|
|
|
|
"""
|
|
|
|
if not view_name:
|
2021-02-07 09:38:37 +00:00
|
|
|
return None
|
2021-02-03 22:42:24 +00:00
|
|
|
|
2021-02-08 10:18:29 +00:00
|
|
|
route = self.name_index.get(view_name)
|
|
|
|
if not route:
|
|
|
|
full_name = self.ctx.app._generate_name(view_name)
|
|
|
|
route = self.name_index.get(full_name)
|
2021-02-03 22:42:24 +00:00
|
|
|
|
|
|
|
if not route:
|
2021-02-07 09:38:37 +00:00
|
|
|
return None
|
2021-01-26 07:24:38 +00:00
|
|
|
|
|
|
|
return route
|
2021-02-03 22:42:24 +00:00
|
|
|
|
2021-02-07 09:38:37 +00:00
|
|
|
@property
|
|
|
|
def routes_all(self):
|
2021-04-19 22:53:42 +01:00
|
|
|
return {route.parts: route for route in self.routes}
|
2021-02-08 10:18:29 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def routes_static(self):
|
|
|
|
return self.static_routes
|
|
|
|
|
|
|
|
@property
|
|
|
|
def routes_dynamic(self):
|
|
|
|
return self.dynamic_routes
|
|
|
|
|
|
|
|
@property
|
|
|
|
def routes_regex(self):
|
|
|
|
return self.regex_routes
|
2021-03-01 13:30:52 +00:00
|
|
|
|
|
|
|
def finalize(self, *args, **kwargs):
|
|
|
|
super().finalize(*args, **kwargs)
|
|
|
|
|
|
|
|
for route in self.dynamic_routes.values():
|
|
|
|
if any(
|
|
|
|
label.startswith("__") and label not in ALLOWED_LABELS
|
|
|
|
for label in route.labels
|
|
|
|
):
|
|
|
|
raise SanicException(
|
|
|
|
f"Invalid route: {route}. Parameter names cannot use '__'."
|
|
|
|
)
|
2021-08-30 18:04:44 +01:00
|
|
|
|
|
|
|
def _normalize(self, uri: str, handler: RouteHandler) -> str:
|
|
|
|
if "<" not in uri:
|
|
|
|
return uri
|
|
|
|
|
|
|
|
sig = signature(handler)
|
|
|
|
mapping = {
|
|
|
|
param.name: param.annotation.__name__.lower()
|
|
|
|
for param in sig.parameters.values()
|
|
|
|
if param.annotation in (str, int, float, UUID)
|
|
|
|
}
|
|
|
|
|
|
|
|
reconstruction = []
|
|
|
|
for part in uri.split("/"):
|
|
|
|
if part.startswith("<") and ":" not in part:
|
|
|
|
name = part[1:-1]
|
|
|
|
annotation = mapping.get(name)
|
|
|
|
if annotation:
|
|
|
|
part = f"<{name}:{annotation}>"
|
|
|
|
reconstruction.append(part)
|
|
|
|
return "/".join(reconstruction)
|