Skip to content

Commit 00d3247

Browse files
committed
Refactor for unifying the HTTPResponse API
1 parent c1d2f55 commit 00d3247

File tree

3 files changed

+135
-169
lines changed

3 files changed

+135
-169
lines changed

adafruit_httpserver/request.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
"""
99

1010
try:
11-
from typing import Dict, Tuple
11+
from typing import Dict, Tuple, Union
12+
from socket import socket
13+
from socketpool import SocketPool
1214
except ImportError:
1315
pass
1416

@@ -21,6 +23,21 @@ class HTTPRequest:
2123
It is passed as first argument to route handlers.
2224
"""
2325

26+
connection: Union["SocketPool.Socket", "socket.socket"]
27+
"""
28+
Socket object usable to send and receive data on the connection.
29+
"""
30+
31+
address: Tuple[str, int]
32+
"""
33+
Address bound to the socket on the other end of the connection.
34+
35+
Example::
36+
37+
request.address
38+
# ('192.168.137.1', 40684)
39+
"""
40+
2441
method: str
2542
"""Request method e.g. "GET" or "POST"."""
2643

@@ -53,7 +70,14 @@ class HTTPRequest:
5370
Should **not** be modified directly.
5471
"""
5572

56-
def __init__(self, raw_request: bytes = None) -> None:
73+
def __init__(
74+
self,
75+
connection: Union["SocketPool.Socket", "socket.socket"],
76+
address: Tuple[str, int],
77+
raw_request: bytes = None,
78+
) -> None:
79+
self.connection = connection
80+
self.address = address
5781
self.raw_request = raw_request
5882

5983
if raw_request is None:

adafruit_httpserver/response.py

Lines changed: 94 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -14,191 +14,152 @@
1414
except ImportError:
1515
pass
1616

17-
from errno import EAGAIN, ECONNRESET
1817
import os
19-
18+
from errno import EAGAIN, ECONNRESET
2019

2120
from .mime_type import MIMEType
21+
from .request import HTTPRequest
2222
from .status import HTTPStatus, CommonHTTPStatus
2323
from .headers import HTTPHeaders
2424

2525

2626
class HTTPResponse:
27-
"""Details of an HTTP response. Use in `HTTPServer.route` decorator functions."""
27+
"""
28+
Response to a given `HTTPRequest`. Use in `HTTPServer.route` decorator functions.
29+
30+
Example::
31+
32+
# Response with 'Content-Length' header
33+
@server.route(path, method)
34+
def route_func(request):
35+
response = HTTPResponse(request)
36+
response.send("Some content", content_type="text/plain")
37+
38+
# Response with 'Transfer-Encoding: chunked' header
39+
@server.route(path, method)
40+
def route_func(request):
41+
response = HTTPResponse(request, content_type="text/html")
42+
response.send_headers(content_type="text/plain", chunked=True)
43+
response.send_body_chunk("Some content")
44+
response.send_body_chunk("Some more content")
45+
response.send_body_chunk("") # Send empty packet to finish chunked stream
46+
"""
47+
48+
request: HTTPRequest
49+
"""The request that this is a response to."""
2850

2951
http_version: str
3052
status: HTTPStatus
3153
headers: HTTPHeaders
32-
content_type: str
33-
cache: Optional[int]
34-
filename: Optional[str]
35-
root_path: str
36-
37-
body: str
3854

3955
def __init__( # pylint: disable=too-many-arguments
4056
self,
57+
request: HTTPRequest,
4158
status: Union[HTTPStatus, Tuple[int, str]] = CommonHTTPStatus.OK_200,
42-
body: str = "",
4359
headers: Union[HTTPHeaders, Dict[str, str]] = None,
44-
content_type: str = MIMEType.TYPE_TXT,
45-
cache: Optional[int] = 0,
46-
filename: Optional[str] = None,
47-
root_path: str = "",
4860
http_version: str = "HTTP/1.1",
4961
) -> None:
5062
"""
5163
Creates an HTTP response.
5264
53-
Returns ``body`` if ``filename`` is ``None``, otherwise returns contents of ``filename``.
65+
Sets `status`, ``headers`` and `http_version`.
66+
67+
To send the response, call `send` or `send_file`.
68+
For chunked response ``send_headers(chunked=True)`` and then `send_chunk_body`.
5469
"""
70+
self.request = request
5571
self.status = status if isinstance(status, HTTPStatus) else HTTPStatus(*status)
56-
self.body = body
5772
self.headers = (
5873
headers.copy() if isinstance(headers, HTTPHeaders) else HTTPHeaders(headers)
5974
)
60-
self.content_type = content_type
61-
self.cache = cache
62-
self.filename = filename
63-
self.root_path = root_path
6475
self.http_version = http_version
6576

66-
@staticmethod
67-
def _construct_response_bytes( # pylint: disable=too-many-arguments
68-
http_version: str = "HTTP/1.1",
69-
status: HTTPStatus = CommonHTTPStatus.OK_200,
77+
def send_headers(
78+
self,
79+
content_length: Optional[int] = None,
7080
content_type: str = MIMEType.TYPE_TXT,
71-
content_length: Union[int, None] = None,
72-
cache: int = 0,
73-
headers: Dict[str, str] = None,
74-
body: str = "",
7581
chunked: bool = False,
76-
) -> bytes:
77-
"""Constructs the response bytes from the given parameters."""
82+
) -> None:
83+
"""
84+
Send response with `body` over the given socket.
85+
"""
86+
headers = self.headers.copy()
7887

79-
response_message_header = f"{http_version} {status.code} {status.text}\r\n"
80-
encoded_response_message_body = body.encode("utf-8")
88+
response_message_header = (
89+
f"{self.http_version} {self.status.code} {self.status.text}\r\n"
90+
)
8191

8292
headers.setdefault("Content-Type", content_type)
83-
headers.setdefault(
84-
"Content-Length", content_length or len(encoded_response_message_body)
85-
)
8693
headers.setdefault("Connection", "close")
87-
88-
response_headers.setdefault("Content-Type", content_type)
89-
response_headers.setdefault("Connection", "close")
9094
if chunked:
91-
response_headers.setdefault("Transfer-Encoding", "chunked")
95+
headers.setdefault("Transfer-Encoding", "chunked")
9296
else:
93-
response_headers.setdefault("Content-Length", content_length or len(body))
94-
95-
for header, value in response_headers.items():
96-
response += f"{header}: {value}\r\n"
97-
98-
response += f"Cache-Control: max-age={cache}\r\n"
97+
headers.setdefault("Content-Length", content_length)
9998

100-
response += f"\r\n{body}"
99+
for header, value in headers.items():
100+
response_message_header += f"{header}: {value}\r\n"
101+
response_message_header += "\r\n"
101102

102-
return response.encode("utf-8")
103+
self._send_bytes(
104+
self.request.connection, response_message_header.encode("utf-8")
105+
)
103106

104-
def send(self, conn: Union["SocketPool.Socket", "socket.socket"]) -> None:
107+
def send(
108+
self,
109+
body: str = "",
110+
content_type: str = MIMEType.TYPE_TXT,
111+
) -> None:
105112
"""
106-
Send the constructed response over the given socket.
113+
Send response with `body` over the given socket.
114+
Implicitly calls `send_headers` before sending the body.
107115
"""
116+
encoded_response_message_body = body.encode("utf-8")
108117

109-
if self.filename is not None:
110-
try:
111-
file_length = os.stat(self.root_path + self.filename)[6]
112-
self._send_file_response(
113-
conn,
114-
filename=self.filename,
115-
root_path=self.root_path,
116-
file_length=file_length,
117-
headers=self.headers,
118-
)
119-
except OSError:
120-
self._send_response(
121-
conn,
122-
status=CommonHTTPStatus.NOT_FOUND_404,
123-
content_type=MIMEType.TYPE_TXT,
124-
body=f"{CommonHTTPStatus.NOT_FOUND_404} {self.filename}",
125-
)
126-
else:
127-
self._send_response(
128-
conn,
129-
status=self.status,
130-
content_type=self.content_type,
131-
headers=self.headers,
132-
body=self.body,
133-
)
134-
135-
def send_chunk_headers(
136-
self, conn: Union["SocketPool.Socket", "socket.socket"]
137-
) -> None:
138-
"""Send Headers for a chunked response over the given socket."""
139-
self._send_bytes(
140-
conn,
141-
self._construct_response_bytes(
142-
status=self.status,
143-
content_type=self.content_type,
144-
chunked=True,
145-
cache=self.cache,
146-
body="",
147-
),
118+
self.send_headers(
119+
content_type=content_type,
120+
content_length=len(encoded_response_message_body),
148121
)
122+
self._send_bytes(self.request.connection, encoded_response_message_body)
149123

150-
def send_body_chunk(
151-
self, conn: Union["SocketPool.Socket", "socket.socket"], chunk: str
124+
def send_file(
125+
self,
126+
filename: str = "index.html",
127+
root_path: str = "./",
152128
) -> None:
153-
"""Send chunk of data to the given socket. Send an empty("") chunk to finish the session.
129+
"""
130+
Send response with content of ``filename`` located in ``root_path`` over the given socket.
131+
"""
132+
if not root_path.endswith("/"):
133+
root_path += "/"
134+
try:
135+
file_length = os.stat(root_path + filename)[6]
136+
except OSError:
137+
# If the file doesn't exist, return 404.
138+
HTTPResponse(self.request, status=CommonHTTPStatus.NOT_FOUND_404).send()
139+
return
140+
141+
self.send_headers(
142+
content_type=MIMEType.from_file_name(filename),
143+
content_length=file_length,
144+
)
154145

155-
:param Union["SocketPool.Socket", "socket.socket"] conn: Current connection.
156-
:param str chunk: String data to be sent.
146+
with open(root_path + filename, "rb") as file:
147+
while bytes_read := file.read(2048):
148+
self._send_bytes(self.request.connection, bytes_read)
149+
150+
def send_chunk_body(self, chunk: str = "") -> None:
157151
"""
158-
size = "%X\r\n".encode() % len(chunk)
159-
self._send_bytes(conn, size)
160-
self._send_bytes(conn, chunk.encode() + b"\r\n")
152+
Send chunk of data to the given socket.
161153
162-
def _send_response( # pylint: disable=too-many-arguments
163-
self,
164-
conn: Union["SocketPool.Socket", "socket.socket"],
165-
status: HTTPStatus,
166-
content_type: str,
167-
body: str,
168-
headers: HTTPHeaders = None,
169-
):
170-
self._send_bytes(
171-
conn,
172-
self._construct_response_bytes(
173-
status=status,
174-
content_type=content_type,
175-
cache=self.cache,
176-
headers=headers,
177-
body=body,
178-
),
179-
)
154+
Call without `chunk` to finish the session.
155+
156+
:param str chunk: String data to be sent.
157+
"""
158+
hex_length = hex(len(chunk)).lstrip("0x").rstrip("L")
180159

181-
def _send_file_response( # pylint: disable=too-many-arguments
182-
self,
183-
conn: Union["SocketPool.Socket", "socket.socket"],
184-
filename: str,
185-
root_path: str,
186-
file_length: int,
187-
headers: HTTPHeaders = None,
188-
):
189160
self._send_bytes(
190-
conn,
191-
self._construct_response_bytes(
192-
status=self.status,
193-
content_type=MIMEType.from_file_name(filename),
194-
content_length=file_length,
195-
cache=self.cache,
196-
headers=headers,
197-
),
161+
self.request.connection, f"{hex_length}\r\n{chunk}\r\n".encode("utf-8")
198162
)
199-
with open(root_path + filename, "rb") as file:
200-
while bytes_read := file.read(2048):
201-
self._send_bytes(conn, bytes_read)
202163

203164
@staticmethod
204165
def _send_bytes(

0 commit comments

Comments
 (0)