Skip to content

Commit 7e8d156

Browse files
Add StatusCode (#142)
* Cleanup __init__ * Add StatusCode helper * StatusCode class * Add StatusCode
1 parent 8302eb5 commit 7e8d156

File tree

8 files changed

+270
-166
lines changed

8 files changed

+270
-166
lines changed

src/ahttpx/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ._parsers import * # HTTPParser, HTTPStream, ProtocolError
77
from ._pool import * # Connection, ConnectionPool, Transport
88
from ._quickstart import * # get, post, put, patch, delete
9-
from ._response import * # Response
9+
from ._response import * # StatusCode, Response
1010
from ._request import * # Method, Request
1111
from ._streams import * # ByteStream, DuplexStream, FileStream, Stream
1212
from ._server import * # serve_http, run
@@ -47,6 +47,7 @@
4747
"Request",
4848
"run",
4949
"serve_http",
50+
"StatusCode",
5051
"Stream",
5152
"Text",
5253
"timeout",

src/ahttpx/_headers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,12 @@ def copy_update(self, update: "Headers" | typing.Mapping[str, str] | None) -> "H
169169

170170
return Headers(h)
171171

172+
def as_byte_pairs(self) -> list[tuple[bytes, bytes]]:
173+
return [
174+
(k.encode('ascii'), v.encode('ascii'))
175+
for k, v in self.items()
176+
]
177+
172178
def __getitem__(self, key: str) -> str:
173179
match = key.lower()
174180
for k, v in self._dict.items():

src/ahttpx/_response.py

Lines changed: 124 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -7,84 +7,137 @@
77

88
__all__ = ["Response"]
99

10-
# We're using the same set as stdlib `http.HTTPStatus` here...
11-
#
12-
# https://github.com/python/cpython/blob/main/Lib/http/__init__.py
13-
_codes = {
14-
100: "Continue",
15-
101: "Switching Protocols",
16-
102: "Processing",
17-
103: "Early Hints",
18-
200: "OK",
19-
201: "Created",
20-
202: "Accepted",
21-
203: "Non-Authoritative Information",
22-
204: "No Content",
23-
205: "Reset Content",
24-
206: "Partial Content",
25-
207: "Multi-Status",
26-
208: "Already Reported",
27-
226: "IM Used",
28-
300: "Multiple Choices",
29-
301: "Moved Permanently",
30-
302: "Found",
31-
303: "See Other",
32-
304: "Not Modified",
33-
305: "Use Proxy",
34-
307: "Temporary Redirect",
35-
308: "Permanent Redirect",
36-
400: "Bad Request",
37-
401: "Unauthorized",
38-
402: "Payment Required",
39-
403: "Forbidden",
40-
404: "Not Found",
41-
405: "Method Not Allowed",
42-
406: "Not Acceptable",
43-
407: "Proxy Authentication Required",
44-
408: "Request Timeout",
45-
409: "Conflict",
46-
410: "Gone",
47-
411: "Length Required",
48-
412: "Precondition Failed",
49-
413: "Content Too Large",
50-
414: "URI Too Long",
51-
415: "Unsupported Media Type",
52-
416: "Range Not Satisfiable",
53-
417: "Expectation Failed",
54-
418: "I'm a Teapot",
55-
421: "Misdirected Request",
56-
422: "Unprocessable Content",
57-
423: "Locked",
58-
424: "Failed Dependency",
59-
425: "Too Early",
60-
426: "Upgrade Required",
61-
428: "Precondition Required",
62-
429: "Too Many Requests",
63-
431: "Request Header Fields Too Large",
64-
451: "Unavailable For Legal Reasons",
65-
500: "Internal Server Error",
66-
501: "Not Implemented",
67-
502: "Bad Gateway",
68-
503: "Service Unavailable",
69-
504: "Gateway Timeout",
70-
505: "HTTP Version Not Supported",
71-
506: "Variant Also Negotiates",
72-
507: "Insufficient Storage",
73-
508: "Loop Detected",
74-
510: "Not Extended",
75-
511: "Network Authentication Required",
76-
}
10+
11+
class StatusCode:
12+
# We're using the same set as stdlib `http.HTTPStatus` here...
13+
#
14+
# https://github.com/python/cpython/blob/main/Lib/http/__init__.py
15+
_codes = {
16+
100: "Continue",
17+
101: "Switching Protocols",
18+
102: "Processing",
19+
103: "Early Hints",
20+
200: "OK",
21+
201: "Created",
22+
202: "Accepted",
23+
203: "Non-Authoritative Information",
24+
204: "No Content",
25+
205: "Reset Content",
26+
206: "Partial Content",
27+
207: "Multi-Status",
28+
208: "Already Reported",
29+
226: "IM Used",
30+
300: "Multiple Choices",
31+
301: "Moved Permanently",
32+
302: "Found",
33+
303: "See Other",
34+
304: "Not Modified",
35+
305: "Use Proxy",
36+
307: "Temporary Redirect",
37+
308: "Permanent Redirect",
38+
400: "Bad Request",
39+
401: "Unauthorized",
40+
402: "Payment Required",
41+
403: "Forbidden",
42+
404: "Not Found",
43+
405: "Method Not Allowed",
44+
406: "Not Acceptable",
45+
407: "Proxy Authentication Required",
46+
408: "Request Timeout",
47+
409: "Conflict",
48+
410: "Gone",
49+
411: "Length Required",
50+
412: "Precondition Failed",
51+
413: "Content Too Large",
52+
414: "URI Too Long",
53+
415: "Unsupported Media Type",
54+
416: "Range Not Satisfiable",
55+
417: "Expectation Failed",
56+
418: "I'm a Teapot",
57+
421: "Misdirected Request",
58+
422: "Unprocessable Content",
59+
423: "Locked",
60+
424: "Failed Dependency",
61+
425: "Too Early",
62+
426: "Upgrade Required",
63+
428: "Precondition Required",
64+
429: "Too Many Requests",
65+
431: "Request Header Fields Too Large",
66+
451: "Unavailable For Legal Reasons",
67+
500: "Internal Server Error",
68+
501: "Not Implemented",
69+
502: "Bad Gateway",
70+
503: "Service Unavailable",
71+
504: "Gateway Timeout",
72+
505: "HTTP Version Not Supported",
73+
506: "Variant Also Negotiates",
74+
507: "Insufficient Storage",
75+
508: "Loop Detected",
76+
510: "Not Extended",
77+
511: "Network Authentication Required",
78+
}
79+
80+
def __init__(self, status_code: int):
81+
if status_code < 100 or status_code > 999:
82+
raise ValueError("Invalid status code {status_code!r}")
83+
self.value = status_code
84+
self.reason_phrase = self._codes.get(status_code, "Unknown Status Code")
85+
86+
def is_1xx_informational(self) -> bool:
87+
"""
88+
Returns `True` for 1xx status codes, `False` otherwise.
89+
"""
90+
return 100 <= int(self) <= 199
91+
92+
def is_2xx_success(self) -> bool:
93+
"""
94+
Returns `True` for 2xx status codes, `False` otherwise.
95+
"""
96+
return 200 <= int(self) <= 299
97+
98+
def is_3xx_redirect(self) -> bool:
99+
"""
100+
Returns `True` for 3xx status codes, `False` otherwise.
101+
"""
102+
return 300 <= int(self) <= 399
103+
104+
def is_4xx_client_error(self) -> bool:
105+
"""
106+
Returns `True` for 4xx status codes, `False` otherwise.
107+
"""
108+
return 400 <= int(self) <= 499
109+
110+
def is_5xx_server_error(self) -> bool:
111+
"""
112+
Returns `True` for 5xx status codes, `False` otherwise.
113+
"""
114+
return 500 <= int(self) <= 599
115+
116+
def as_tuple(self) -> tuple[int, bytes]:
117+
return (self.value, self.reason_phrase.encode('ascii'))
118+
119+
def __eq__(self, other) -> bool:
120+
return int(self) == int(other)
121+
122+
def __int__(self) -> int:
123+
return self.value
124+
125+
def __str__(self) -> str:
126+
return f"{self.value} {self.reason_phrase}"
127+
128+
def __repr__(self) -> str:
129+
return f"<StatusCode [{self.value} {self.reason_phrase}]>"
77130

78131

79132
class Response:
80133
def __init__(
81134
self,
82-
status_code: int,
135+
status_code: StatusCode | int,
83136
*,
84137
headers: Headers | typing.Mapping[str, str] | None = None,
85138
content: Content | Stream | bytes | None = None,
86139
):
87-
self.status_code = status_code
140+
self.status_code = StatusCode(status_code) if not isinstance(status_code, StatusCode) else status_code
88141
self.headers = Headers(headers) if not isinstance(headers, Headers) else headers
89142
self.stream: Stream = ByteStream(b"")
90143

@@ -106,17 +159,13 @@ def __init__(
106159
# All 1xx (informational), 204 (no content), and 304 (not modified) responses
107160
# MUST NOT include a message-body. All other responses do include a
108161
# message-body, although it MAY be of zero length.
109-
if status_code >= 200 and status_code != 204 and status_code != 304:
162+
if not(self.status_code.is_1xx_informational() or self.status_code == 204 or self.status_code == 304):
110163
content_length: int | None = self.stream.size
111164
if content_length is None:
112165
self.headers = self.headers.copy_set("Transfer-Encoding", "chunked")
113166
else:
114167
self.headers = self.headers.copy_set("Content-Length", str(content_length))
115168

116-
@property
117-
def reason_phrase(self):
118-
return _codes.get(self.status_code, "Unknown Status Code")
119-
120169
@property
121170
def body(self) -> bytes:
122171
if not hasattr(self, '_body'):
@@ -155,4 +204,4 @@ async def __aexit__(self,
155204
await self.close()
156205

157206
def __repr__(self):
158-
return f"<Response [{self.status_code} {self.reason_phrase}]>"
207+
return f"<Response [{int(self.status_code)} {self.status_code.reason_phrase}]>"

src/ahttpx/_server.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ async def handle_requests(self):
3737
async with Request(method, url, headers=headers, content=stream) as request:
3838
try:
3939
response = await self._endpoint(request)
40-
status_line = f"{request.method} {request.url.target} [{response.status_code} {response.reason_phrase}]"
40+
status_line = f"{request.method} {request.url.target} [{response.status_code} {response.status_code.reason_phrase}]"
4141
logger.info(status_line)
4242
except Exception:
4343
logger.error("Internal Server Error", exc_info=True)
@@ -72,13 +72,9 @@ async def _recv_body(self):
7272
# Return the response...
7373
async def _send_head(self, response: Response):
7474
protocol = b"HTTP/1.1"
75-
status = response.status_code
76-
reason = response.reason_phrase.encode('ascii')
75+
status, reason = response.status_code.as_tuple()
7776
await self._parser.send_status_line(protocol, status, reason)
78-
headers = [
79-
(k.encode('ascii'), v.encode('ascii'))
80-
for k, v in response.headers.items()
81-
]
77+
headers = response.headers.as_byte_pairs()
8278
await self._parser.send_headers(headers)
8379

8480
async def _send_body(self, response: Response):

src/httpx/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ._parsers import * # HTTPParser, HTTPStream, ProtocolError
77
from ._pool import * # Connection, ConnectionPool, Transport
88
from ._quickstart import * # get, post, put, patch, delete
9-
from ._response import * # Response
9+
from ._response import * # StatusCode, Response
1010
from ._request import * # Method, Request
1111
from ._streams import * # ByteStream, DuplexStream, FileStream, Stream
1212
from ._server import * # serve_http, run
@@ -47,6 +47,7 @@
4747
"Request",
4848
"run",
4949
"serve_http",
50+
"StatusCode",
5051
"Stream",
5152
"Text",
5253
"timeout",

src/httpx/_headers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,12 @@ def copy_update(self, update: "Headers" | typing.Mapping[str, str] | None) -> "H
169169

170170
return Headers(h)
171171

172+
def as_byte_pairs(self) -> list[tuple[bytes, bytes]]:
173+
return [
174+
(k.encode('ascii'), v.encode('ascii'))
175+
for k, v in self.items()
176+
]
177+
172178
def __getitem__(self, key: str) -> str:
173179
match = key.lower()
174180
for k, v in self._dict.items():

0 commit comments

Comments
 (0)