From: Tom Christie Date: Fri, 24 May 2019 09:27:35 +0000 (+0100) Subject: Status code tweaks (#77) X-Git-Tag: 0.3.1~8 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c12c27127605574c68f480060b4f58b69fc37e41;p=thirdparty%2Fhttpx.git Status code tweaks (#77) * Add top-level API * Add tests for top-level API * Further work towards parallel support * StatusCode tweaks * Drop erronous commit --- diff --git a/httpcore/__init__.py b/httpcore/__init__.py index 8d443963..6d073a8d 100644 --- a/httpcore/__init__.py +++ b/httpcore/__init__.py @@ -30,6 +30,6 @@ from .exceptions import ( ) from .interfaces import BaseReader, BaseWriter, ConcurrencyBackend, Dispatcher, Protocol from .models import URL, Cookies, Headers, Origin, QueryParams, Request, Response -from .status_codes import codes +from .status_codes import StatusCode, codes __version__ = "0.3.0" diff --git a/httpcore/models.py b/httpcore/models.py index 69ad6b15..f2bc9356 100644 --- a/httpcore/models.py +++ b/httpcore/models.py @@ -26,13 +26,8 @@ from .exceptions import ( ResponseNotRead, StreamConsumed, ) -from .status_codes import codes -from .utils import ( - get_reason_phrase, - is_known_encoding, - normalize_header_key, - normalize_header_value, -) +from .status_codes import StatusCode +from .utils import is_known_encoding, normalize_header_key, normalize_header_value URLTypes = typing.Union["URL", str] @@ -578,12 +573,8 @@ class Response: request: Request = None, history: typing.List["Response"] = None, ): - try: - # Use a StatusCode IntEnum if possible, for a nicer representation. - self.status_code = codes(status_code) # type: int - except ValueError: - self.status_code = status_code - self.reason_phrase = reason_phrase or get_reason_phrase(status_code) + self.status_code = StatusCode.enum_or_int(status_code) + self.reason_phrase = StatusCode.get_reason_phrase(status_code) self.protocol = protocol self.headers = Headers(headers) @@ -748,17 +739,7 @@ class Response: @property def is_redirect(self) -> bool: - return ( - self.status_code - in ( - codes.MOVED_PERMANENTLY, - codes.FOUND, - codes.SEE_OTHER, - codes.TEMPORARY_REDIRECT, - codes.PERMANENT_REDIRECT, - ) - and "location" in self.headers - ) + return StatusCode.is_redirect(self.status_code) and "location" in self.headers def raise_for_status(self) -> None: """ @@ -769,9 +750,9 @@ class Response: "For more information check: https://httpstatuses.com/{0.status_code}" ) - if 400 <= self.status_code < 500: + if StatusCode.is_client_error(self.status_code): message = message.format(self, error_type="Client Error") - elif 500 <= self.status_code < 600: + elif StatusCode.is_server_error(self.status_code): message = message.format(self, error_type="Server Error") else: message = "" diff --git a/httpcore/status_codes.py b/httpcore/status_codes.py index d1b0dacf..b839dcfe 100644 --- a/httpcore/status_codes.py +++ b/httpcore/status_codes.py @@ -1,3 +1,129 @@ -from http import HTTPStatus +from enum import IntEnum -codes = HTTPStatus + +class StatusCode(IntEnum): + """HTTP status codes and reason phrases + Status codes from the following RFCs are all observed: + * RFC 7231: Hypertext Transfer Protocol (HTTP/1.1), obsoletes 2616 + * RFC 6585: Additional HTTP Status Codes + * RFC 3229: Delta encoding in HTTP + * RFC 4918: HTTP Extensions for WebDAV, obsoletes 2518 + * RFC 5842: Binding Extensions to WebDAV + * RFC 7238: Permanent Redirect + * RFC 2295: Transparent Content Negotiation in HTTP + * RFC 2774: An HTTP Extension Framework + * RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2) + """ + + def __new__(cls, value: int, phrase: str = "") -> "StatusCode": + obj = int.__new__(cls, value) # type: ignore + obj._value_ = value + + obj.phrase = phrase + return obj + + def __str__(self) -> str: + return str(self.value) + + @classmethod + def enum_or_int(cls, value: int) -> int: + try: + return StatusCode(value) + except ValueError: + return value + + @classmethod + def get_reason_phrase(cls, value: int) -> str: + try: + return StatusCode(value).phrase # type: ignore + except ValueError: + return "" + + @classmethod + def is_redirect(cls, value: int) -> bool: + return value in ( + StatusCode.MOVED_PERMANENTLY, # 301 (Cacheable redirect. Method may change to GET.) + StatusCode.FOUND, # 302 (Uncacheable redirect. Method may change to GET.) + StatusCode.SEE_OTHER, # 303 (Client should make a GET or HEAD request.) + StatusCode.TEMPORARY_REDIRECT, # 307 (Equiv. 302, but retain method) + StatusCode.PERMANENT_REDIRECT, # 308 (Equiv. 301, but retain method) + ) + + @classmethod + def is_client_error(cls, value: int) -> bool: + return value >= 400 and value <= 499 + + @classmethod + def is_server_error(cls, value: int) -> bool: + return value >= 500 and value <= 599 + + # informational + CONTINUE = 100, "Continue" + SWITCHING_PROTOCOLS = 101, "Switching Protocols" + PROCESSING = 102, "Processing" + + # success + OK = 200, "OK" + CREATED = 201, "Created" + ACCEPTED = 202, "Accepted" + NON_AUTHORITATIVE_INFORMATION = 203, "Non-Authoritative Information" + NO_CONTENT = 204, "No Content" + RESET_CONTENT = 205, "Reset Content" + PARTIAL_CONTENT = 206, "Partial Content" + MULTI_STATUS = 207, "Multi-Status" + ALREADY_REPORTED = 208, "Already Reported" + IM_USED = 226, "IM Used" + + # redirection + MULTIPLE_CHOICES = 300, "Multiple Choices" + MOVED_PERMANENTLY = 301, "Moved Permanently" + FOUND = 302, "Found" + SEE_OTHER = 303, "See Other" + NOT_MODIFIED = 304, "Not Modified" + USE_PROXY = 305, "Use Proxy" + TEMPORARY_REDIRECT = 307, "Temporary Redirect" + PERMANENT_REDIRECT = 308, "Permanent Redirect" + + # client error + BAD_REQUEST = 400, "Bad Request" + UNAUTHORIZED = 401, "Unauthorized" + PAYMENT_REQUIRED = 402, "Payment Required" + FORBIDDEN = 403, "Forbidden" + NOT_FOUND = 404, "Not Found" + METHOD_NOT_ALLOWED = 405, "Method Not Allowed" + NOT_ACCEPTABLE = 406, "Not Acceptable" + PROXY_AUTHENTICATION_REQUIRED = 407, "Proxy Authentication Required" + REQUEST_TIMEOUT = 408, "Request Timeout" + CONFLICT = 409, "Conflict" + GONE = 410, "Gone" + LENGTH_REQUIRED = 411, "Length Required" + PRECONDITION_FAILED = 412, "Precondition Failed" + REQUEST_ENTITY_TOO_LARGE = 413, "Request Entity Too Large" + REQUEST_URI_TOO_LONG = 414, "Request-URI Too Long" + UNSUPPORTED_MEDIA_TYPE = 415, "Unsupported Media Type" + REQUESTED_RANGE_NOT_SATISFIABLE = 416, "Requested Range Not Satisfiable" + EXPECTATION_FAILED = 417, "Expectation Failed" + MISDIRECTED_REQUEST = 421, "Misdirected Request" + UNPROCESSABLE_ENTITY = 422, "Unprocessable Entity" + LOCKED = 423, "Locked" + FAILED_DEPENDENCY = 424, "Failed Dependency" + UPGRADE_REQUIRED = 426, "Upgrade Required" + PRECONDITION_REQUIRED = 428, "Precondition Required" + TOO_MANY_REQUESTS = 429, "Too Many Requests" + REQUEST_HEADER_FIELDS_TOO_LARGE = 431, "Request Header Fields Too Large" + + # server errors + INTERNAL_SERVER_ERROR = 500, "Internal Server Error" + NOT_IMPLEMENTED = 501, "Not Implemented" + BAD_GATEWAY = 502, "Bad Gateway" + SERVICE_UNAVAILABLE = 503, "Service Unavailable" + GATEWAY_TIMEOUT = 504, "Gateway Timeout" + HTTP_VERSION_NOT_SUPPORTED = 505, "HTTP Version Not Supported" + VARIANT_ALSO_NEGOTIATES = 506, "Variant Also Negotiates" + INSUFFICIENT_STORAGE = 507, "Insufficient Storage" + LOOP_DETECTED = 508, "Loop Detected" + NOT_EXTENDED = 510, "Not Extended" + NETWORK_AUTHENTICATION_REQUIRED = 511, "Network Authentication Required" + + +codes = StatusCode diff --git a/httpcore/utils.py b/httpcore/utils.py index 0f5ec1b3..7d61aaf6 100644 --- a/httpcore/utils.py +++ b/httpcore/utils.py @@ -1,5 +1,4 @@ import codecs -import http import typing @@ -21,16 +20,6 @@ def normalize_header_value(value: typing.AnyStr, encoding: str = None) -> bytes: return value.encode(encoding or "ascii") -def get_reason_phrase(status_code: int) -> str: - """ - Return an HTTP reason phrase such as "OK" for 200, or "Not Found" for 404. - """ - try: - return http.HTTPStatus(status_code).phrase - except ValueError as exc: - return "" - - def is_known_encoding(encoding: str) -> bool: """ Return `True` if `encoding` is a known codec. diff --git a/tests/dispatch/test_connections.py b/tests/dispatch/test_connections.py index f323f55d..ca401c78 100644 --- a/tests/dispatch/test_connections.py +++ b/tests/dispatch/test_connections.py @@ -14,7 +14,9 @@ async def test_get(server): @pytest.mark.asyncio async def test_post(server): conn = HTTPConnection(origin="http://127.0.0.1:8000/") - response = await conn.request("GET", "http://127.0.0.1:8000/", data=b"Hello, world!") + response = await conn.request( + "GET", "http://127.0.0.1:8000/", data=b"Hello, world!" + ) assert response.status_code == 200 diff --git a/tests/dispatch/utils.py b/tests/dispatch/utils.py index 8214c904..651fddf8 100644 --- a/tests/dispatch/utils.py +++ b/tests/dispatch/utils.py @@ -107,7 +107,7 @@ class MockHTTP2Server(BaseReader, BaseWriter): response = self.app(request) # Write the response to the buffer. - status_code_bytes = str(int(response.status_code)).encode("ascii") + status_code_bytes = str(response.status_code).encode("ascii") response_headers = [(b":status", status_code_bytes)] + response.headers.raw self.conn.send_headers(stream_id, response_headers) diff --git a/tests/test_parallel.py b/tests/test_parallel.py new file mode 100644 index 00000000..e69de29b