Skip to content

Commit e4a7e0e

Browse files
authored
Merge pull request #29 from michalpokusa/httpheaders-and-some-fixes
Case insensitive HTTPHeaders, HTTPResponse context manager and some fixes
2 parents 5de66fa + 5b0135a commit e4a7e0e

14 files changed

+609
-267
lines changed

README.rst

+4-2
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ Introduction
2121
:target: https://github.com/psf/black
2222
:alt: Code Style: Black
2323

24-
Simple HTTP Server for CircuitPython.
24+
HTTP Server for CircuitPython.
2525

2626
- Supports `socketpool` or `socket` as a source of sockets; can be used in CPython.
2727
- HTTP 1.1.
2828
- Serves files from a designated root.
29-
- Simple routing available to serve computed results.
29+
- Routing for serving computed responses from handler.
30+
- Gives access to request headers, query parameters, body and client's address, the one from which the request came.
31+
- Supports chunked transfer encoding.
3032

3133

3234
Dependencies

adafruit_httpserver/headers.py

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2022 Dan Halbert for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
"""
5+
`adafruit_httpserver.headers.HTTPHeaders`
6+
====================================================
7+
* Author(s): Michał Pokusa
8+
"""
9+
10+
try:
11+
from typing import Dict, Tuple
12+
except ImportError:
13+
pass
14+
15+
16+
class HTTPHeaders:
17+
"""
18+
A dict-like class for storing HTTP headers.
19+
20+
Allows access to headers using **case insensitive** names.
21+
22+
Does **not** implement all dict methods.
23+
24+
Examples::
25+
26+
headers = HTTPHeaders({"Content-Type": "text/html", "Content-Length": "1024"})
27+
28+
len(headers)
29+
# 2
30+
31+
headers.setdefault("Access-Control-Allow-Origin", "*")
32+
headers["Access-Control-Allow-Origin"]
33+
# '*'
34+
35+
headers["Content-Length"]
36+
# '1024'
37+
38+
headers["content-type"]
39+
# 'text/html'
40+
41+
headers["User-Agent"]
42+
# KeyError: User-Agent
43+
44+
"CONTENT-TYPE" in headers
45+
# True
46+
"""
47+
48+
_storage: Dict[str, Tuple[str, str]]
49+
50+
def __init__(self, headers: Dict[str, str] = None) -> None:
51+
52+
headers = headers or {}
53+
54+
self._storage = {key.lower(): [key, value] for key, value in headers.items()}
55+
56+
def get(self, name: str, default: str = None):
57+
"""Returns the value for the given header name, or default if not found."""
58+
return self._storage.get(name.lower(), [None, default])[1]
59+
60+
def setdefault(self, name: str, default: str = None):
61+
"""Sets the value for the given header name if it does not exist."""
62+
return self._storage.setdefault(name.lower(), [name, default])[1]
63+
64+
def items(self):
65+
"""Returns a list of (name, value) tuples."""
66+
return dict(self._storage.values()).items()
67+
68+
def keys(self):
69+
"""Returns a list of header names."""
70+
return dict(self._storage.values()).keys()
71+
72+
def values(self):
73+
"""Returns a list of header values."""
74+
return dict(self._storage.values()).values()
75+
76+
def update(self, headers: Dict[str, str]):
77+
"""Updates the headers with the given dict."""
78+
return self._storage.update(
79+
{key.lower(): [key, value] for key, value in headers.items()}
80+
)
81+
82+
def copy(self):
83+
"""Returns a copy of the headers."""
84+
return HTTPHeaders(dict(self._storage.values()))
85+
86+
def __getitem__(self, name: str):
87+
return self._storage[name.lower()][1]
88+
89+
def __setitem__(self, name: str, value: str):
90+
self._storage[name.lower()] = [name, value]
91+
92+
def __delitem__(self, name: str):
93+
del self._storage[name.lower()]
94+
95+
def __iter__(self):
96+
return iter(dict(self._storage.values()))
97+
98+
def __len__(self):
99+
return len(self._storage)
100+
101+
def __contains__(self, key: str):
102+
return key.lower() in self._storage.keys()
103+
104+
def __repr__(self):
105+
return f"{self.__class__.__name__}({dict(self._storage.values())})"

adafruit_httpserver/request.py

+43-22
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,36 @@
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

17+
from .headers import HTTPHeaders
18+
1519

1620
class HTTPRequest:
1721
"""
1822
Incoming request, constructed from raw incoming bytes.
1923
It is passed as first argument to route handlers.
2024
"""
2125

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

@@ -39,26 +58,26 @@ class HTTPRequest:
3958
http_version: str
4059
"""HTTP version, e.g. "HTTP/1.1"."""
4160

42-
headers: Dict[str, str]
61+
headers: HTTPHeaders
4362
"""
44-
Headers from the request as `dict`.
45-
46-
Values should be accessed using **lower case header names**.
47-
48-
Example::
49-
50-
request.headers
51-
# {'connection': 'keep-alive', 'content-length': '64' ...}
52-
request.headers["content-length"]
53-
# '64'
54-
request.headers["Content-Length"]
55-
# KeyError: 'Content-Length'
63+
Headers from the request.
5664
"""
5765

5866
raw_request: bytes
59-
"""Raw bytes passed to the constructor."""
67+
"""
68+
Raw 'bytes' passed to the constructor and body 'bytes' received later.
69+
70+
Should **not** be modified directly.
71+
"""
6072

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

6483
if raw_request is None:
@@ -120,12 +139,14 @@ def _parse_start_line(header_bytes: bytes) -> Tuple[str, str, Dict[str, str], st
120139
return method, path, query_params, http_version
121140

122141
@staticmethod
123-
def _parse_headers(header_bytes: bytes) -> Dict[str, str]:
142+
def _parse_headers(header_bytes: bytes) -> HTTPHeaders:
124143
"""Parse HTTP headers from raw request."""
125144
header_lines = header_bytes.decode("utf8").splitlines()[1:]
126145

127-
return {
128-
name.lower(): value
129-
for header_line in header_lines
130-
for name, value in [header_line.split(": ", 1)]
131-
}
146+
return HTTPHeaders(
147+
{
148+
name: value
149+
for header_line in header_lines
150+
for name, value in [header_line.split(": ", 1)]
151+
}
152+
)

0 commit comments

Comments
 (0)