Skip to content

Commit 9a021bd

Browse files
committed
Refactor of HTTPResponse, addition of new attributes
1 parent bb6ef75 commit 9a021bd

File tree

1 file changed

+111
-50
lines changed

1 file changed

+111
-50
lines changed

adafruit_httpserver/response.py

Lines changed: 111 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,158 @@
11
try:
2-
from typing import Any, Optional
2+
from typing import Optional, Dict, Union
3+
from socket import socket
34
except ImportError:
45
pass
56

67
from errno import EAGAIN, ECONNRESET
78
import os
89

10+
from socketpool import SocketPool
11+
912
from .mime_type import MIMEType
10-
from .status import HTTPStatus
13+
from .status import HTTPStatus, OK_200, NOT_FOUND_404
1114

1215
class HTTPResponse:
1316
"""Details of an HTTP response. Use in `HTTPServer.route` decorator functions."""
1417

15-
_HEADERS_FORMAT = (
16-
"HTTP/1.1 {}\r\n"
17-
"Content-Type: {}\r\n"
18-
"Content-Length: {}\r\n"
19-
"Connection: close\r\n"
20-
"\r\n"
21-
)
18+
http_version: str
19+
status: HTTPStatus
20+
headers: Dict[str, str]
21+
content_type: str
22+
23+
filename: Optional[str]
24+
root_directory: str
25+
26+
body: str
2227

2328
def __init__(
2429
self,
25-
*,
26-
status: tuple = HTTPStatus.OK,
27-
content_type: str = MIMEType.TEXT_PLAIN,
30+
status: HTTPStatus = OK_200,
2831
body: str = "",
32+
headers: Dict[str, str] = None,
33+
content_type: str = MIMEType.TEXT_PLAIN,
2934
filename: Optional[str] = None,
30-
root: str = "",
35+
root_directory: str = "",
36+
http_version: str = "HTTP/1.1"
3137
) -> None:
32-
"""Create an HTTP response.
33-
34-
:param tuple status: The HTTP status code to return, as a tuple of (int, "message").
35-
Common statuses are available in `HTTPStatus`.
36-
:param str content_type: The MIME type of the data being returned.
37-
Common MIME types are available in `MIMEType`.
38-
:param Union[str|bytes] body:
39-
The data to return in the response body, if ``filename`` is not ``None``.
40-
:param str filename: If not ``None``,
41-
return the contents of the specified file, and ignore ``body``.
42-
:param str root: root directory for filename, without a trailing slash
4338
"""
39+
Creates an HTTP response.
40+
41+
Returns `body` if `filename` is `None`, otherwise returns the contents of `filename`.
42+
"""
43+
4444
self.status = status
45+
self.body = body
46+
self.headers = headers or {}
4547
self.content_type = content_type
46-
self.body = body.encode() if isinstance(body, str) else body
4748
self.filename = filename
49+
self.root_directory = root_directory
50+
self.http_version = http_version
4851

49-
self.root = root
50-
51-
def send(self, conn: Any) -> None:
52-
# TODO: Use Union[SocketPool.Socket | socket.socket] for the type annotation in some way.
52+
@staticmethod
53+
def _construct_response_bytes(
54+
http_version: str = "HTTP/1.1",
55+
status: HTTPStatus = OK_200,
56+
content_type: str = "text/plain",
57+
content_length: Union[int, None] = None,
58+
headers: Dict[str, str] = None,
59+
body: str = "",
60+
) -> str:
5361
"""Send the constructed response over the given socket."""
54-
if self.filename:
62+
63+
response = f"{http_version} {status.code} {status.text}\r\n"
64+
65+
headers = headers or {}
66+
67+
headers["Content-Type"] = content_type
68+
headers["Content-Length"] = content_length if content_length is not None else len(body)
69+
headers["Connection"] = "close"
70+
71+
for header, value in headers.items():
72+
response += f"{header}: {value}\r\n"
73+
74+
response += f"\r\n{body}"
75+
76+
return response
77+
78+
def send(self, conn: Union[SocketPool.Socket, socket.socket]) -> None:
79+
"""
80+
Send the constructed response over the given socket.
81+
"""
82+
83+
if self.filename is not None:
5584
try:
56-
file_length = os.stat(self.root + self.filename)[6]
57-
self._send_file_response(conn, self.filename, self.root, file_length)
85+
file_length = os.stat(self.root_directory + self.filename)[6]
86+
self._send_file_response(
87+
conn,
88+
filename = self.filename,
89+
root_directory = self.root_directory,
90+
file_length = file_length
91+
)
5892
except OSError:
5993
self._send_response(
6094
conn,
61-
HTTPStatus.NOT_FOUND,
62-
MIMEType.TEXT_PLAIN,
63-
f"{HTTPStatus.NOT_FOUND} {self.filename}\r\n",
95+
status = NOT_FOUND_404,
96+
content_type = MIMEType.TEXT_PLAIN,
97+
body = f"{NOT_FOUND_404} {self.filename}",
6498
)
6599
else:
66-
self._send_response(conn, self.status, self.content_type, self.body)
100+
self._send_response(
101+
conn,
102+
status = self.status,
103+
content_type = self.content_type,
104+
headers = self.headers,
105+
body = self.body,
106+
)
67107

68-
def _send_response(self, conn, status, content_type, body):
108+
def _send_response(
109+
self,
110+
conn: Union[SocketPool.Socket, socket.socket],
111+
status: HTTPStatus,
112+
content_type: str,
113+
body: str,
114+
headers: Dict[str, str] = None
115+
):
69116
self._send_bytes(
70-
conn, self._HEADERS_FORMAT.format(status, content_type, len(body))
117+
conn,
118+
self._construct_response_bytes(
119+
status = status,
120+
content_type = content_type,
121+
headers = headers,
122+
body = body,
123+
)
71124
)
72-
self._send_bytes(conn, body)
73125

74-
def _send_file_response(self, conn, filename, root, file_length):
126+
def _send_file_response(
127+
self,
128+
conn: Union[SocketPool.Socket, socket.socket],
129+
filename: str,
130+
root_directory: str,
131+
file_length: int
132+
):
75133
self._send_bytes(
76134
conn,
77-
self._HEADERS_FORMAT.format(
78-
self.status, MIMEType.mime_type(filename), file_length
135+
self._construct_response_bytes(
136+
status = self.status,
137+
content_type = MIMEType.mime_type(filename),
138+
content_length = file_length
79139
),
80140
)
81-
with open(root + filename, "rb") as file:
141+
with open(root_directory + filename, "rb") as file:
82142
while bytes_read := file.read(2048):
83143
self._send_bytes(conn, bytes_read)
84144

85145
@staticmethod
86-
def _send_bytes(conn, buf):
146+
def _send_bytes(
147+
conn: Union[SocketPool.Socket, socket.socket],
148+
buffer: Union[bytes, bytearray, memoryview],
149+
):
87150
bytes_sent = 0
88-
bytes_to_send = len(buf)
89-
view = memoryview(buf)
151+
bytes_to_send = len(buffer)
152+
view = memoryview(buffer)
90153
while bytes_sent < bytes_to_send:
91154
try:
92155
bytes_sent += conn.send(view[bytes_sent:])
93156
except OSError as exc:
94-
if exc.errno == EAGAIN:
95-
continue
96-
if exc.errno == ECONNRESET:
97-
return
157+
if exc.errno == EAGAIN: continue
158+
if exc.errno == ECONNRESET: return

0 commit comments

Comments
 (0)