forked from adafruit/Adafruit_CircuitPython_HTTPServer
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathroute.py
197 lines (141 loc) · 6.02 KB
/
route.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# 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`."""
@staticmethod
def _prepare_path_pattern(path: str, append_slash: bool) -> str:
# Escape all dots
path = re.sub(r"\.", r"\\.", path)
# Replace url parameters with regex groups
path = re.sub(r"<\w+>", r"([^/]+)", path)
# Replace wildcards with corresponding regex
path = path.replace(r"\.\.\.\.", r".+").replace(r"\.\.\.", r"[^/]+")
# Add optional slash at the end if append_slash is True
if append_slash:
path += r"/?"
# Add start and end of string anchors
return f"^{path}$"
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.path = path
self.methods = (
set(methods) if isinstance(methods, (set, list, tuple)) else set([methods])
)
self.handler = handler
self.parameters_names = [
name[1:-1] for name in re.compile(r"/[^<>]*/?").split(path) if name != ""
]
self.path_pattern = re.compile(self._prepare_path_pattern(path, append_slash))
@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 given ``method`` and ``path``.
If the route contains parameters, it will check if the ``path`` contains values for
them.
Returns tuple of a boolean that indicates if the routes matches and a dict containing
values for url parameters.
If the route does not match ``path`` or ``method`` if will return ``None`` instead of dict.
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