]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Status code tweaks (#77)
authorTom Christie <tom@tomchristie.com>
Fri, 24 May 2019 09:27:35 +0000 (10:27 +0100)
committerGitHub <noreply@github.com>
Fri, 24 May 2019 09:27:35 +0000 (10:27 +0100)
* Add top-level API

* Add tests for top-level API

* Further work towards parallel support

* StatusCode tweaks

* Drop erronous commit

httpcore/__init__.py
httpcore/models.py
httpcore/status_codes.py
httpcore/utils.py
tests/dispatch/test_connections.py
tests/dispatch/utils.py
tests/test_parallel.py [new file with mode: 0644]

index 8d443963cd7b1384548951262cb694ecaa520f2f..6d073a8de332e4047407970fd00d9af2ec1b5847 100644 (file)
@@ -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"
index 69ad6b15729af0d415de2bd38d1039c91b1306dc..f2bc93569e61ee4ce506d906b615ca449720a0b1 100644 (file)
@@ -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 = ""
index d1b0dacf4192a29394d4aa8f846447c198563263..b839dcfe2d02c216255d3398de631bb9a9f87449 100644 (file)
@@ -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
index 0f5ec1b317e090f97a031a6680e9d0a5a89a8149..7d61aaf615881763647677a9209c6b1933569e48 100644 (file)
@@ -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.
index f323f55df9500ce03c2e84818ec52523326440be..ca401c7813316ef5c21574dbffbc11bb8d6e9a09 100644 (file)
@@ -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
 
 
index 8214c9042b908d0d3a1cd31784374ab477d84e5d..651fddf8c1d7426603a3c71f66fb5573c401c9b0 100644 (file)
@@ -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 (file)
index 0000000..e69de29