185 lines
5.6 KiB
Python
185 lines
5.6 KiB
Python
# SPDX-FileCopyrightText: Copyright (c) 2022 Dan Halbert for Adafruit Industries, Michał Pokusa
|
|
#
|
|
# SPDX-License-Identifier: MIT
|
|
"""
|
|
`adafruit_httpserver.route`
|
|
====================================================
|
|
* Author(s): Dan Halbert, Michał Pokusa
|
|
"""
|
|
|
|
try:
|
|
from typing import Callable, Iterable, Union, Tuple, Literal, Dict, TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from .response import Response
|
|
except ImportError:
|
|
pass
|
|
|
|
import re
|
|
|
|
from .methods import GET
|
|
|
|
|
|
class Route:
|
|
"""Route definition for different paths, see `adafruit_httpserver.server.Server.route`."""
|
|
|
|
def __init__(
|
|
self,
|
|
path: str = "",
|
|
methods: Union[str, Iterable[str]] = GET,
|
|
handler: Callable = None,
|
|
*,
|
|
append_slash: bool = False,
|
|
) -> None:
|
|
self._validate_path(path, append_slash)
|
|
|
|
self.parameters_names = [
|
|
name[1:-1] for name in re.compile(r"/[^<>]*/?").split(path) if name != ""
|
|
]
|
|
self.path_pattern = re.compile(
|
|
r"^"
|
|
+ re.sub(r"<\w+>", r"([^/]+)", path)
|
|
.replace(r"....", r".+")
|
|
.replace(r"...", r"[^/]+")
|
|
+ (r"/?" if append_slash else r"")
|
|
+ r"$"
|
|
)
|
|
self.path = path
|
|
self.methods = (
|
|
set(methods) if isinstance(methods, (set, list, tuple)) else set([methods])
|
|
)
|
|
self.handler = handler
|
|
|
|
@staticmethod
|
|
def _validate_path(path: str, append_slash: bool) -> None:
|
|
if not path.startswith("/"):
|
|
raise ValueError("Path must start with a slash.")
|
|
|
|
if path.endswith("/") and append_slash:
|
|
raise ValueError("Cannot use append_slash=True when path ends with /")
|
|
|
|
if "//" in path:
|
|
raise ValueError("Path cannot contain double slashes.")
|
|
|
|
if "<>" in path:
|
|
raise ValueError("All URL parameters must be named.")
|
|
|
|
if re.search(r"[^/]<[^/]+>|<[^/]+>[^/]", path):
|
|
raise ValueError("All URL parameters must be between slashes.")
|
|
|
|
if re.search(r"[^/.]\.\.\.\.?|\.?\.\.\.[^/.]", path):
|
|
raise ValueError("... and .... must be between slashes")
|
|
|
|
if "....." in path:
|
|
raise ValueError("Path cannot contain more than 4 dots in a row.")
|
|
|
|
def matches(
|
|
self, method: str, path: str
|
|
) -> Union[Tuple[Literal[False], None], Tuple[Literal[True], Dict[str, str]]]:
|
|
"""
|
|
Checks if the route matches the other route.
|
|
|
|
If the route contains parameters, it will check if the ``other`` route contains values for
|
|
them.
|
|
|
|
Returns tuple of a boolean and a list of strings. The boolean indicates if the routes match,
|
|
and the list contains the values of the url parameters from the ``other`` route.
|
|
|
|
Examples::
|
|
|
|
route = Route("/example", GET, append_slash=True)
|
|
|
|
route.matches(GET, "/example") # True, {}
|
|
route.matches(GET, "/example/") # True, {}
|
|
|
|
route.matches(GET, "/other-example") # False, None
|
|
route.matches(POST, "/example/") # False, None
|
|
|
|
...
|
|
|
|
route = Route("/example/<parameter>", GET)
|
|
|
|
route.matches(GET, "/example/123") # True, {"parameter": "123"}
|
|
|
|
route.matches(GET, "/other-example") # False, None
|
|
|
|
...
|
|
|
|
route = Route("/example/.../something", GET)
|
|
route.matches(GET, "/example/123/something") # True, {}
|
|
|
|
route = Route("/example/..../something", GET)
|
|
route.matches(GET, "/example/123/456/something") # True, {}
|
|
"""
|
|
|
|
if method not in self.methods:
|
|
return False, None
|
|
|
|
path_match = self.path_pattern.match(path)
|
|
if path_match is None:
|
|
return False, None
|
|
|
|
url_parameters_values = path_match.groups()
|
|
|
|
return True, dict(zip(self.parameters_names, url_parameters_values))
|
|
|
|
def __repr__(self) -> str:
|
|
path = repr(self.path)
|
|
methods = repr(self.methods)
|
|
handler = repr(self.handler)
|
|
|
|
return f"Route({path=}, {methods=}, {handler=})"
|
|
|
|
|
|
def as_route(
|
|
path: str,
|
|
methods: Union[str, Iterable[str]] = GET,
|
|
*,
|
|
append_slash: bool = False,
|
|
) -> "Callable[[Callable[..., Response]], Route]":
|
|
"""
|
|
Decorator used to convert a function into a ``Route`` object.
|
|
|
|
``as_route`` can be only used once per function, because it replaces the function with
|
|
a ``Route`` object that has the same name as the function.
|
|
|
|
Later it can be imported and registered in the ``Server``.
|
|
|
|
:param str path: URL path
|
|
:param str methods: HTTP method(s): ``"GET"``, ``"POST"``, ``["GET", "POST"]`` etc.
|
|
:param bool append_slash: If True, the route will be accessible with and without a
|
|
trailing slash
|
|
|
|
Example::
|
|
|
|
# Converts a function into a Route object
|
|
@as_route("/example")
|
|
def some_func(request):
|
|
...
|
|
|
|
some_func # Route(path="/example", methods={"GET"}, handler=<function some_func at 0x...>)
|
|
|
|
# WRONG: as_route can be used only once per function
|
|
@as_route("/wrong-example1")
|
|
@as_route("/wrong-example2")
|
|
def wrong_func2(request):
|
|
...
|
|
|
|
# If a route is in another file, you can import it and register it to the server
|
|
|
|
from .routes import some_func
|
|
|
|
...
|
|
|
|
server.add_routes([
|
|
some_func,
|
|
])
|
|
"""
|
|
|
|
def route_decorator(func: Callable) -> Route:
|
|
if isinstance(func, Route):
|
|
raise ValueError("as_route can be used only once per function.")
|
|
|
|
return Route(path, methods, func, append_slash=append_slash)
|
|
|
|
return route_decorator
|