From aad7010218f40e98cded15e9c03f4f31665e7d41 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 19 Dec 2022 19:33:19 +0000 Subject: [PATCH 01/21] Added HTTPHeaders --- adafruit_httpserver/headers.py | 100 +++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 adafruit_httpserver/headers.py diff --git a/adafruit_httpserver/headers.py b/adafruit_httpserver/headers.py new file mode 100644 index 0000000..c37c93a --- /dev/null +++ b/adafruit_httpserver/headers.py @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_httpserver.headers.HTTPHeaders` +==================================================== +* Author(s): MichaƂ Pokusa +""" + +try: + from typing import Dict, Tuple +except ImportError: + pass + + +class HTTPHeaders: + """ + A dict-like object for storing HTTP headers. + + Allows access to headers using **case insensitive** names. + + Does **not** implement all dict methods. + + Examples:: + + headers = HTTPHeaders({"Content-Type": "text/html", "Content-Length": "1024"}) + + len(headers) + # 2 + + headers.setdefault("Access-Control-Allow-Origin", "*") + headers["Access-Control-Allow-Origin"] + # '*' + + headers["Content-Length"] + # '1024' + + headers["content-type"] + # 'text/html' + + "CONTENT-TYPE" in headers + # True + """ + + _storage: Dict[str, Tuple[str, str]] + + def __init__(self, headers: Dict[str, str] = None) -> None: + + headers = headers or {} + + self._storage = {key.lower(): [key, value] for key, value in headers.items()} + + def get(self, name: str, default: str = None): + """Returns the value for the given header name, or default if not found.""" + return self._storage.get(name.lower(), [None, default])[1] + + def setdefault(self, name: str, default: str = None): + """Sets the value for the given header name if it does not exist.""" + return self._storage.setdefault(name.lower(), [name, default])[1] + + def items(self): + """Returns a list of (name, value) tuples.""" + return dict(self._storage.values()).items() + + def keys(self): + """Returns a list of header names.""" + return dict(self._storage.values()).keys() + + def values(self): + """Returns a list of header values.""" + return dict(self._storage.values()).values() + + def update(self, headers: Dict[str, str]): + """Updates the headers with the given dict.""" + return self._storage.update({key.lower(): [key, value] for key, value in headers.items()}) + + def copy(self): + """Returns a copy of the headers.""" + return HTTPHeaders(dict(self._storage.values())) + + def __getitem__(self, name: str): + return self._storage[name.lower()][1] + + def __setitem__(self, name: str, value: str): + self._storage[name.lower()] = [name, value] + + def __delitem__(self, name: str): + del self._storage[name.lower()] + + def __iter__(self): + return iter(dict(self._storage.values())) + + def __len__(self): + return len(self._storage) + + def __contains__(self, key: str): + return key.lower() in self._storage.keys() + + def __repr__(self): + return f'{self.__class__.__name__}({dict(self._storage.values())})' From f1878b3aced51b560f0ec64a20f8a1fe5e44dd3a Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 19 Dec 2022 20:01:34 +0000 Subject: [PATCH 02/21] Replacing dict with HTTPHeaders in other modules --- adafruit_httpserver/request.py | 25 ++++++++----------------- adafruit_httpserver/response.py | 24 +++++++++++------------- 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/adafruit_httpserver/request.py b/adafruit_httpserver/request.py index 22758a8..96b1546 100644 --- a/adafruit_httpserver/request.py +++ b/adafruit_httpserver/request.py @@ -12,6 +12,8 @@ except ImportError: pass +from .headers import HTTPHeaders + class HTTPRequest: """ @@ -39,20 +41,9 @@ class HTTPRequest: http_version: str """HTTP version, e.g. "HTTP/1.1".""" - headers: Dict[str, str] + headers: HTTPHeaders """ - Headers from the request as `dict`. - - Values should be accessed using **lower case header names**. - - Example:: - - request.headers - # {'connection': 'keep-alive', 'content-length': '64' ...} - request.headers["content-length"] - # '64' - request.headers["Content-Length"] - # KeyError: 'Content-Length' + Headers from the request. """ raw_request: bytes @@ -120,12 +111,12 @@ def _parse_start_line(header_bytes: bytes) -> Tuple[str, str, Dict[str, str], st return method, path, query_params, http_version @staticmethod - def _parse_headers(header_bytes: bytes) -> Dict[str, str]: + def _parse_headers(header_bytes: bytes) -> HTTPHeaders: """Parse HTTP headers from raw request.""" header_lines = header_bytes.decode("utf8").splitlines()[1:] - return { - name.lower(): value + return HTTPHeaders({ + name: value for header_line in header_lines for name, value in [header_line.split(": ", 1)] - } + }) diff --git a/adafruit_httpserver/response.py b/adafruit_httpserver/response.py index 1c5c0f9..7d6bf16 100644 --- a/adafruit_httpserver/response.py +++ b/adafruit_httpserver/response.py @@ -20,6 +20,7 @@ from .mime_type import MIMEType from .status import HTTPStatus, CommonHTTPStatus +from .headers import HTTPHeaders class HTTPResponse: @@ -27,7 +28,7 @@ class HTTPResponse: http_version: str status: HTTPStatus - headers: Dict[str, str] + headers: HTTPHeaders content_type: str filename: Optional[str] @@ -39,7 +40,7 @@ def __init__( # pylint: disable=too-many-arguments self, status: Union[HTTPStatus, Tuple[int, str]] = CommonHTTPStatus.OK_200, body: str = "", - headers: Dict[str, str] = None, + headers: Union[HTTPHeaders, Dict[str, str]] = None, content_type: str = MIMEType.TYPE_TXT, filename: Optional[str] = None, root_path: str = "", @@ -52,7 +53,7 @@ def __init__( # pylint: disable=too-many-arguments """ self.status = status if isinstance(status, HTTPStatus) else HTTPStatus(*status) self.body = body - self.headers = headers or {} + self.headers = headers.copy() if isinstance(headers, HTTPHeaders) else HTTPHeaders(headers) self.content_type = content_type self.filename = filename self.root_path = root_path @@ -64,21 +65,18 @@ def _construct_response_bytes( # pylint: disable=too-many-arguments status: HTTPStatus = CommonHTTPStatus.OK_200, content_type: str = MIMEType.TYPE_TXT, content_length: Union[int, None] = None, - headers: Dict[str, str] = None, + headers: HTTPHeaders = None, body: str = "", ) -> bytes: """Constructs the response bytes from the given parameters.""" response = f"{http_version} {status.code} {status.text}\r\n" - # Make a copy of the headers so that we don't modify the incoming dict - response_headers = {} if headers is None else headers.copy() + headers.setdefault("Content-Type", content_type) + headers.setdefault("Content-Length", content_length or len(body)) + headers.setdefault("Connection", "close") - response_headers.setdefault("Content-Type", content_type) - response_headers.setdefault("Content-Length", content_length or len(body)) - response_headers.setdefault("Connection", "close") - - for header, value in response_headers.items(): + for header, value in headers.items(): response += f"{header}: {value}\r\n" response += f"\r\n{body}" @@ -122,7 +120,7 @@ def _send_response( # pylint: disable=too-many-arguments status: HTTPStatus, content_type: str, body: str, - headers: Dict[str, str] = None, + headers: HTTPHeaders = None, ): self._send_bytes( conn, @@ -140,7 +138,7 @@ def _send_file_response( # pylint: disable=too-many-arguments filename: str, root_path: str, file_length: int, - headers: Dict[str, str] = None, + headers: HTTPHeaders = None, ): self._send_bytes( conn, From d547c7fa390e68fa77a3a54822ebdb090cebb191 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 19 Dec 2022 20:18:26 +0000 Subject: [PATCH 03/21] Fixed and extended docstrings --- adafruit_httpserver/headers.py | 2 +- adafruit_httpserver/request.py | 6 +++++- adafruit_httpserver/server.py | 5 +++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/adafruit_httpserver/headers.py b/adafruit_httpserver/headers.py index c37c93a..4d86128 100644 --- a/adafruit_httpserver/headers.py +++ b/adafruit_httpserver/headers.py @@ -15,7 +15,7 @@ class HTTPHeaders: """ - A dict-like object for storing HTTP headers. + A dict-like class for storing HTTP headers. Allows access to headers using **case insensitive** names. diff --git a/adafruit_httpserver/request.py b/adafruit_httpserver/request.py index 96b1546..76f37c7 100644 --- a/adafruit_httpserver/request.py +++ b/adafruit_httpserver/request.py @@ -47,7 +47,11 @@ class HTTPRequest: """ raw_request: bytes - """Raw bytes passed to the constructor.""" + """ + Raw 'bytes' passed to the constructor and body 'bytes' received later. + + Should **not** be modified directly. + """ def __init__(self, raw_request: bytes = None) -> None: self.raw_request = raw_request diff --git a/adafruit_httpserver/server.py b/adafruit_httpserver/server.py index 8bf1045..503b9b7 100644 --- a/adafruit_httpserver/server.py +++ b/adafruit_httpserver/server.py @@ -211,9 +211,10 @@ def request_buffer_size(self, value: int) -> None: def socket_timeout(self) -> int: """ Timeout after which the socket will stop waiting for more incoming data. - When exceeded, raises `OSError` with `errno.ETIMEDOUT`. - Default timeout is 0, which means socket is in non-blocking mode. + Must be set to positive integer or float. Default is 1 second. + + When exceeded, raises `OSError` with `errno.ETIMEDOUT`. Example:: From d0d5b802a7039c701fa752f44386d8e46e359a57 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 19 Dec 2022 20:19:10 +0000 Subject: [PATCH 04/21] Changed order of Docs References and updated version --- docs/api.rst | 13 ++++++++----- docs/conf.py | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index cf4ba22..4615507 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -7,10 +7,7 @@ .. automodule:: adafruit_httpserver :members: -.. automodule:: adafruit_httpserver.methods - :members: - -.. automodule:: adafruit_httpserver.mime_type +.. automodule:: adafruit_httpserver.server :members: .. automodule:: adafruit_httpserver.request @@ -19,8 +16,14 @@ .. automodule:: adafruit_httpserver.response :members: -.. automodule:: adafruit_httpserver.server +.. automodule:: adafruit_httpserver.headers :members: .. automodule:: adafruit_httpserver.status :members: + +.. automodule:: adafruit_httpserver.methods + :members: + +.. automodule:: adafruit_httpserver.mime_type + :members: diff --git a/docs/conf.py b/docs/conf.py index 5dda03c..e46d17f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,9 +65,9 @@ # built documents. # # The short X.Y version. -version = "1.0" +version = "1.1.0" # The full version, including alpha/beta/rc tags. -release = "1.0" +release = "1.1.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From d3adfd824c65ad9e05104cc836a3c49d2ace778d Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 19 Dec 2022 20:26:24 +0000 Subject: [PATCH 05/21] Fixed wrong content length for multibyte characters --- adafruit_httpserver/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_httpserver/response.py b/adafruit_httpserver/response.py index 7d6bf16..23861df 100644 --- a/adafruit_httpserver/response.py +++ b/adafruit_httpserver/response.py @@ -73,7 +73,7 @@ def _construct_response_bytes( # pylint: disable=too-many-arguments response = f"{http_version} {status.code} {status.text}\r\n" headers.setdefault("Content-Type", content_type) - headers.setdefault("Content-Length", content_length or len(body)) + headers.setdefault("Content-Length", content_length or len(body.encode("utf-8"))) headers.setdefault("Connection", "close") for header, value in headers.items(): From be20bb12e76a99f5e8cb15eb189789669d80da02 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 19 Dec 2022 21:15:05 +0000 Subject: [PATCH 06/21] Black format changes --- adafruit_httpserver/headers.py | 6 ++++-- adafruit_httpserver/request.py | 12 +++++++----- adafruit_httpserver/response.py | 8 ++++++-- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/adafruit_httpserver/headers.py b/adafruit_httpserver/headers.py index 4d86128..d63fdfa 100644 --- a/adafruit_httpserver/headers.py +++ b/adafruit_httpserver/headers.py @@ -72,7 +72,9 @@ def values(self): def update(self, headers: Dict[str, str]): """Updates the headers with the given dict.""" - return self._storage.update({key.lower(): [key, value] for key, value in headers.items()}) + return self._storage.update( + {key.lower(): [key, value] for key, value in headers.items()} + ) def copy(self): """Returns a copy of the headers.""" @@ -97,4 +99,4 @@ def __contains__(self, key: str): return key.lower() in self._storage.keys() def __repr__(self): - return f'{self.__class__.__name__}({dict(self._storage.values())})' + return f"{self.__class__.__name__}({dict(self._storage.values())})" diff --git a/adafruit_httpserver/request.py b/adafruit_httpserver/request.py index 76f37c7..74a57fe 100644 --- a/adafruit_httpserver/request.py +++ b/adafruit_httpserver/request.py @@ -119,8 +119,10 @@ def _parse_headers(header_bytes: bytes) -> HTTPHeaders: """Parse HTTP headers from raw request.""" header_lines = header_bytes.decode("utf8").splitlines()[1:] - return HTTPHeaders({ - name: value - for header_line in header_lines - for name, value in [header_line.split(": ", 1)] - }) + return HTTPHeaders( + { + name: value + for header_line in header_lines + for name, value in [header_line.split(": ", 1)] + } + ) diff --git a/adafruit_httpserver/response.py b/adafruit_httpserver/response.py index 23861df..57bc847 100644 --- a/adafruit_httpserver/response.py +++ b/adafruit_httpserver/response.py @@ -53,7 +53,9 @@ def __init__( # pylint: disable=too-many-arguments """ self.status = status if isinstance(status, HTTPStatus) else HTTPStatus(*status) self.body = body - self.headers = headers.copy() if isinstance(headers, HTTPHeaders) else HTTPHeaders(headers) + self.headers = ( + headers.copy() if isinstance(headers, HTTPHeaders) else HTTPHeaders(headers) + ) self.content_type = content_type self.filename = filename self.root_path = root_path @@ -73,7 +75,9 @@ def _construct_response_bytes( # pylint: disable=too-many-arguments response = f"{http_version} {status.code} {status.text}\r\n" headers.setdefault("Content-Type", content_type) - headers.setdefault("Content-Length", content_length or len(body.encode("utf-8"))) + headers.setdefault( + "Content-Length", content_length or len(body.encode("utf-8")) + ) headers.setdefault("Connection", "close") for header, value in headers.items(): From aeb95a293251192b4ab3cea61a8fb70d9cee4d51 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Tue, 20 Dec 2022 01:02:51 +0000 Subject: [PATCH 07/21] Encoding body only once when constructing response bytes --- adafruit_httpserver/response.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/adafruit_httpserver/response.py b/adafruit_httpserver/response.py index 57bc847..435473a 100644 --- a/adafruit_httpserver/response.py +++ b/adafruit_httpserver/response.py @@ -72,20 +72,20 @@ def _construct_response_bytes( # pylint: disable=too-many-arguments ) -> bytes: """Constructs the response bytes from the given parameters.""" - response = f"{http_version} {status.code} {status.text}\r\n" + response_message_header = f"{http_version} {status.code} {status.text}\r\n" + encoded_response_message_body = body.encode("utf-8") headers.setdefault("Content-Type", content_type) headers.setdefault( - "Content-Length", content_length or len(body.encode("utf-8")) + "Content-Length", content_length or len(encoded_response_message_body) ) headers.setdefault("Connection", "close") for header, value in headers.items(): - response += f"{header}: {value}\r\n" + response_message_header += f"{header}: {value}\r\n" + response_message_header += "\r\n" - response += f"\r\n{body}" - - return response.encode("utf-8") + return response_message_header.encode("utf-8") + encoded_response_message_body def send(self, conn: Union["SocketPool.Socket", "socket.socket"]) -> None: """ From 00d32478258971e689d4fb1506a75751644c66d1 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Fri, 23 Dec 2022 11:55:57 +0000 Subject: [PATCH 08/21] Refactor for unifying the HTTPResponse API --- adafruit_httpserver/request.py | 28 +++- adafruit_httpserver/response.py | 227 +++++++++++++------------------- adafruit_httpserver/server.py | 49 +++---- 3 files changed, 135 insertions(+), 169 deletions(-) diff --git a/adafruit_httpserver/request.py b/adafruit_httpserver/request.py index 74a57fe..7b10b2c 100644 --- a/adafruit_httpserver/request.py +++ b/adafruit_httpserver/request.py @@ -8,7 +8,9 @@ """ try: - from typing import Dict, Tuple + from typing import Dict, Tuple, Union + from socket import socket + from socketpool import SocketPool except ImportError: pass @@ -21,6 +23,21 @@ class HTTPRequest: It is passed as first argument to route handlers. """ + connection: Union["SocketPool.Socket", "socket.socket"] + """ + Socket object usable to send and receive data on the connection. + """ + + address: Tuple[str, int] + """ + Address bound to the socket on the other end of the connection. + + Example:: + + request.address + # ('192.168.137.1', 40684) + """ + method: str """Request method e.g. "GET" or "POST".""" @@ -53,7 +70,14 @@ class HTTPRequest: Should **not** be modified directly. """ - def __init__(self, raw_request: bytes = None) -> None: + def __init__( + self, + connection: Union["SocketPool.Socket", "socket.socket"], + address: Tuple[str, int], + raw_request: bytes = None, + ) -> None: + self.connection = connection + self.address = address self.raw_request = raw_request if raw_request is None: diff --git a/adafruit_httpserver/response.py b/adafruit_httpserver/response.py index a821e6f..731af65 100644 --- a/adafruit_httpserver/response.py +++ b/adafruit_httpserver/response.py @@ -14,191 +14,152 @@ except ImportError: pass -from errno import EAGAIN, ECONNRESET import os - +from errno import EAGAIN, ECONNRESET from .mime_type import MIMEType +from .request import HTTPRequest from .status import HTTPStatus, CommonHTTPStatus from .headers import HTTPHeaders class HTTPResponse: - """Details of an HTTP response. Use in `HTTPServer.route` decorator functions.""" + """ + Response to a given `HTTPRequest`. Use in `HTTPServer.route` decorator functions. + + Example:: + + # 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 with 'Transfer-Encoding: chunked' header + @server.route(path, method) + def route_func(request): + response = HTTPResponse(request, content_type="text/html") + response.send_headers(content_type="text/plain", chunked=True) + response.send_body_chunk("Some content") + response.send_body_chunk("Some more content") + response.send_body_chunk("") # Send empty packet to finish chunked stream + """ + + request: HTTPRequest + """The request that this is a response to.""" http_version: str status: HTTPStatus headers: HTTPHeaders - content_type: str - cache: Optional[int] - filename: Optional[str] - root_path: str - - body: str def __init__( # pylint: disable=too-many-arguments self, + request: HTTPRequest, status: Union[HTTPStatus, Tuple[int, str]] = CommonHTTPStatus.OK_200, - body: str = "", headers: Union[HTTPHeaders, Dict[str, str]] = None, - content_type: str = MIMEType.TYPE_TXT, - cache: Optional[int] = 0, - filename: Optional[str] = None, - root_path: str = "", http_version: str = "HTTP/1.1", ) -> None: """ Creates an HTTP response. - Returns ``body`` if ``filename`` is ``None``, otherwise returns contents of ``filename``. + Sets `status`, ``headers`` and `http_version`. + + To send the response, call `send` or `send_file`. + For chunked response ``send_headers(chunked=True)`` and then `send_chunk_body`. """ + self.request = request self.status = status if isinstance(status, HTTPStatus) else HTTPStatus(*status) - self.body = body self.headers = ( headers.copy() if isinstance(headers, HTTPHeaders) else HTTPHeaders(headers) ) - self.content_type = content_type - self.cache = cache - self.filename = filename - self.root_path = root_path self.http_version = http_version - @staticmethod - def _construct_response_bytes( # pylint: disable=too-many-arguments - http_version: str = "HTTP/1.1", - status: HTTPStatus = CommonHTTPStatus.OK_200, + def send_headers( + self, + content_length: Optional[int] = None, content_type: str = MIMEType.TYPE_TXT, - content_length: Union[int, None] = None, - cache: int = 0, - headers: Dict[str, str] = None, - body: str = "", chunked: bool = False, - ) -> bytes: - """Constructs the response bytes from the given parameters.""" + ) -> None: + """ + Send response with `body` over the given socket. + """ + headers = self.headers.copy() - response_message_header = f"{http_version} {status.code} {status.text}\r\n" - encoded_response_message_body = body.encode("utf-8") + response_message_header = ( + f"{self.http_version} {self.status.code} {self.status.text}\r\n" + ) headers.setdefault("Content-Type", content_type) - headers.setdefault( - "Content-Length", content_length or len(encoded_response_message_body) - ) headers.setdefault("Connection", "close") - - response_headers.setdefault("Content-Type", content_type) - response_headers.setdefault("Connection", "close") if chunked: - response_headers.setdefault("Transfer-Encoding", "chunked") + headers.setdefault("Transfer-Encoding", "chunked") else: - response_headers.setdefault("Content-Length", content_length or len(body)) - - for header, value in response_headers.items(): - response += f"{header}: {value}\r\n" - - response += f"Cache-Control: max-age={cache}\r\n" + headers.setdefault("Content-Length", content_length) - response += f"\r\n{body}" + for header, value in headers.items(): + response_message_header += f"{header}: {value}\r\n" + response_message_header += "\r\n" - return response.encode("utf-8") + self._send_bytes( + self.request.connection, response_message_header.encode("utf-8") + ) - def send(self, conn: Union["SocketPool.Socket", "socket.socket"]) -> None: + def send( + self, + body: str = "", + content_type: str = MIMEType.TYPE_TXT, + ) -> None: """ - Send the constructed response over the given socket. + Send response with `body` over the given socket. + Implicitly calls `send_headers` before sending the body. """ + encoded_response_message_body = body.encode("utf-8") - if self.filename is not None: - try: - file_length = os.stat(self.root_path + self.filename)[6] - self._send_file_response( - conn, - filename=self.filename, - root_path=self.root_path, - file_length=file_length, - headers=self.headers, - ) - except OSError: - self._send_response( - conn, - status=CommonHTTPStatus.NOT_FOUND_404, - content_type=MIMEType.TYPE_TXT, - body=f"{CommonHTTPStatus.NOT_FOUND_404} {self.filename}", - ) - else: - self._send_response( - conn, - status=self.status, - content_type=self.content_type, - headers=self.headers, - body=self.body, - ) - - def send_chunk_headers( - self, conn: Union["SocketPool.Socket", "socket.socket"] - ) -> None: - """Send Headers for a chunked response over the given socket.""" - self._send_bytes( - conn, - self._construct_response_bytes( - status=self.status, - content_type=self.content_type, - chunked=True, - cache=self.cache, - body="", - ), + self.send_headers( + content_type=content_type, + content_length=len(encoded_response_message_body), ) + self._send_bytes(self.request.connection, encoded_response_message_body) - def send_body_chunk( - self, conn: Union["SocketPool.Socket", "socket.socket"], chunk: str + def send_file( + self, + filename: str = "index.html", + root_path: str = "./", ) -> None: - """Send chunk of data to the given socket. Send an empty("") chunk to finish the session. + """ + Send response with content of ``filename`` located in ``root_path`` over the given socket. + """ + if not root_path.endswith("/"): + root_path += "/" + try: + file_length = os.stat(root_path + filename)[6] + except OSError: + # If the file doesn't exist, return 404. + HTTPResponse(self.request, status=CommonHTTPStatus.NOT_FOUND_404).send() + return + + self.send_headers( + content_type=MIMEType.from_file_name(filename), + content_length=file_length, + ) - :param Union["SocketPool.Socket", "socket.socket"] conn: Current connection. - :param str chunk: String data to be sent. + with open(root_path + filename, "rb") as file: + while bytes_read := file.read(2048): + self._send_bytes(self.request.connection, bytes_read) + + def send_chunk_body(self, chunk: str = "") -> None: """ - size = "%X\r\n".encode() % len(chunk) - self._send_bytes(conn, size) - self._send_bytes(conn, chunk.encode() + b"\r\n") + Send chunk of data to the given socket. - def _send_response( # pylint: disable=too-many-arguments - self, - conn: Union["SocketPool.Socket", "socket.socket"], - status: HTTPStatus, - content_type: str, - body: str, - headers: HTTPHeaders = None, - ): - self._send_bytes( - conn, - self._construct_response_bytes( - status=status, - content_type=content_type, - cache=self.cache, - headers=headers, - body=body, - ), - ) + Call without `chunk` to finish the session. + + :param str chunk: String data to be sent. + """ + hex_length = hex(len(chunk)).lstrip("0x").rstrip("L") - def _send_file_response( # pylint: disable=too-many-arguments - self, - conn: Union["SocketPool.Socket", "socket.socket"], - filename: str, - root_path: str, - file_length: int, - headers: HTTPHeaders = None, - ): self._send_bytes( - conn, - self._construct_response_bytes( - status=self.status, - content_type=MIMEType.from_file_name(filename), - content_length=file_length, - cache=self.cache, - headers=headers, - ), + self.request.connection, f"{hex_length}\r\n{chunk}\r\n".encode("utf-8") ) - with open(root_path + filename, "rb") as file: - while bytes_read := file.read(2048): - self._send_bytes(conn, bytes_read) @staticmethod def _send_bytes( diff --git a/adafruit_httpserver/server.py b/adafruit_httpserver/server.py index 520cd61..0e0d5bc 100644 --- a/adafruit_httpserver/server.py +++ b/adafruit_httpserver/server.py @@ -40,30 +40,17 @@ def __init__(self, socket_source: Protocol) -> None: self.root_path = "/" def route(self, path: str, method: HTTPMethod = HTTPMethod.GET): - """Decorator used to add a route. + """ + Decorator used to add a route. :param str path: filename path :param HTTPMethod method: HTTP method: HTTPMethod.GET, HTTPMethod.POST, etc. Example:: - @server.route(path, method) + @server.route("/example", HTTPMethod.GET) def route_func(request): - raw_text = request.raw_request.decode("utf8") - print("Received a request of length", len(raw_text), "bytes") - return HTTPResponse(body="hello world") - - - @server.route(path, method) - def route_func(request, conn): - raw_text = request.raw_request.decode("utf8") - print("Received a request of length", len(raw_text), "bytes") - res = HTTPResponse(content_type="text/html") - res.send_chunk_headers(conn) - res.send_body_chunk(conn, "Some content") - res.send_body_chunk(conn, "Some more content") - res.send_body_chunk(conn, "") # Send empty packet to finish chunked stream - return None # Return None, so server knows that nothing else needs to be sent. + ... """ def route_decorator(func: Callable) -> Callable: @@ -146,7 +133,7 @@ def poll(self): the application callable will be invoked. """ try: - conn, _ = self._sock.accept() + conn, address = self._sock.accept() with conn: conn.settimeout(self._timeout) @@ -157,9 +144,9 @@ def poll(self): if not header_bytes: return - request = HTTPRequest(header_bytes) + request = HTTPRequest(conn, address, header_bytes) - content_length = int(request.headers.get("content-length", 0)) + content_length = int(request.headers.get("Content-Length", 0)) received_body_bytes = request.body # Receiving remaining body bytes @@ -173,25 +160,19 @@ def poll(self): # If a handler for route exists and is callable, call it. if handler is not None and callable(handler): - # Need to pass connection for chunked encoding to work. - try: - response = handler(request, conn) - except TypeError: - response = handler(request) - if response is None: - return + handler(request) # If no handler exists and request method is GET, try to serve a file. - elif request.method == HTTPMethod.GET: - response = HTTPResponse( - filename=request.path, root_path=self.root_path, cache=604800 + elif handler is None and request.method == HTTPMethod.GET: + HTTPResponse(request).send_file( + filename=request.path, + root_path=self.root_path, ) - - # If no handler exists and request method is not GET, return 400 Bad Request. else: - response = HTTPResponse(status=CommonHTTPStatus.BAD_REQUEST_400) + HTTPResponse( + request, status=CommonHTTPStatus.BAD_REQUEST_400 + ).send() - response.send(conn) except OSError as ex: # handle EAGAIN and ECONNRESET if ex.errno == EAGAIN: From 2ca4756bac16f1a6c1d06a134a28b0a40fc80d06 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Fri, 23 Dec 2022 11:57:27 +0000 Subject: [PATCH 09/21] Changed HTTPResponse to use context managers --- adafruit_httpserver/response.py | 58 +++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/adafruit_httpserver/response.py b/adafruit_httpserver/response.py index 731af65..d8f9edf 100644 --- a/adafruit_httpserver/response.py +++ b/adafruit_httpserver/response.py @@ -34,15 +34,18 @@ class HTTPResponse: def route_func(request): response = HTTPResponse(request) response.send("Some content", content_type="text/plain") + # or + @server.route(path, method) + def route_func(request): + 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 = HTTPResponse(request, content_type="text/html") - response.send_headers(content_type="text/plain", chunked=True) - response.send_body_chunk("Some content") - response.send_body_chunk("Some more content") - response.send_body_chunk("") # Send empty packet to finish chunked stream + with HTTPResponse(request, content_type="text/plain", chunked=True) as response: + response.send_body_chunk("Some content") + response.send_body_chunk("Some more content") """ request: HTTPRequest @@ -51,13 +54,16 @@ def route_func(request): http_version: str status: HTTPStatus headers: HTTPHeaders + content_type: str def __init__( # pylint: disable=too-many-arguments self, request: HTTPRequest, status: Union[HTTPStatus, Tuple[int, str]] = CommonHTTPStatus.OK_200, headers: Union[HTTPHeaders, Dict[str, str]] = None, + content_type: str = MIMEType.TYPE_TXT, http_version: str = "HTTP/1.1", + chunked: bool = False, ) -> None: """ Creates an HTTP response. @@ -65,23 +71,27 @@ def __init__( # pylint: disable=too-many-arguments Sets `status`, ``headers`` and `http_version`. To send the response, call `send` or `send_file`. - For chunked response ``send_headers(chunked=True)`` and then `send_chunk_body`. + For chunked response use + ``with HTTPRequest(request, content_type=..., chunked=True) as r:`` and `send_chunk_body`. """ self.request = request self.status = status if isinstance(status, HTTPStatus) else HTTPStatus(*status) self.headers = ( headers.copy() if isinstance(headers, HTTPHeaders) else HTTPHeaders(headers) ) + self.content_type = content_type self.http_version = http_version + self.chunked = chunked - def send_headers( + def _send_headers( self, content_length: Optional[int] = None, content_type: str = MIMEType.TYPE_TXT, - chunked: bool = False, ) -> None: """ - Send response with `body` over the given socket. + Sends headers. + Implicitly called by `send` and `send_file` and in + ``with HTTPResponse(request, chunked=True) as response:`` context manager. """ headers = self.headers.copy() @@ -89,9 +99,9 @@ def send_headers( f"{self.http_version} {self.status.code} {self.status.text}\r\n" ) - headers.setdefault("Content-Type", content_type) + headers.setdefault("Content-Type", content_type or self.content_type) headers.setdefault("Connection", "close") - if chunked: + if self.chunked: headers.setdefault("Transfer-Encoding", "chunked") else: headers.setdefault("Content-Length", content_length) @@ -107,16 +117,17 @@ def send_headers( def send( self, body: str = "", - content_type: str = MIMEType.TYPE_TXT, + content_type: str = None, ) -> None: """ - Send response with `body` over the given socket. - Implicitly calls `send_headers` before sending the body. + Sends response with `body` over the given socket. + Implicitly calls ``_send_headers`` before sending the body. + Should be called only once per response. """ encoded_response_message_body = body.encode("utf-8") - self.send_headers( - content_type=content_type, + self._send_headers( + content_type=content_type or self.content_type, content_length=len(encoded_response_message_body), ) self._send_bytes(self.request.connection, encoded_response_message_body) @@ -138,7 +149,7 @@ def send_file( HTTPResponse(self.request, status=CommonHTTPStatus.NOT_FOUND_404).send() return - self.send_headers( + self._send_headers( content_type=MIMEType.from_file_name(filename), content_length=file_length, ) @@ -151,7 +162,8 @@ def send_chunk_body(self, chunk: str = "") -> None: """ Send chunk of data to the given socket. - Call without `chunk` to finish the session. + Should be used only inside + ``with HTTPResponse(request, chunked=True) as response:`` context manager. :param str chunk: String data to be sent. """ @@ -161,6 +173,16 @@ def send_chunk_body(self, chunk: str = "") -> None: self.request.connection, f"{hex_length}\r\n{chunk}\r\n".encode("utf-8") ) + def __enter__(self): + if self.chunked: + self._send_headers() + return self + + def __exit__(self, *args, **kwargs): + if self.chunked: + self.send_chunk_body("") + return True + @staticmethod def _send_bytes( conn: Union["SocketPool.Socket", "socket.socket"], From a12d4abbc696da7e8e767486d7a85bd44b454cec Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Fri, 23 Dec 2022 18:57:28 +0000 Subject: [PATCH 10/21] Minor changes to docstrings --- adafruit_httpserver/headers.py | 3 +++ adafruit_httpserver/response.py | 32 ++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/adafruit_httpserver/headers.py b/adafruit_httpserver/headers.py index d63fdfa..cf9ea20 100644 --- a/adafruit_httpserver/headers.py +++ b/adafruit_httpserver/headers.py @@ -38,6 +38,9 @@ class HTTPHeaders: headers["content-type"] # 'text/html' + headers["User-Agent"] + # KeyError: User-Agent + "CONTENT-TYPE" in headers # True """ diff --git a/adafruit_httpserver/response.py b/adafruit_httpserver/response.py index d8f9edf..2ca92e6 100644 --- a/adafruit_httpserver/response.py +++ b/adafruit_httpserver/response.py @@ -32,17 +32,32 @@ class HTTPResponse: # Response with 'Content-Length' header @server.route(path, method) def route_func(request): + response = HTTPResponse(request) response.send("Some content", content_type="text/plain") - # or - @server.route(path, method) - def route_func(request): + + # or + + response = HTTPResponse(request) + with response: + response.send(body='Some content', content_type="text/plain") + + # or + 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 = HTTPResponse(request, content_type="text/plain", chunked=True) + with response: + response.send_body_chunk("Some content") + response.send_body_chunk("Some more content") + + # or + with HTTPResponse(request, content_type="text/plain", chunked=True) as response: response.send_body_chunk("Some content") response.send_body_chunk("Some more content") @@ -68,7 +83,8 @@ def __init__( # pylint: disable=too-many-arguments """ Creates an HTTP response. - Sets `status`, ``headers`` and `http_version`. + Sets `status`, ``headers`` and `http_version` + and optionally default ``content_type``. To send the response, call `send` or `send_file`. For chunked response use @@ -122,7 +138,8 @@ def send( """ Sends response with `body` over the given socket. Implicitly calls ``_send_headers`` before sending the body. - Should be called only once per response. + + Should be called **only once** per response. """ encoded_response_message_body = body.encode("utf-8") @@ -139,6 +156,9 @@ def send_file( ) -> None: """ Send response with content of ``filename`` located in ``root_path`` over the given socket. + Implicitly calls ``_send_headers`` before sending the file content. + + Should be called **only once** per response. """ if not root_path.endswith("/"): root_path += "/" @@ -162,7 +182,7 @@ def send_chunk_body(self, chunk: str = "") -> None: """ Send chunk of data to the given socket. - Should be used only inside + Should be used **only** inside ``with HTTPResponse(request, chunked=True) as response:`` context manager. :param str chunk: String data to be sent. From c222d09a71631e2bf53aefed4a9192f609923e62 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Sat, 24 Dec 2022 06:26:23 +0000 Subject: [PATCH 11/21] Changed send_chunk_body to send_chunk for unification --- adafruit_httpserver/response.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/adafruit_httpserver/response.py b/adafruit_httpserver/response.py index 2ca92e6..f2f0869 100644 --- a/adafruit_httpserver/response.py +++ b/adafruit_httpserver/response.py @@ -53,14 +53,14 @@ def route_func(request): response = HTTPResponse(request, content_type="text/plain", chunked=True) with response: - response.send_body_chunk("Some content") - response.send_body_chunk("Some more content") + response.send_chunk("Some content") + response.send_chunk("Some more content") # or with HTTPResponse(request, content_type="text/plain", chunked=True) as response: - response.send_body_chunk("Some content") - response.send_body_chunk("Some more content") + response.send_chunk("Some content") + response.send_chunk("Some more content") """ request: HTTPRequest @@ -88,7 +88,7 @@ def __init__( # pylint: disable=too-many-arguments To send the response, call `send` or `send_file`. For chunked response use - ``with HTTPRequest(request, content_type=..., chunked=True) as r:`` and `send_chunk_body`. + ``with HTTPRequest(request, content_type=..., chunked=True) as r:`` and `send_chunk`. """ self.request = request self.status = status if isinstance(status, HTTPStatus) else HTTPStatus(*status) @@ -136,7 +136,7 @@ def send( content_type: str = None, ) -> None: """ - Sends response with `body` over the given socket. + Sends response with content built from ``body``. Implicitly calls ``_send_headers`` before sending the body. Should be called **only once** per response. @@ -155,7 +155,7 @@ def send_file( root_path: str = "./", ) -> None: """ - Send response with content of ``filename`` located in ``root_path`` over the given socket. + Send response with content of ``filename`` located in ``root_path``. Implicitly calls ``_send_headers`` before sending the file content. Should be called **only once** per response. @@ -178,9 +178,9 @@ def send_file( while bytes_read := file.read(2048): self._send_bytes(self.request.connection, bytes_read) - def send_chunk_body(self, chunk: str = "") -> None: + def send_chunk(self, chunk: str = "") -> None: """ - Send chunk of data to the given socket. + Sends chunk of response. Should be used **only** inside ``with HTTPResponse(request, chunked=True) as response:`` context manager. @@ -200,7 +200,7 @@ def __enter__(self): def __exit__(self, *args, **kwargs): if self.chunked: - self.send_chunk_body("") + self.send_chunk("") return True @staticmethod From 4b61d74e1f055bf2e0d5e1abad3419ebc58058cb Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 26 Dec 2022 00:41:10 +0000 Subject: [PATCH 12/21] Fix: Missing chunk length in ending message due to .lstrip() --- adafruit_httpserver/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_httpserver/response.py b/adafruit_httpserver/response.py index f2f0869..6c69e68 100644 --- a/adafruit_httpserver/response.py +++ b/adafruit_httpserver/response.py @@ -187,7 +187,7 @@ def send_chunk(self, chunk: str = "") -> None: :param str chunk: String data to be sent. """ - hex_length = hex(len(chunk)).lstrip("0x").rstrip("L") + hex_length = hex(len(chunk))[2:] # removing 0x self._send_bytes( self.request.connection, f"{hex_length}\r\n{chunk}\r\n".encode("utf-8") From f0b61a721f1382d5448496d7595e16fc66804135 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Sun, 25 Dec 2022 10:29:28 +0000 Subject: [PATCH 13/21] Prevented from calling .send() multiple times and added deprecation error if handler returns HTTPResponse --- adafruit_httpserver/response.py | 14 +++++++++++++- adafruit_httpserver/server.py | 7 ++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/adafruit_httpserver/response.py b/adafruit_httpserver/response.py index 6c69e68..81b1fdd 100644 --- a/adafruit_httpserver/response.py +++ b/adafruit_httpserver/response.py @@ -98,6 +98,7 @@ def __init__( # pylint: disable=too-many-arguments self.content_type = content_type self.http_version = http_version self.chunked = chunked + self._response_already_sent = False def _send_headers( self, @@ -141,6 +142,9 @@ def send( Should be called **only once** per response. """ + if self._response_already_sent: + raise RuntimeError("Response was already sent") + encoded_response_message_body = body.encode("utf-8") self._send_headers( @@ -148,6 +152,7 @@ def send( content_length=len(encoded_response_message_body), ) self._send_bytes(self.request.connection, encoded_response_message_body) + self._response_already_sent = True def send_file( self, @@ -160,6 +165,9 @@ def send_file( Should be called **only once** per response. """ + if self._response_already_sent: + raise RuntimeError("Response was already sent") + if not root_path.endswith("/"): root_path += "/" try: @@ -177,6 +185,7 @@ def send_file( with open(root_path + filename, "rb") as file: while bytes_read := file.read(2048): self._send_bytes(self.request.connection, bytes_read) + self._response_already_sent = True def send_chunk(self, chunk: str = "") -> None: """ @@ -198,7 +207,10 @@ def __enter__(self): self._send_headers() return self - def __exit__(self, *args, **kwargs): + def __exit__(self, exception_type, exception_value, exception_traceback): + if exception_type is not None: + return False + if self.chunked: self.send_chunk("") return True diff --git a/adafruit_httpserver/server.py b/adafruit_httpserver/server.py index 0e0d5bc..0fe0895 100644 --- a/adafruit_httpserver/server.py +++ b/adafruit_httpserver/server.py @@ -160,7 +160,12 @@ def poll(self): # If a handler for route exists and is callable, call it. if handler is not None and callable(handler): - handler(request) + 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." + ) # If no handler exists and request method is GET, try to serve a file. elif handler is None and request.method == HTTPMethod.GET: From e2e396b7180ebb02d15909665dbe7aeffaf44306 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 26 Dec 2022 12:55:40 +0000 Subject: [PATCH 14/21] Updated existing and added more examples --- docs/examples.rst | 65 +++++++++++++++---- ...r_temperature.py => httpserver_chunked.py} | 33 ++++++---- examples/httpserver_cpu_information.py | 44 +++++++++++++ examples/httpserver_mdns.py | 44 +++++++++++++ examples/httpserver_neopixel.py | 45 +++++++++++++ ...lepolling.py => httpserver_simple_poll.py} | 34 ++++++---- ...mpletest.py => httpserver_simple_serve.py} | 25 ++++--- 7 files changed, 240 insertions(+), 50 deletions(-) rename examples/{httpserver_temperature.py => httpserver_chunked.py} (50%) create mode 100644 examples/httpserver_cpu_information.py create mode 100644 examples/httpserver_mdns.py create mode 100644 examples/httpserver_neopixel.py rename examples/{httpserver_simplepolling.py => httpserver_simple_poll.py} (56%) rename examples/{httpserver_simpletest.py => httpserver_simple_serve.py} (58%) diff --git a/docs/examples.rst b/docs/examples.rst index 0100e37..e6d63d3 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -1,27 +1,64 @@ -Simple test ------------- +Simple file serving +------------------- -Ensure your device works with this simple test. +Serving the content of index.html from the filesystem. -.. literalinclude:: ../examples/httpserver_simpletest.py - :caption: examples/httpserver_simpletest.py +.. literalinclude:: ../examples/httpserver_simple_serve.py + :caption: examples/httpserver_simple_serve.py :linenos: -Temperature test --------------------- +If you want your code to do more than just serve web pages, +use the ``.start()``/``.poll()`` methods as shown in this example. -Send the microcontroller temperature back to the browser with this simple test. +Between calling ``.poll()`` you can do something useful, +for example read a sensor and capture an average or +a running total of the last 10 samples. -.. literalinclude:: ../examples/httpserver_temperature.py - :caption: examples/httpserver_temperature.py +.. literalinclude:: ../examples/httpserver_simple_poll.py + :caption: examples/httpserver_simple_poll.py :linenos: -Simple polling test -------------------- +Server with MDNS +---------------- + +It is possible to use the MDNS protocol to make the server +accessible via a hostname in addition to an IP address. + +In this example, the server is accessible via ``http://custom-mdns-hostname/`` and ``http://custom-mdns-hostname.local/``. + +.. literalinclude:: ../examples/httpserver_cpu_information.py + :caption: examples/httpserver_cpu_information.py + :linenos: + +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. -.. literalinclude:: ../examples/httpserver_simplepolling.py - :caption: examples/httpserver_simplepolling.py +For example by going to ``/change-neopixel-color?r=255&g=0&b=0`` you can change the color of the NeoPixel to red. +Tested on ESP32-S2 Feather. + +.. literalinclude:: ../examples/httpserver_neopixel.py + :caption: examples/httpserver_neopixel.py + :linenos: + +Get CPU information +--------------------- + +You can return data from sensors or any computed value as JSON. +That makes it easy to use the data in other applications. + +.. literalinclude:: ../examples/httpserver_cpu_information.py + :caption: examples/httpserver_cpu_information.py + :linenos: + +Chunked response +--------------------- + +Libraries supports chunked responses. This is useful for streaming data. +To use it, you need to set the ``chunked=True`` when creating a ``HTTPResponse`` object. + +.. literalinclude:: ../examples/httpserver_chunked.py + :caption: examples/httpserver_chunked.py :linenos: diff --git a/examples/httpserver_temperature.py b/examples/httpserver_chunked.py similarity index 50% rename from examples/httpserver_temperature.py rename to examples/httpserver_chunked.py index 94aa541..0b85533 100644 --- a/examples/httpserver_temperature.py +++ b/examples/httpserver_chunked.py @@ -2,31 +2,38 @@ # # SPDX-License-Identifier: Unlicense -from secrets import secrets # pylint: disable=no-name-in-module - -import microcontroller import socketpool import wifi -from adafruit_httpserver.server import HTTPServer from adafruit_httpserver.response import HTTPResponse +from adafruit_httpserver.server import HTTPServer + +import secrets + + +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD -ssid = secrets["ssid"] print("Connecting to", ssid) -wifi.radio.connect(ssid, secrets["password"]) +wifi.radio.connect(ssid, password) print("Connected to", ssid) -print(f"Listening on http://{wifi.radio.ipv4_address}:80") pool = socketpool.SocketPool(wifi.radio) server = HTTPServer(pool) -@server.route("/temperature") -def base(request): # pylint: disable=unused-argument - """Return the current temperature""" - # pylint: disable=no-member - return HTTPResponse(body=f"{str(microcontroller.cpu.temperature)}") +@server.route("/chunked") +def chunked(request): + """ + Return the response with ``Transfer-Encoding: chunked``. + """ + with HTTPResponse(request, chunked=True) as response: + response.send_chunk("Adaf") + response.send_chunk("ruit") + response.send_chunk(" Indus") + response.send_chunk("tr") + response.send_chunk("ies") -# Never returns + +print(f"Listening on http://{wifi.radio.ipv4_address}:80") server.serve_forever(str(wifi.radio.ipv4_address)) diff --git a/examples/httpserver_cpu_information.py b/examples/httpserver_cpu_information.py new file mode 100644 index 0000000..2b6ed8a --- /dev/null +++ b/examples/httpserver_cpu_information.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +import json +import microcontroller +import socketpool +import wifi + +from adafruit_httpserver.mime_type import MIMEType +from adafruit_httpserver.response import HTTPResponse +from adafruit_httpserver.server import HTTPServer + +import secrets + + +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD + +print("Connecting to", ssid) +wifi.radio.connect(ssid, password) +print("Connected to", ssid) + +pool = socketpool.SocketPool(wifi.radio) +server = HTTPServer(pool) + + +@server.route("/cpu-information") +def cpu_information_handler(request): + """ + Return the current CPU temperature, frequency, and voltage as JSON. + """ + + data = { + "temperature": microcontroller.cpu.temperature, + "frequency": microcontroller.cpu.frequency, + "voltage": microcontroller.cpu.voltage, + } + + with HTTPResponse(request, content_type=MIMEType.TYPE_JSON) as response: + response.send(json.dumps(data)) + + +print(f"Listening on http://{wifi.radio.ipv4_address}:80") +server.serve_forever(str(wifi.radio.ipv4_address)) diff --git a/examples/httpserver_mdns.py b/examples/httpserver_mdns.py new file mode 100644 index 0000000..1bd2f83 --- /dev/null +++ b/examples/httpserver_mdns.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +import mdns +import socketpool +import wifi + +from adafruit_httpserver.mime_type import MIMEType +from adafruit_httpserver.response import HTTPResponse +from adafruit_httpserver.server import HTTPServer + +import secrets + + +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD + +print("Connecting to", ssid) +wifi.radio.connect(ssid, password) +print("Connected to", ssid) + +mdns_server = mdns.Server(wifi.radio) +mdns_server.hostname = "custom-mdns-hostname" +mdns_server.advertise_service( + service_type="_http", + protocol="_tcp", + port=80 +) + +pool = socketpool.SocketPool(wifi.radio) +server = HTTPServer(pool) + + +@server.route("/") +def base(request): + """ + Serve the default index.html file. + """ + with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response: + response.send_file("index.html") + + +print(f"Listening on http://{wifi.radio.ipv4_address}:80") +server.serve_forever(str(wifi.radio.ipv4_address)) diff --git a/examples/httpserver_neopixel.py b/examples/httpserver_neopixel.py new file mode 100644 index 0000000..3770b93 --- /dev/null +++ b/examples/httpserver_neopixel.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +import board +import neopixel +import socketpool +import wifi + +from adafruit_httpserver.mime_type import MIMEType +from adafruit_httpserver.response import HTTPResponse +from adafruit_httpserver.server import HTTPServer + +import secrets + + +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD + +print("Connecting to", ssid) +wifi.radio.connect(ssid, password) +print("Connected to", ssid) + +pool = socketpool.SocketPool(wifi.radio) +server = HTTPServer(pool) + +pixel = neopixel.NeoPixel(board.NEOPIXEL, 1) + + +@server.route("/change-neopixel-color") +def change_neopixel_color_handler(request): + """ + Changes the color of the built-in NeoPixel. + """ + r = request.query_params.get("r") + g = request.query_params.get("g") + b = request.query_params.get("b") + + 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_simplepolling.py b/examples/httpserver_simple_poll.py similarity index 56% rename from examples/httpserver_simplepolling.py rename to examples/httpserver_simple_poll.py index d924a5c..a92d0bb 100644 --- a/examples/httpserver_simplepolling.py +++ b/examples/httpserver_simple_poll.py @@ -2,40 +2,48 @@ # # SPDX-License-Identifier: Unlicense -from secrets import secrets # pylint: disable=no-name-in-module - import socketpool import wifi -from adafruit_httpserver.server import HTTPServer +from adafruit_httpserver.mime_type import MIMEType from adafruit_httpserver.response import HTTPResponse +from adafruit_httpserver.server import HTTPServer + +import secrets + + +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD -ssid = secrets["ssid"] print("Connecting to", ssid) -wifi.radio.connect(ssid, secrets["password"]) +wifi.radio.connect(ssid, password) print("Connected to", ssid) -print(f"Listening on http://{wifi.radio.ipv4_address}:80") pool = socketpool.SocketPool(wifi.radio) server = HTTPServer(pool) @server.route("/") -def base(request): # pylint: disable=unused-argument - """Default reponse is /index.html""" - return HTTPResponse(filename="/index.html") +def base(request): + """ + Serve the default index.html file. + """ + with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response: + response.send_file("index.html") -# startup the server +print(f"Listening on http://{wifi.radio.ipv4_address}:80") + +# Start the server. server.start(str(wifi.radio.ipv4_address)) while True: try: - # do something useful in this section, + # Do something useful in this section, # for example read a sensor and capture an average, # or a running total of the last 10 samples - # process any waiting requests + # Process any waiting requests server.poll() - except OSError: + except OSError as error: + print(error) continue diff --git a/examples/httpserver_simpletest.py b/examples/httpserver_simple_serve.py similarity index 58% rename from examples/httpserver_simpletest.py rename to examples/httpserver_simple_serve.py index e1be4b0..81aa8f5 100644 --- a/examples/httpserver_simpletest.py +++ b/examples/httpserver_simple_serve.py @@ -2,29 +2,34 @@ # # SPDX-License-Identifier: Unlicense -from secrets import secrets # pylint: disable=no-name-in-module - import socketpool import wifi -from adafruit_httpserver.server import HTTPServer +from adafruit_httpserver.mime_type import MIMEType from adafruit_httpserver.response import HTTPResponse +from adafruit_httpserver.server import HTTPServer + +import secrets + + +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD -ssid = secrets["ssid"] print("Connecting to", ssid) -wifi.radio.connect(ssid, secrets["password"]) +wifi.radio.connect(ssid, password) print("Connected to", ssid) -print(f"Listening on http://{wifi.radio.ipv4_address}:80") pool = socketpool.SocketPool(wifi.radio) server = HTTPServer(pool) @server.route("/") -def base(request): # pylint: disable=unused-argument - """Default reponse is /index.html""" - return HTTPResponse(filename="/index.html") +def base(request): + """ + Serve the default index.html file. + """ + with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response: + response.send_file("index.html") -# Never returns +print(f"Listening on http://{wifi.radio.ipv4_address}:80") server.serve_forever(str(wifi.radio.ipv4_address)) From 287c5e0681b659b2900af86009e4f27f50d31ac2 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 26 Dec 2022 12:56:05 +0000 Subject: [PATCH 15/21] Updated README.rst --- README.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index fe888f0..b652145 100644 --- a/README.rst +++ b/README.rst @@ -21,12 +21,14 @@ Introduction :target: https://github.com/psf/black :alt: Code Style: Black -Simple HTTP Server for CircuitPython. +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. -- Simple routing available to serve computed results. +- Routing for serving computed responses from handler. +- Gives access to request headers, query parameters, body and address from which the request came. +- Supports chunked transfer encoding. Dependencies From 21aa09e8ea048dae33c6cebed7d795a89fbe963b Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 26 Dec 2022 13:08:52 +0000 Subject: [PATCH 16/21] Fixed CI for examples/ --- examples/httpserver_chunked.py | 6 +++--- examples/httpserver_cpu_information.py | 6 +++--- examples/httpserver_mdns.py | 12 ++++-------- examples/httpserver_neopixel.py | 6 +++--- examples/httpserver_simple_poll.py | 6 +++--- examples/httpserver_simple_serve.py | 6 +++--- 6 files changed, 19 insertions(+), 23 deletions(-) diff --git a/examples/httpserver_chunked.py b/examples/httpserver_chunked.py index 0b85533..7d40e08 100644 --- a/examples/httpserver_chunked.py +++ b/examples/httpserver_chunked.py @@ -2,16 +2,16 @@ # # SPDX-License-Identifier: Unlicense +import secrets # pylint: disable=no-name-in-module + import socketpool import wifi from adafruit_httpserver.response import HTTPResponse from adafruit_httpserver.server import HTTPServer -import secrets - -ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD # pylint: disable=no-member print("Connecting to", ssid) wifi.radio.connect(ssid, password) diff --git a/examples/httpserver_cpu_information.py b/examples/httpserver_cpu_information.py index 2b6ed8a..74645ea 100644 --- a/examples/httpserver_cpu_information.py +++ b/examples/httpserver_cpu_information.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Unlicense +import secrets # pylint: disable=no-name-in-module + import json import microcontroller import socketpool @@ -11,10 +13,8 @@ from adafruit_httpserver.response import HTTPResponse from adafruit_httpserver.server import HTTPServer -import secrets - -ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD # pylint: disable=no-member print("Connecting to", ssid) wifi.radio.connect(ssid, password) diff --git a/examples/httpserver_mdns.py b/examples/httpserver_mdns.py index 1bd2f83..ca77a93 100644 --- a/examples/httpserver_mdns.py +++ b/examples/httpserver_mdns.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Unlicense +import secrets # pylint: disable=no-name-in-module + import mdns import socketpool import wifi @@ -10,10 +12,8 @@ from adafruit_httpserver.response import HTTPResponse from adafruit_httpserver.server import HTTPServer -import secrets - -ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD # pylint: disable=no-member print("Connecting to", ssid) wifi.radio.connect(ssid, password) @@ -21,11 +21,7 @@ mdns_server = mdns.Server(wifi.radio) mdns_server.hostname = "custom-mdns-hostname" -mdns_server.advertise_service( - service_type="_http", - protocol="_tcp", - port=80 -) +mdns_server.advertise_service(service_type="_http", protocol="_tcp", port=80) pool = socketpool.SocketPool(wifi.radio) server = HTTPServer(pool) diff --git a/examples/httpserver_neopixel.py b/examples/httpserver_neopixel.py index 3770b93..077b7f1 100644 --- a/examples/httpserver_neopixel.py +++ b/examples/httpserver_neopixel.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Unlicense +import secrets # pylint: disable=no-name-in-module + import board import neopixel import socketpool @@ -11,10 +13,8 @@ from adafruit_httpserver.response import HTTPResponse from adafruit_httpserver.server import HTTPServer -import secrets - -ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD # pylint: disable=no-member print("Connecting to", ssid) wifi.radio.connect(ssid, password) diff --git a/examples/httpserver_simple_poll.py b/examples/httpserver_simple_poll.py index a92d0bb..dafa2d4 100644 --- a/examples/httpserver_simple_poll.py +++ b/examples/httpserver_simple_poll.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Unlicense +import secrets # pylint: disable=no-name-in-module + import socketpool import wifi @@ -9,10 +11,8 @@ from adafruit_httpserver.response import HTTPResponse from adafruit_httpserver.server import HTTPServer -import secrets - -ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD # pylint: disable=no-member print("Connecting to", ssid) wifi.radio.connect(ssid, password) diff --git a/examples/httpserver_simple_serve.py b/examples/httpserver_simple_serve.py index 81aa8f5..eccd9ce 100644 --- a/examples/httpserver_simple_serve.py +++ b/examples/httpserver_simple_serve.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Unlicense +import secrets # pylint: disable=no-name-in-module + import socketpool import wifi @@ -9,10 +11,8 @@ from adafruit_httpserver.response import HTTPResponse from adafruit_httpserver.server import HTTPServer -import secrets - -ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD +ssid, password = secrets.WIFI_SSID, secrets.WIFI_PASSWORD # pylint: disable=no-member print("Connecting to", ssid) wifi.radio.connect(ssid, password) From d14a13a1c472f7b9aa8d6d3519732012bd62a735 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Wed, 28 Dec 2022 05:51:40 +0000 Subject: [PATCH 17/21] Fix: Content type not setting properly --- adafruit_httpserver/response.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/adafruit_httpserver/response.py b/adafruit_httpserver/response.py index 81b1fdd..dbd0a03 100644 --- a/adafruit_httpserver/response.py +++ b/adafruit_httpserver/response.py @@ -70,13 +70,21 @@ def route_func(request): status: HTTPStatus headers: HTTPHeaders content_type: str + """ + Defaults to ``text/plain`` if not set. + + Can be explicitly provided in the constructor, in `send()` or + implicitly determined from filename in `send_file()`. + + Common MIME types are defined in `adafruit_httpserver.mime_type.MIMEType`. + """ def __init__( # pylint: disable=too-many-arguments self, request: HTTPRequest, status: Union[HTTPStatus, Tuple[int, str]] = CommonHTTPStatus.OK_200, headers: Union[HTTPHeaders, Dict[str, str]] = None, - content_type: str = MIMEType.TYPE_TXT, + content_type: str = None, http_version: str = "HTTP/1.1", chunked: bool = False, ) -> None: @@ -103,7 +111,7 @@ def __init__( # pylint: disable=too-many-arguments def _send_headers( self, content_length: Optional[int] = None, - content_type: str = MIMEType.TYPE_TXT, + content_type: str = None, ) -> None: """ Sends headers. @@ -116,7 +124,9 @@ def _send_headers( f"{self.http_version} {self.status.code} {self.status.text}\r\n" ) - headers.setdefault("Content-Type", content_type or self.content_type) + headers.setdefault( + "Content-Type", content_type or self.content_type or MIMEType.TYPE_TXT + ) headers.setdefault("Connection", "close") if self.chunked: headers.setdefault("Transfer-Encoding", "chunked") From c758e519c0bb266e191d955e28706e48791c1d82 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Wed, 28 Dec 2022 17:44:05 +0000 Subject: [PATCH 18/21] Changed root to root_path in docstrings --- adafruit_httpserver/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_httpserver/server.py b/adafruit_httpserver/server.py index 0fe0895..6c44dbf 100644 --- a/adafruit_httpserver/server.py +++ b/adafruit_httpserver/server.py @@ -64,7 +64,7 @@ def serve_forever(self, host: str, port: int = 80, root_path: str = "") -> None: :param str host: host name or IP address :param int port: port - :param str root: root directory to serve files from + :param str root_path: root directory to serve files from """ self.start(host, port, root_path) @@ -81,7 +81,7 @@ def start(self, host: str, port: int = 80, root_path: str = "") -> None: :param str host: host name or IP address :param int port: port - :param str root: root directory to serve files from + :param str root_path: root directory to serve files from """ self.root_path = root_path From 140c0e16b7c3b34e619e9af0311df4310c1f9131 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 2 Jan 2023 16:07:46 +0000 Subject: [PATCH 19/21] Minor adjustments to examples --- docs/examples.rst | 2 +- examples/httpserver_chunked.py | 3 ++- examples/httpserver_cpu_information.py | 3 ++- examples/httpserver_mdns.py | 3 ++- examples/httpserver_neopixel.py | 3 ++- examples/httpserver_simple_poll.py | 3 ++- examples/httpserver_simple_serve.py | 3 ++- 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index e6d63d3..6080b32 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -56,7 +56,7 @@ That makes it easy to use the data in other applications. Chunked response --------------------- -Libraries supports chunked responses. This is useful for streaming data. +Library supports chunked responses. This is useful for streaming data. To use it, you need to set the ``chunked=True`` when creating a ``HTTPResponse`` object. .. literalinclude:: ../examples/httpserver_chunked.py diff --git a/examples/httpserver_chunked.py b/examples/httpserver_chunked.py index 7d40e08..ae519ec 100644 --- a/examples/httpserver_chunked.py +++ b/examples/httpserver_chunked.py @@ -7,6 +7,7 @@ import socketpool import wifi +from adafruit_httpserver.request import HTTPRequest from adafruit_httpserver.response import HTTPResponse from adafruit_httpserver.server import HTTPServer @@ -22,7 +23,7 @@ @server.route("/chunked") -def chunked(request): +def chunked(request: HTTPRequest): """ Return the response with ``Transfer-Encoding: chunked``. """ diff --git a/examples/httpserver_cpu_information.py b/examples/httpserver_cpu_information.py index 74645ea..cf3d13b 100644 --- a/examples/httpserver_cpu_information.py +++ b/examples/httpserver_cpu_information.py @@ -10,6 +10,7 @@ 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 @@ -25,7 +26,7 @@ @server.route("/cpu-information") -def cpu_information_handler(request): +def cpu_information_handler(request: HTTPRequest): """ Return the current CPU temperature, frequency, and voltage as JSON. """ diff --git a/examples/httpserver_mdns.py b/examples/httpserver_mdns.py index ca77a93..d2228c9 100644 --- a/examples/httpserver_mdns.py +++ b/examples/httpserver_mdns.py @@ -9,6 +9,7 @@ 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 @@ -28,7 +29,7 @@ @server.route("/") -def base(request): +def base(request: HTTPRequest): """ Serve the default index.html file. """ diff --git a/examples/httpserver_neopixel.py b/examples/httpserver_neopixel.py index 077b7f1..ab7dabd 100644 --- a/examples/httpserver_neopixel.py +++ b/examples/httpserver_neopixel.py @@ -10,6 +10,7 @@ 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 @@ -27,7 +28,7 @@ @server.route("/change-neopixel-color") -def change_neopixel_color_handler(request): +def change_neopixel_color_handler(request: HTTPRequest): """ Changes the color of the built-in NeoPixel. """ diff --git a/examples/httpserver_simple_poll.py b/examples/httpserver_simple_poll.py index dafa2d4..db876c4 100644 --- a/examples/httpserver_simple_poll.py +++ b/examples/httpserver_simple_poll.py @@ -8,6 +8,7 @@ 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 @@ -23,7 +24,7 @@ @server.route("/") -def base(request): +def base(request: HTTPRequest): """ Serve the default index.html file. """ diff --git a/examples/httpserver_simple_serve.py b/examples/httpserver_simple_serve.py index eccd9ce..632c234 100644 --- a/examples/httpserver_simple_serve.py +++ b/examples/httpserver_simple_serve.py @@ -8,6 +8,7 @@ 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 @@ -23,7 +24,7 @@ @server.route("/") -def base(request): +def base(request: HTTPRequest): """ Serve the default index.html file. """ From c609a821d5d9676a8f464b9adef1abe7a7dff388 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 2 Jan 2023 17:22:53 +0000 Subject: [PATCH 20/21] Changed address to client_address to match CPython's http.server module naming --- README.rst | 2 +- adafruit_httpserver/request.py | 10 +++++----- adafruit_httpserver/server.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index b652145..bb54843 100644 --- a/README.rst +++ b/README.rst @@ -27,7 +27,7 @@ HTTP Server for CircuitPython. - HTTP 1.1. - Serves files from a designated root. - Routing for serving computed responses from handler. -- Gives access to request headers, query parameters, body and address from which the request came. +- Gives access to request headers, query parameters, body and client's address, the one from which the request came. - Supports chunked transfer encoding. diff --git a/adafruit_httpserver/request.py b/adafruit_httpserver/request.py index 7b10b2c..9ee5fc0 100644 --- a/adafruit_httpserver/request.py +++ b/adafruit_httpserver/request.py @@ -28,13 +28,13 @@ class HTTPRequest: Socket object usable to send and receive data on the connection. """ - address: Tuple[str, int] + client_address: Tuple[str, int] """ - Address bound to the socket on the other end of the connection. + Address and port bound to the socket on the other end of the connection. Example:: - request.address + request.client_address # ('192.168.137.1', 40684) """ @@ -73,11 +73,11 @@ class HTTPRequest: def __init__( self, connection: Union["SocketPool.Socket", "socket.socket"], - address: Tuple[str, int], + client_address: Tuple[str, int], raw_request: bytes = None, ) -> None: self.connection = connection - self.address = address + self.client_address = client_address self.raw_request = raw_request if raw_request is None: diff --git a/adafruit_httpserver/server.py b/adafruit_httpserver/server.py index 6c44dbf..0a64541 100644 --- a/adafruit_httpserver/server.py +++ b/adafruit_httpserver/server.py @@ -133,7 +133,7 @@ def poll(self): the application callable will be invoked. """ try: - conn, address = self._sock.accept() + conn, client_address = self._sock.accept() with conn: conn.settimeout(self._timeout) @@ -144,7 +144,7 @@ def poll(self): if not header_bytes: return - request = HTTPRequest(conn, address, header_bytes) + request = HTTPRequest(conn, client_address, header_bytes) content_length = int(request.headers.get("Content-Length", 0)) received_body_bytes = request.body From 5b0135a6303221c088548b750e32aa15806cd43d Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 2 Jan 2023 17:47:46 +0000 Subject: [PATCH 21/21] Changed version in docs/conf.py from 1.1.0 to 1.0, similar to other libraries --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index e46d17f..5dda03c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,9 +65,9 @@ # built documents. # # The short X.Y version. -version = "1.1.0" +version = "1.0" # The full version, including alpha/beta/rc tags. -release = "1.1.0" +release = "1.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages.