Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/ahttpx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ._parsers import * # HTTPParser, HTTPStream, ProtocolError
from ._pool import * # Connection, ConnectionPool, Transport
from ._quickstart import * # get, post, put, patch, delete
from ._response import * # Response
from ._response import * # StatusCode, Response
from ._request import * # Method, Request
from ._streams import * # ByteStream, DuplexStream, FileStream, Stream
from ._server import * # serve_http, run
Expand Down Expand Up @@ -47,6 +47,7 @@
"Request",
"run",
"serve_http",
"StatusCode",
"Stream",
"Text",
"timeout",
Expand Down
6 changes: 6 additions & 0 deletions src/ahttpx/_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ def copy_update(self, update: "Headers" | typing.Mapping[str, str] | None) -> "H

return Headers(h)

def as_byte_pairs(self) -> list[tuple[bytes, bytes]]:
return [
(k.encode('ascii'), v.encode('ascii'))
for k, v in self.items()
]

def __getitem__(self, key: str) -> str:
match = key.lower()
for k, v in self._dict.items():
Expand Down
199 changes: 124 additions & 75 deletions src/ahttpx/_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,84 +7,137 @@

__all__ = ["Response"]

# We're using the same set as stdlib `http.HTTPStatus` here...
#
# https://github.com/python/cpython/blob/main/Lib/http/__init__.py
_codes = {
100: "Continue",
101: "Switching Protocols",
102: "Processing",
103: "Early Hints",
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
207: "Multi-Status",
208: "Already Reported",
226: "IM Used",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Found",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
307: "Temporary Redirect",
308: "Permanent Redirect",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Content Too Large",
414: "URI Too Long",
415: "Unsupported Media Type",
416: "Range Not Satisfiable",
417: "Expectation Failed",
418: "I'm a Teapot",
421: "Misdirected Request",
422: "Unprocessable Content",
423: "Locked",
424: "Failed Dependency",
425: "Too Early",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
451: "Unavailable For Legal Reasons",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
510: "Not Extended",
511: "Network Authentication Required",
}

class StatusCode:
# We're using the same set as stdlib `http.HTTPStatus` here...
#
# https://github.com/python/cpython/blob/main/Lib/http/__init__.py
_codes = {
100: "Continue",
101: "Switching Protocols",
102: "Processing",
103: "Early Hints",
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
207: "Multi-Status",
208: "Already Reported",
226: "IM Used",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Found",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
307: "Temporary Redirect",
308: "Permanent Redirect",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Content Too Large",
414: "URI Too Long",
415: "Unsupported Media Type",
416: "Range Not Satisfiable",
417: "Expectation Failed",
418: "I'm a Teapot",
421: "Misdirected Request",
422: "Unprocessable Content",
423: "Locked",
424: "Failed Dependency",
425: "Too Early",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
451: "Unavailable For Legal Reasons",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
510: "Not Extended",
511: "Network Authentication Required",
}

def __init__(self, status_code: int):
if status_code < 100 or status_code > 999:
raise ValueError("Invalid status code {status_code!r}")
self.value = status_code
self.reason_phrase = self._codes.get(status_code, "Unknown Status Code")

def is_1xx_informational(self) -> bool:
"""
Returns `True` for 1xx status codes, `False` otherwise.
"""
return 100 <= int(self) <= 199

def is_2xx_success(self) -> bool:
"""
Returns `True` for 2xx status codes, `False` otherwise.
"""
return 200 <= int(self) <= 299

def is_3xx_redirect(self) -> bool:
"""
Returns `True` for 3xx status codes, `False` otherwise.
"""
return 300 <= int(self) <= 399

def is_4xx_client_error(self) -> bool:
"""
Returns `True` for 4xx status codes, `False` otherwise.
"""
return 400 <= int(self) <= 499

def is_5xx_server_error(self) -> bool:
"""
Returns `True` for 5xx status codes, `False` otherwise.
"""
return 500 <= int(self) <= 599

def as_tuple(self) -> tuple[int, bytes]:
return (self.value, self.reason_phrase.encode('ascii'))

def __eq__(self, other) -> bool:
return int(self) == int(other)

def __int__(self) -> int:
return self.value

def __str__(self) -> str:
return f"{self.value} {self.reason_phrase}"

def __repr__(self) -> str:
return f"<StatusCode [{self.value} {self.reason_phrase}]>"


class Response:
def __init__(
self,
status_code: int,
status_code: StatusCode | int,
*,
headers: Headers | typing.Mapping[str, str] | None = None,
content: Content | Stream | bytes | None = None,
):
self.status_code = status_code
self.status_code = StatusCode(status_code) if not isinstance(status_code, StatusCode) else status_code
self.headers = Headers(headers) if not isinstance(headers, Headers) else headers
self.stream: Stream = ByteStream(b"")

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

@property
def reason_phrase(self):
return _codes.get(self.status_code, "Unknown Status Code")

@property
def body(self) -> bytes:
if not hasattr(self, '_body'):
Expand Down Expand Up @@ -155,4 +204,4 @@ async def __aexit__(self,
await self.close()

def __repr__(self):
return f"<Response [{self.status_code} {self.reason_phrase}]>"
return f"<Response [{int(self.status_code)} {self.status_code.reason_phrase}]>"
10 changes: 3 additions & 7 deletions src/ahttpx/_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ async def handle_requests(self):
async with Request(method, url, headers=headers, content=stream) as request:
try:
response = await self._endpoint(request)
status_line = f"{request.method} {request.url.target} [{response.status_code} {response.reason_phrase}]"
status_line = f"{request.method} {request.url.target} [{response.status_code} {response.status_code.reason_phrase}]"
logger.info(status_line)
except Exception:
logger.error("Internal Server Error", exc_info=True)
Expand Down Expand Up @@ -72,13 +72,9 @@ async def _recv_body(self):
# Return the response...
async def _send_head(self, response: Response):
protocol = b"HTTP/1.1"
status = response.status_code
reason = response.reason_phrase.encode('ascii')
status, reason = response.status_code.as_tuple()
await self._parser.send_status_line(protocol, status, reason)
headers = [
(k.encode('ascii'), v.encode('ascii'))
for k, v in response.headers.items()
]
headers = response.headers.as_byte_pairs()
await self._parser.send_headers(headers)

async def _send_body(self, response: Response):
Expand Down
3 changes: 2 additions & 1 deletion src/httpx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ._parsers import * # HTTPParser, HTTPStream, ProtocolError
from ._pool import * # Connection, ConnectionPool, Transport
from ._quickstart import * # get, post, put, patch, delete
from ._response import * # Response
from ._response import * # StatusCode, Response
from ._request import * # Method, Request
from ._streams import * # ByteStream, DuplexStream, FileStream, Stream
from ._server import * # serve_http, run
Expand Down Expand Up @@ -47,6 +47,7 @@
"Request",
"run",
"serve_http",
"StatusCode",
"Stream",
"Text",
"timeout",
Expand Down
6 changes: 6 additions & 0 deletions src/httpx/_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ def copy_update(self, update: "Headers" | typing.Mapping[str, str] | None) -> "H

return Headers(h)

def as_byte_pairs(self) -> list[tuple[bytes, bytes]]:
return [
(k.encode('ascii'), v.encode('ascii'))
for k, v in self.items()
]

def __getitem__(self, key: str) -> str:
match = key.lower()
for k, v in self._dict.items():
Expand Down
Loading