From 8e2967b293b78c513d354c72d2a1b90c3a828c66 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Sun, 19 Mar 2023 15:02:36 +0000 Subject: [PATCH 01/14] Changed how routes are mapped to handler functions, added _HTTPRoutes Added option to provide regex-like URLs using <> brackets --- adafruit_httpserver/route.py | 67 +++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/adafruit_httpserver/route.py b/adafruit_httpserver/route.py index 78d8c6c..9289236 100644 --- a/adafruit_httpserver/route.py +++ b/adafruit_httpserver/route.py @@ -7,6 +7,13 @@ * Author(s): Dan Halbert, MichaƂ Pokusa """ +try: + from typing import Callable, List, Union +except ImportError: + pass + +import re + from .methods import HTTPMethod @@ -15,14 +22,66 @@ class _HTTPRoute: def __init__(self, path: str = "", method: HTTPMethod = HTTPMethod.GET) -> None: - self.path = path + contains_regex = re.search(r"<\w*>", path) + + self.path = path if not contains_regex else re.sub(r"<\w*>", r"([^/]*)", path) self.method = method + self.regex = contains_regex + + def matches(self, other: "_HTTPRoute") -> bool: + """ + Checks if the route matches the other route. + + If the route contains parameters, it will check if the other route contains values for them. + """ - def __hash__(self) -> int: - return hash(self.method) ^ hash(self.path) + if self.regex or other.regex: + return re.match(self.path, other.path) and self.method == other.method - def __eq__(self, other: "_HTTPRoute") -> bool: return self.method == other.method and self.path == other.path def __repr__(self) -> str: return f"HTTPRoute(path={repr(self.path)}, method={repr(self.method)})" + + +class _HTTPRoutes: + """A collection of routes and their corresponding handlers.""" + + def __init__(self) -> None: + self._routes: List[_HTTPRoute] = [] + self._handlers: List[Callable] = [] + + def add(self, route: _HTTPRoute, handler: Callable): + """Adds a route and its handler to the collection.""" + + self._routes.append(route) + self._handlers.append(handler) + + def find_handler(self, route: _HTTPRoute) -> Union[Callable, None]: + """ + Finds a handler for a given route. + + If route used URL parameters, the handler will be wrapped to pass the parameters to the + handler. + + Example:: + + @server.route("/example/", HTTPMethod.GET) + def route_func(request, my_parameter): + ... + request.path == "/example/123" # True + my_parameter == "123" # True + """ + + try: + matched_route = next(filter(lambda r: r.matches(route), self._routes)) + except StopIteration: + return None + + handler = self._handlers[self._routes.index(matched_route)] + args = re.match(matched_route.path, route.path).groups() + + def wrapper(request): + return handler(request, *args) + + return wrapper From c95290f390cfd033af1a8b82bf758bc928975713 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Sun, 19 Mar 2023 15:07:44 +0000 Subject: [PATCH 02/14] Replaced route_handlers dict with _HTTPRoutes object in HTTPServer --- adafruit_httpserver/server.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/adafruit_httpserver/server.py b/adafruit_httpserver/server.py index 0a41616..3c27d0e 100644 --- a/adafruit_httpserver/server.py +++ b/adafruit_httpserver/server.py @@ -19,7 +19,7 @@ from .methods import HTTPMethod from .request import HTTPRequest from .response import HTTPResponse -from .route import _HTTPRoute +from .route import _HTTPRoutes, _HTTPRoute from .status import CommonHTTPStatus @@ -34,12 +34,12 @@ def __init__(self, socket_source: Protocol) -> None: """ self._buffer = bytearray(1024) self._timeout = 1 - self.route_handlers = {} + self.routes = _HTTPRoutes() self._socket_source = socket_source self._sock = None self.root_path = "/" - def route(self, path: str, method: HTTPMethod = HTTPMethod.GET): + def route(self, path: str, method: HTTPMethod = HTTPMethod.GET) -> Callable: """ Decorator used to add a route. @@ -54,7 +54,7 @@ def route_func(request): """ def route_decorator(func: Callable) -> Callable: - self.route_handlers[_HTTPRoute(path, method)] = func + self.routes.add(_HTTPRoute(path, method), func) return func return route_decorator @@ -154,8 +154,8 @@ def poll(self): conn, received_body_bytes, content_length ) - handler = self.route_handlers.get( - _HTTPRoute(request.path, request.method), None + handler = self.routes.find_handler( + _HTTPRoute(request.path, request.method) ) # If a handler for route exists and is callable, call it. From 78cfa0e022335728ff040d2d187896ee438f66f0 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Sun, 19 Mar 2023 15:14:34 +0000 Subject: [PATCH 03/14] Updated examples and added new ones hat present added functionality --- adafruit_httpserver/server.py | 6 ++- docs/examples.rst | 28 +++++++++- examples/httpserver_neopixel.py | 17 +++++- examples/httpserver_url_parameters.py | 77 +++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 examples/httpserver_url_parameters.py diff --git a/adafruit_httpserver/server.py b/adafruit_httpserver/server.py index 3c27d0e..20b090b 100644 --- a/adafruit_httpserver/server.py +++ b/adafruit_httpserver/server.py @@ -43,7 +43,7 @@ def route(self, path: str, method: HTTPMethod = HTTPMethod.GET) -> Callable: """ Decorator used to add a route. - :param str path: filename path + :param str path: URL path :param HTTPMethod method: HTTP method: HTTPMethod.GET, HTTPMethod.POST, etc. Example:: @@ -51,6 +51,10 @@ def route(self, path: str, method: HTTPMethod = HTTPMethod.GET) -> Callable: @server.route("/example", HTTPMethod.GET) def route_func(request): ... + + @server.route("/example/", HTTPMethod.GET) + def route_func(request, my_parameter): + ... """ def route_decorator(func: Callable) -> Callable: diff --git a/docs/examples.rst b/docs/examples.rst index e9fb5bc..90a4757 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -36,7 +36,8 @@ Change NeoPixel color If you want your code to do more than just serve web pages, use the start/poll methods as shown in this example. -For example by going to ``/change-neopixel-color?r=255&g=0&b=0`` you can change the color of the NeoPixel to red. +For example by going to ``/change-neopixel-color?r=255&g=0&b=0`` or ``/change-neopixel-color/255/0/0`` +you can change the color of the NeoPixel to red. Tested on ESP32-S2 Feather. .. literalinclude:: ../examples/httpserver_neopixel.py @@ -62,3 +63,28 @@ To use it, you need to set the ``chunked=True`` when creating a ``HTTPResponse`` .. literalinclude:: ../examples/httpserver_chunked.py :caption: examples/httpserver_chunked.py :linenos: + +URL parameters +--------------------- + +Alternatively to using query parameters, you can use URL parameters. + +In order to use URL parameters, you need to wrap them inside ``<>`` in ``HTTPServer.route``, e.g. ````. + +All URL parameters are passed as positional arguments to the handler function, in order they are specified. + +Notice how the handler function in example below accepts two additional arguments : ``device_id`` and ``action``. + +If you specify multiple routes for single handler function and they have different number of URL parameters, +make sure to add default values for all the ones that might not be passed. +In the example below the second route has only one URL parameter, so the ``action`` parameter has a default value of ``None``. + +Keep in mind that URL parameters are always passed as strings, so you need to convert them to the desired type. +Also note that the names of the function parameters **do not have to match** with the ones used in route, but they **must** be in the same order. +Look at the example below to see how the ``route_param_1`` and ``route_param_1`` are named differently in the handler function. + +Although it is possible, it makes more sens be consistent with the names of the parameters in the route and in the handler function. + +.. literalinclude:: ../examples/httpserver_url_parameters.py + :caption: examples/httpserver_url_parameters.py + :linenos: diff --git a/examples/httpserver_neopixel.py b/examples/httpserver_neopixel.py index ab7dabd..814a7af 100644 --- a/examples/httpserver_neopixel.py +++ b/examples/httpserver_neopixel.py @@ -28,9 +28,9 @@ @server.route("/change-neopixel-color") -def change_neopixel_color_handler(request: HTTPRequest): +def change_neopixel_color_handler_query_params(request: HTTPRequest): """ - Changes the color of the built-in NeoPixel. + Changes the color of the built-in NeoPixel using query/GET params. """ r = request.query_params.get("r") g = request.query_params.get("g") @@ -42,5 +42,18 @@ def change_neopixel_color_handler(request: HTTPRequest): response.send(f"Changed NeoPixel to color ({r}, {g}, {b})") +@server.route("/change-neopixel-color///") +def change_neopixel_color_handler_url_params( + request: HTTPRequest, r: str, g: str, b: str +): + """ + Changes the color of the built-in NeoPixel using URL params. + """ + pixel.fill((int(r or 0), int(g or 0), int(b or 0))) + + with HTTPResponse(request, content_type=MIMEType.TYPE_TXT) as response: + response.send(f"Changed NeoPixel to color ({r}, {g}, {b})") + + print(f"Listening on http://{wifi.radio.ipv4_address}:80") server.serve_forever(str(wifi.radio.ipv4_address)) diff --git a/examples/httpserver_url_parameters.py b/examples/httpserver_url_parameters.py new file mode 100644 index 0000000..ad801f6 --- /dev/null +++ b/examples/httpserver_url_parameters.py @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +import secrets # pylint: disable=no-name-in-module + +import socketpool +import wifi + +from adafruit_httpserver.mime_type import MIMEType +from adafruit_httpserver.request import HTTPRequest +from adafruit_httpserver.response import HTTPResponse +from adafruit_httpserver.server import HTTPServer + + +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD # pylint: disable=no-member + +print("Connecting to", ssid) +wifi.radio.connect(ssid, password) +print("Connected to", ssid) + +pool = socketpool.SocketPool(wifi.radio) +server = HTTPServer(pool) + + +class Device: + def turn_on(self): + raise NotImplementedError + + def turn_off(self): + raise NotImplementedError + + +def get_device(device_id: str) -> Device: # pylint: disable=unused-argument + """ + This is a **made up** function that returns a `Device` object. + """ + return Device() + + +@server.route("/device//action/") +@server.route("/device/emergency-power-off/") +def perform_action(request: HTTPRequest, device_id: str, action: str = None): + """ + Performs an "action" on a specified device. + """ + + device = get_device(device_id) + + if action == "turn_on": + device.turn_on() + elif action == "turn_off" or action is None: + device.turn_off() + + with HTTPResponse(request, content_type=MIMEType.TYPE_TXT) as response: + response.send(f"Action ({action}) performed on device with ID: {device_id}") + + +@server.route("/something//") +def different_name_parameters( + request: HTTPRequest, + handler_param_1: str, # pylint: disable=unused-argument + handler_param_2: str = None, # pylint: disable=unused-argument +): + """ + Presents that the parameters can be named anything. + + ``route_param_1`` -> ``handler_param_1`` + ``route_param_2`` -> ``handler_param_2`` + """ + + with HTTPResponse(request, content_type=MIMEType.TYPE_TXT) as response: + response.send("200 OK") + + +print(f"Listening on http://{wifi.radio.ipv4_address}:80") +server.serve_forever(str(wifi.radio.ipv4_address)) From 030390a252809e9526b8e4859e2b2915d6facd95 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Sun, 19 Mar 2023 15:36:42 +0000 Subject: [PATCH 04/14] Fix: Prevent creating empty query param --- adafruit_httpserver/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_httpserver/request.py b/adafruit_httpserver/request.py index 9ee5fc0..908b462 100644 --- a/adafruit_httpserver/request.py +++ b/adafruit_httpserver/request.py @@ -133,7 +133,7 @@ def _parse_start_line(header_bytes: bytes) -> Tuple[str, str, Dict[str, str], st if "=" in query_param: key, value = query_param.split("=", 1) query_params[key] = value - else: + elif query_param: query_params[query_param] = "" return method, path, query_params, http_version From cf6903599f81d4c6a6ee57fac0e78afe1c9509bf Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 20 Mar 2023 03:11:05 +0000 Subject: [PATCH 05/14] Removed old deprecation error --- adafruit_httpserver/server.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/adafruit_httpserver/server.py b/adafruit_httpserver/server.py index 20b090b..84bb256 100644 --- a/adafruit_httpserver/server.py +++ b/adafruit_httpserver/server.py @@ -164,12 +164,7 @@ def poll(self): # If a handler for route exists and is callable, call it. if handler is not None and callable(handler): - output = handler(request) - # TODO: Remove this deprecation error in future - if isinstance(output, HTTPResponse): - raise RuntimeError( - "Returning an HTTPResponse from a route handler is deprecated." - ) + handler(request) # If no handler exists and request method is GET, try to serve a file. elif handler is None and request.method == HTTPMethod.GET: From e2a4761eefdabf8d34921d94a3b4253af4ee4e63 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 20 Mar 2023 03:37:08 +0000 Subject: [PATCH 06/14] Made variable name and docstring more verbose --- adafruit_httpserver/route.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/adafruit_httpserver/route.py b/adafruit_httpserver/route.py index 9289236..b50afd3 100644 --- a/adafruit_httpserver/route.py +++ b/adafruit_httpserver/route.py @@ -26,16 +26,17 @@ def __init__(self, path: str = "", method: HTTPMethod = HTTPMethod.GET) -> None: self.path = path if not contains_regex else re.sub(r"<\w*>", r"([^/]*)", path) self.method = method - self.regex = contains_regex + self._contains_regex = contains_regex def matches(self, other: "_HTTPRoute") -> bool: """ Checks if the route matches the other route. - If the route contains parameters, it will check if the other route contains values for them. + If the route contains parameters, it will check if the ``other`` route contains values for + them. """ - if self.regex or other.regex: + if self._contains_regex: return re.match(self.path, other.path) and self.method == other.method return self.method == other.method and self.path == other.path From 813c53296b6dbef6824c8ada0aa72e84d5fefac7 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 20 Mar 2023 04:05:14 +0000 Subject: [PATCH 07/14] Optimization, reduced number of re.match calls per request --- adafruit_httpserver/route.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/adafruit_httpserver/route.py b/adafruit_httpserver/route.py index b50afd3..69b46d8 100644 --- a/adafruit_httpserver/route.py +++ b/adafruit_httpserver/route.py @@ -22,11 +22,12 @@ class _HTTPRoute: def __init__(self, path: str = "", method: HTTPMethod = HTTPMethod.GET) -> None: - contains_regex = re.search(r"<\w*>", path) + contains_regex = re.search(r"<\w*>", path) is not None self.path = path if not contains_regex else re.sub(r"<\w*>", r"([^/]*)", path) self.method = method self._contains_regex = contains_regex + self._last_match_groups: Union[List[str], None] = None def matches(self, other: "_HTTPRoute") -> bool: """ @@ -35,11 +36,27 @@ def matches(self, other: "_HTTPRoute") -> bool: If the route contains parameters, it will check if the ``other`` route contains values for them. """ + if self.method != other.method: + return False - if self._contains_regex: - return re.match(self.path, other.path) and self.method == other.method + if not self._contains_regex: + return self.path == other.path - return self.method == other.method and self.path == other.path + regex_match = re.match(self.path, other.path) + if regex_match is None: + return False + + self._last_match_groups = regex_match.groups() + return True + + def last_match_groups(self) -> Union[List[str], None]: + """ + Returns the last match groups from the last call to `matches`. + + Useful for getting the values of the parameters from the route, without the need to call + `re.match` again. + """ + return self._last_match_groups def __repr__(self) -> str: return f"HTTPRoute(path={repr(self.path)}, method={repr(self.method)})" @@ -80,7 +97,7 @@ def route_func(request, my_parameter): return None handler = self._handlers[self._routes.index(matched_route)] - args = re.match(matched_route.path, route.path).groups() + args = matched_route.last_match_groups() or [] def wrapper(request): return handler(request, *args) From 994a7e87c0cb12c94a9f0be24fbcf3870f18298f Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 20 Mar 2023 04:12:02 +0000 Subject: [PATCH 08/14] Minor changes to repr of _HTTPRoute and _HTTPRoutes --- adafruit_httpserver/route.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/adafruit_httpserver/route.py b/adafruit_httpserver/route.py index 69b46d8..798f2eb 100644 --- a/adafruit_httpserver/route.py +++ b/adafruit_httpserver/route.py @@ -59,7 +59,7 @@ def last_match_groups(self) -> Union[List[str], None]: return self._last_match_groups def __repr__(self) -> str: - return f"HTTPRoute(path={repr(self.path)}, method={repr(self.method)})" + return f"_HTTPRoute(path={repr(self.path)}, method={repr(self.method)})" class _HTTPRoutes: @@ -103,3 +103,6 @@ def wrapper(request): return handler(request, *args) return wrapper + + def __repr__(self) -> str: + return f"_HTTPRoutes({repr(self._routes)})" From 8f516cd8ef243b64a83999617912356a0217d93d Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Tue, 21 Mar 2023 00:11:22 +0000 Subject: [PATCH 09/14] Replaced NotImplementedError in example with prints --- examples/httpserver_url_parameters.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/httpserver_url_parameters.py b/examples/httpserver_url_parameters.py index ad801f6..4502792 100644 --- a/examples/httpserver_url_parameters.py +++ b/examples/httpserver_url_parameters.py @@ -25,10 +25,10 @@ class Device: def turn_on(self): - raise NotImplementedError + print("Turning on device.") def turn_off(self): - raise NotImplementedError + print("Turning off device.") def get_device(device_id: str) -> Device: # pylint: disable=unused-argument @@ -40,17 +40,21 @@ def get_device(device_id: str) -> Device: # pylint: disable=unused-argument @server.route("/device//action/") @server.route("/device/emergency-power-off/") -def perform_action(request: HTTPRequest, device_id: str, action: str = None): +def perform_action(request: HTTPRequest, device_id: str, action: str = "emergency_power_off"): """ Performs an "action" on a specified device. """ device = get_device(device_id) - if action == "turn_on": + if action in ["turn_on",]: device.turn_on() - elif action == "turn_off" or action is None: + elif action in ["turn_off", "emergency_power_off"]: device.turn_off() + else: + with HTTPResponse(request, content_type=MIMEType.TYPE_TXT) as response: + response.send(f"Unknown action ({action})") + return with HTTPResponse(request, content_type=MIMEType.TYPE_TXT) as response: response.send(f"Action ({action}) performed on device with ID: {device_id}") From 4fb06a0c2b93111dda00b4cbe74f0f924bd6eb51 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Tue, 21 Mar 2023 00:19:08 +0000 Subject: [PATCH 10/14] Fix to CI --- examples/httpserver_url_parameters.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/httpserver_url_parameters.py b/examples/httpserver_url_parameters.py index 4502792..2f95163 100644 --- a/examples/httpserver_url_parameters.py +++ b/examples/httpserver_url_parameters.py @@ -24,10 +24,10 @@ class Device: - def turn_on(self): + def turn_on(self): # pylint: disable=no-self-use print("Turning on device.") - def turn_off(self): + def turn_off(self): # pylint: disable=no-self-use print("Turning off device.") @@ -40,14 +40,16 @@ def get_device(device_id: str) -> Device: # pylint: disable=unused-argument @server.route("/device//action/") @server.route("/device/emergency-power-off/") -def perform_action(request: HTTPRequest, device_id: str, action: str = "emergency_power_off"): +def perform_action( + request: HTTPRequest, device_id: str, action: str = "emergency_power_off" +): """ Performs an "action" on a specified device. """ device = get_device(device_id) - if action in ["turn_on",]: + if action in ["turn_on"]: device.turn_on() elif action in ["turn_off", "emergency_power_off"]: device.turn_off() From d70e2c521114e71216bb090480c2eba79384b163 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Tue, 21 Mar 2023 23:23:54 +0000 Subject: [PATCH 11/14] Updated README.rst --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index bb54843..caac2d7 100644 --- a/README.rst +++ b/README.rst @@ -26,9 +26,10 @@ HTTP Server for CircuitPython. - Supports `socketpool` or `socket` as a source of sockets; can be used in CPython. - HTTP 1.1. - Serves files from a designated root. -- Routing for serving computed responses from handler. +- Routing for serving computed responses from handlers. - Gives access to request headers, query parameters, body and client's address, the one from which the request came. - Supports chunked transfer encoding. +- Supports URL parameters and wildcard URLs. Dependencies From e6ad4d029a3276d9ee7b1ea22c2fe4422eee952f Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Wed, 22 Mar 2023 11:05:37 +0000 Subject: [PATCH 12/14] Minor changes in docs for examples --- docs/examples.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index 90a4757..befd552 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -71,19 +71,19 @@ Alternatively to using query parameters, you can use URL parameters. In order to use URL parameters, you need to wrap them inside ``<>`` in ``HTTPServer.route``, e.g. ````. -All URL parameters are passed as positional arguments to the handler function, in order they are specified. +All URL parameters are **passed as positional (not keyword) arguments** to the handler function, in order they are specified in ``HTTPServer.route``. Notice how the handler function in example below accepts two additional arguments : ``device_id`` and ``action``. If you specify multiple routes for single handler function and they have different number of URL parameters, make sure to add default values for all the ones that might not be passed. -In the example below the second route has only one URL parameter, so the ``action`` parameter has a default value of ``None``. +In the example below the second route has only one URL parameter, so the ``action`` parameter has a default value. Keep in mind that URL parameters are always passed as strings, so you need to convert them to the desired type. Also note that the names of the function parameters **do not have to match** with the ones used in route, but they **must** be in the same order. Look at the example below to see how the ``route_param_1`` and ``route_param_1`` are named differently in the handler function. -Although it is possible, it makes more sens be consistent with the names of the parameters in the route and in the handler function. +Although it is possible, it makes more sense be consistent with the names of the parameters in the route and in the handler function. .. literalinclude:: ../examples/httpserver_url_parameters.py :caption: examples/httpserver_url_parameters.py From d5817b0abdc2c0c777206ea4235212b6b17c4f65 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Sat, 1 Apr 2023 18:01:24 +0000 Subject: [PATCH 13/14] Refactor for removing the need for saving last match group --- adafruit_httpserver/route.py | 81 +++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/adafruit_httpserver/route.py b/adafruit_httpserver/route.py index 798f2eb..b141695 100644 --- a/adafruit_httpserver/route.py +++ b/adafruit_httpserver/route.py @@ -8,7 +8,7 @@ """ try: - from typing import Callable, List, Union + from typing import Callable, List, Union, Tuple except ImportError: pass @@ -22,41 +22,56 @@ class _HTTPRoute: def __init__(self, path: str = "", method: HTTPMethod = HTTPMethod.GET) -> None: - contains_regex = re.search(r"<\w*>", path) is not None + contains_parameters = re.search(r"<\w*>", path) is not None - self.path = path if not contains_regex else re.sub(r"<\w*>", r"([^/]*)", path) + self.path = ( + path if not contains_parameters else re.sub(r"<\w*>", r"([^/]*)", path) + ) self.method = method - self._contains_regex = contains_regex - self._last_match_groups: Union[List[str], None] = None + self._contains_parameters = contains_parameters - def matches(self, other: "_HTTPRoute") -> bool: + def match(self, other: "_HTTPRoute") -> Tuple[bool, List[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 = _HTTPRoute("/example", HTTPMethod.GET) + + other1 = _HTTPRoute("/example", HTTPMethod.GET) + route.matches(other1) # True, [] + + other2 = _HTTPRoute("/other-example", HTTPMethod.GET) + route.matches(other2) # False, [] + + ... + + route = _HTTPRoute("/example/", HTTPMethod.GET) + + other1 = _HTTPRoute("/example/123", HTTPMethod.GET) + route.matches(other1) # True, ["123"] + + other2 = _HTTPRoute("/other-example", HTTPMethod.GET) + route.matches(other2) # False, [] """ + if self.method != other.method: - return False + return False, [] - if not self._contains_regex: - return self.path == other.path + if not self._contains_parameters: + return self.path == other.path, [] regex_match = re.match(self.path, other.path) if regex_match is None: - return False - - self._last_match_groups = regex_match.groups() - return True - - def last_match_groups(self) -> Union[List[str], None]: - """ - Returns the last match groups from the last call to `matches`. + return False, [] - Useful for getting the values of the parameters from the route, without the need to call - `re.match` again. - """ - return self._last_match_groups + return True, regex_match.groups() def __repr__(self) -> str: return f"_HTTPRoute(path={repr(self.path)}, method={repr(self.method)})" @@ -90,19 +105,27 @@ def route_func(request, my_parameter): request.path == "/example/123" # True my_parameter == "123" # True """ + if not self._routes: + raise ValueError("No routes added") + + found_route, _route = False, None + + for _route in self._routes: + matches, url_parameters_values = _route.match(route) + + if matches: + found_route = True + break - try: - matched_route = next(filter(lambda r: r.matches(route), self._routes)) - except StopIteration: + if not found_route: return None - handler = self._handlers[self._routes.index(matched_route)] - args = matched_route.last_match_groups() or [] + handler = self._handlers[self._routes.index(_route)] - def wrapper(request): - return handler(request, *args) + def wrapped_handler(request): + return handler(request, *url_parameters_values) - return wrapper + return wrapped_handler def __repr__(self) -> str: return f"_HTTPRoutes({repr(self._routes)})" From 7ddc32a38e23a2c80125f23a86fbfa9ea92fa357 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 3 Apr 2023 09:25:45 +0000 Subject: [PATCH 14/14] Removed unnecessary indentation in docstring example --- adafruit_httpserver/response.py | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/adafruit_httpserver/response.py b/adafruit_httpserver/response.py index c6cdcea..fbb2e3f 100644 --- a/adafruit_httpserver/response.py +++ b/adafruit_httpserver/response.py @@ -29,38 +29,38 @@ class HTTPResponse: Example:: - # Response with 'Content-Length' header - @server.route(path, method) - def route_func(request): + # Response with 'Content-Length' header + @server.route(path, method) + def route_func(request): - response = HTTPResponse(request) - response.send("Some content", content_type="text/plain") + response = HTTPResponse(request) + response.send("Some content", content_type="text/plain") - # or + # or - response = HTTPResponse(request) - with response: - response.send(body='Some content', content_type="text/plain") + response = HTTPResponse(request) + with response: + response.send(body='Some content', content_type="text/plain") - # or + # or - with HTTPResponse(request) as response: - response.send("Some content", content_type="text/plain") + with HTTPResponse(request) as response: + response.send("Some content", content_type="text/plain") - # Response with 'Transfer-Encoding: chunked' header - @server.route(path, method) - def route_func(request): + # Response with 'Transfer-Encoding: chunked' header + @server.route(path, method) + def route_func(request): - response = HTTPResponse(request, content_type="text/plain", chunked=True) - with response: - response.send_chunk("Some content") - response.send_chunk("Some more content") + response = HTTPResponse(request, content_type="text/plain", chunked=True) + with response: + response.send_chunk("Some content") + response.send_chunk("Some more content") - # or + # or - with HTTPResponse(request, content_type="text/plain", chunked=True) as response: - response.send_chunk("Some content") - response.send_chunk("Some more content") + with HTTPResponse(request, content_type="text/plain", chunked=True) as response: + response.send_chunk("Some content") + response.send_chunk("Some more content") """ request: HTTPRequest