diff --git a/src/ahttpx/__init__.py b/src/ahttpx/__init__.py index 478a50b..cc779d6 100644 --- a/src/ahttpx/__init__.py +++ b/src/ahttpx/__init__.py @@ -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 @@ -47,6 +47,7 @@ "Request", "run", "serve_http", + "StatusCode", "Stream", "Text", "timeout", diff --git a/src/ahttpx/_client.py b/src/ahttpx/_client.py index 45c46b9..46c9bb9 100644 --- a/src/ahttpx/_client.py +++ b/src/ahttpx/_client.py @@ -35,7 +35,7 @@ def build_request( self, method: Method | str, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ) -> Request: return Request( @@ -49,7 +49,7 @@ async def request( self, method: Method | str, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ) -> Response: request = self.build_request(method, url, headers=headers, content=content) @@ -61,7 +61,7 @@ async def stream( self, method: Method | str, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ) -> Response: request = self.build_request(method, url, headers=headers, content=content) @@ -70,14 +70,14 @@ async def stream( async def get( self, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, ): return await self.request("GET", url, headers=headers) async def post( self, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ): return await self.request("POST", url, headers=headers, content=content) @@ -85,7 +85,7 @@ async def post( async def put( self, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ): return await self.request("PUT", url, headers=headers, content=content) @@ -93,7 +93,7 @@ async def put( async def patch( self, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ): return await self.request("PATCH", url, headers=headers, content=content) @@ -101,7 +101,7 @@ async def patch( async def delete( self, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, ): return await self.request("DELETE", url, headers=headers) diff --git a/src/ahttpx/_headers.py b/src/ahttpx/_headers.py index dade805..857754f 100644 --- a/src/ahttpx/_headers.py +++ b/src/ahttpx/_headers.py @@ -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(): diff --git a/src/ahttpx/_pool.py b/src/ahttpx/_pool.py index a322c84..c4838da 100644 --- a/src/ahttpx/_pool.py +++ b/src/ahttpx/_pool.py @@ -185,7 +185,7 @@ async def request( self, method: Method | str, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ) -> Response: url = self._origin.join(url) @@ -198,7 +198,7 @@ async def stream( self, method: Method | str, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ) -> Response: url = self._origin.join(url) @@ -211,10 +211,7 @@ async def _send_head(self, request: Request) -> None: target = request.url.target.encode('ascii') protocol = b'HTTP/1.1' await self._parser.send_method_line(method, target, protocol) - headers = [ - (k.encode('ascii'), v.encode('ascii')) - for k, v in request.headers.items() - ] + headers = request.headers.as_byte_pairs() await self._parser.send_headers(headers) async def _send_body(self, request: Request) -> None: diff --git a/src/ahttpx/_quickstart.py b/src/ahttpx/_quickstart.py index 8b6e12f..045b9f1 100644 --- a/src/ahttpx/_quickstart.py +++ b/src/ahttpx/_quickstart.py @@ -12,14 +12,14 @@ async def get( url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, ): async with Client() as client: return await client.request("GET", url=url, headers=headers) async def post( url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ): async with Client() as client: @@ -27,7 +27,7 @@ async def post( async def put( url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ): async with Client() as client: @@ -35,7 +35,7 @@ async def put( async def patch( url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ): async with Client() as client: @@ -43,7 +43,7 @@ async def patch( async def delete( url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, ): async with Client() as client: return await client.request("DELETE", url=url, headers=headers) diff --git a/src/ahttpx/_request.py b/src/ahttpx/_request.py index b5f1345..8f836b5 100644 --- a/src/ahttpx/_request.py +++ b/src/ahttpx/_request.py @@ -6,7 +6,7 @@ from ._headers import Headers from ._urls import URL -__all__ = ["Request"] +__all__ = ["Method", "Request"] class Method: @@ -35,7 +35,7 @@ def __init__( self, method: Method | str, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ): self.method = Method(method) if not isinstance(method, Method) else method diff --git a/src/ahttpx/_response.py b/src/ahttpx/_response.py index d695488..c2420a7 100644 --- a/src/ahttpx/_response.py +++ b/src/ahttpx/_response.py @@ -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"" class Response: def __init__( self, - status_code: int, + status_code: StatusCode | int, *, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[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"") @@ -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'): @@ -155,4 +204,4 @@ async def __aexit__(self, await self.close() def __repr__(self): - return f"" + return f"" diff --git a/src/ahttpx/_server.py b/src/ahttpx/_server.py index 973da54..577d001 100644 --- a/src/ahttpx/_server.py +++ b/src/ahttpx/_server.py @@ -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) @@ -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): diff --git a/src/httpx/__init__.py b/src/httpx/__init__.py index 478a50b..cc779d6 100644 --- a/src/httpx/__init__.py +++ b/src/httpx/__init__.py @@ -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 @@ -47,6 +47,7 @@ "Request", "run", "serve_http", + "StatusCode", "Stream", "Text", "timeout", diff --git a/src/httpx/_client.py b/src/httpx/_client.py index aaf8ad9..07091a7 100644 --- a/src/httpx/_client.py +++ b/src/httpx/_client.py @@ -35,7 +35,7 @@ def build_request( self, method: Method | str, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ) -> Request: return Request( @@ -49,7 +49,7 @@ def request( self, method: Method | str, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ) -> Response: request = self.build_request(method, url, headers=headers, content=content) @@ -61,7 +61,7 @@ def stream( self, method: Method | str, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ) -> Response: request = self.build_request(method, url, headers=headers, content=content) @@ -70,14 +70,14 @@ def stream( def get( self, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, ): return self.request("GET", url, headers=headers) def post( self, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ): return self.request("POST", url, headers=headers, content=content) @@ -85,7 +85,7 @@ def post( def put( self, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ): return self.request("PUT", url, headers=headers, content=content) @@ -93,7 +93,7 @@ def put( def patch( self, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ): return self.request("PATCH", url, headers=headers, content=content) @@ -101,7 +101,7 @@ def patch( def delete( self, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, ): return self.request("DELETE", url, headers=headers) diff --git a/src/httpx/_headers.py b/src/httpx/_headers.py index dade805..857754f 100644 --- a/src/httpx/_headers.py +++ b/src/httpx/_headers.py @@ -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(): diff --git a/src/httpx/_pool.py b/src/httpx/_pool.py index f537be8..8959159 100644 --- a/src/httpx/_pool.py +++ b/src/httpx/_pool.py @@ -185,7 +185,7 @@ def request( self, method: Method | str, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ) -> Response: url = self._origin.join(url) @@ -198,7 +198,7 @@ def stream( self, method: Method | str, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ) -> Response: url = self._origin.join(url) @@ -211,10 +211,7 @@ def _send_head(self, request: Request) -> None: target = request.url.target.encode('ascii') protocol = b'HTTP/1.1' self._parser.send_method_line(method, target, protocol) - headers = [ - (k.encode('ascii'), v.encode('ascii')) - for k, v in request.headers.items() - ] + headers = request.headers.as_byte_pairs() self._parser.send_headers(headers) def _send_body(self, request: Request) -> None: diff --git a/src/httpx/_quickstart.py b/src/httpx/_quickstart.py index 1a97530..fe81a8d 100644 --- a/src/httpx/_quickstart.py +++ b/src/httpx/_quickstart.py @@ -12,14 +12,14 @@ def get( url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, ): with Client() as client: return client.request("GET", url=url, headers=headers) def post( url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ): with Client() as client: @@ -27,7 +27,7 @@ def post( def put( url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ): with Client() as client: @@ -35,7 +35,7 @@ def put( def patch( url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ): with Client() as client: @@ -43,7 +43,7 @@ def patch( def delete( url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, ): with Client() as client: return client.request("DELETE", url=url, headers=headers) diff --git a/src/httpx/_request.py b/src/httpx/_request.py index 3cf030c..4e7d2b3 100644 --- a/src/httpx/_request.py +++ b/src/httpx/_request.py @@ -6,7 +6,7 @@ from ._headers import Headers from ._urls import URL -__all__ = ["Request"] +__all__ = ["Method", "Request"] class Method: @@ -35,7 +35,7 @@ def __init__( self, method: Method | str, url: URL | str, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[str, str] | None = None, content: Content | Stream | bytes | None = None, ): self.method = Method(method) if not isinstance(method, Method) else method diff --git a/src/httpx/_response.py b/src/httpx/_response.py index 0dc43ff..4f4fb13 100644 --- a/src/httpx/_response.py +++ b/src/httpx/_response.py @@ -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"" class Response: def __init__( self, - status_code: int, + status_code: StatusCode | int, *, - headers: Headers | typing.Mapping[str, str] | None = None, + headers: Headers | dict[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"") @@ -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'): @@ -155,4 +204,4 @@ def __exit__(self, self.close() def __repr__(self): - return f"" + return f"" diff --git a/src/httpx/_server.py b/src/httpx/_server.py index 31bec5d..4f1ca3a 100644 --- a/src/httpx/_server.py +++ b/src/httpx/_server.py @@ -37,7 +37,7 @@ def handle_requests(self): with Request(method, url, headers=headers, content=stream) as request: try: response = 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) @@ -72,13 +72,9 @@ def _recv_body(self): # Return the response... 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() 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() self._parser.send_headers(headers) def _send_body(self, response: Response):