Coverage for src/httpx/_response.py: 97%

1 import typing

2

3 from ._content import Content

4 from ._streams import ByteStream, Stream

5 from ._headers import Headers

6

7 __all__ = ["Response"]

8

9# We're using the same set as stdlib `http.HTTPStatus` here...

10#

11# https://github.com/python/cpython/blob/main/Lib/http/__init__.py

12 _codes = {

13 100: "Continue",

14 101: "Switching Protocols",

15 102: "Processing",

16 103: "Early Hints",

17 200: "OK",

18 201: "Created",

19 202: "Accepted",

20 203: "Non-Authoritative Information",

21 204: "No Content",

22 205: "Reset Content",

23 206: "Partial Content",

24 207: "Multi-Status",

25 208: "Already Reported",

26 226: "IM Used",

27 300: "Multiple Choices",

28 301: "Moved Permanently",

29 302: "Found",

30 303: "See Other",

31 304: "Not Modified",

32 305: "Use Proxy",

33 307: "Temporary Redirect",

34 308: "Permanent Redirect",

35 400: "Bad Request",

36 401: "Unauthorized",

37 402: "Payment Required",

38 403: "Forbidden",

39 404: "Not Found",

40 405: "Method Not Allowed",

41 406: "Not Acceptable",

42 407: "Proxy Authentication Required",

43 408: "Request Timeout",

44 409: "Conflict",

45 410: "Gone",

46 411: "Length Required",

47 412: "Precondition Failed",

48 413: "Content Too Large",

49 414: "URI Too Long",

50 415: "Unsupported Media Type",

51 416: "Range Not Satisfiable",

52 417: "Expectation Failed",

53 418: "I'm a Teapot",

54 421: "Misdirected Request",

55 422: "Unprocessable Content",

56 423: "Locked",

57 424: "Failed Dependency",

58 425: "Too Early",

59 426: "Upgrade Required",

60 428: "Precondition Required",

61 429: "Too Many Requests",

62 431: "Request Header Fields Too Large",

63 451: "Unavailable For Legal Reasons",

64 500: "Internal Server Error",

65 501: "Not Implemented",

66 502: "Bad Gateway",

67 503: "Service Unavailable",

68 504: "Gateway Timeout",

69 505: "HTTP Version Not Supported",

70 506: "Variant Also Negotiates",

71 507: "Insufficient Storage",

72 508: "Loop Detected",

73 510: "Not Extended",

74 511: "Network Authentication Required",

75}

76

77

78 class Response:

79 def __init__(

80 self,

81 code: int,

82 *,

83 headers: Headers | typing.Mapping[str, str] | None = None,

84 content: Content | Stream | bytes | None = None,

85 ):

86 self.code = code

87 self.headers = Headers(headers)

88 self.stream : Stream = ByteStream(b"")

89

90 if content is not None:

91 if isinstance(content, bytes):

92 self.stream = ByteStream(content)

93 elif isinstance(content, Stream):

94 self.stream = content

95 elif isinstance(content, Content):

96 # Eg. Response(200, content=HTML(...))

97 stream, content_type = content.encode()

98 self.headers = self.headers.copy_set("Content-Type", content_type)

99 self.stream = stream

100 else:

101 raise TypeError(f'Expected `Content | Stream | bytes | None` got {type(content)}')

102

103 # https://datatracker.ietf.org/doc/html/rfc2616#section-4.3

104 # RFC 2616, Section 4.3, Message Body.

105 #

106 # All 1xx (informational), 204 (no content), and 304 (not modified) responses

107 # MUST NOT include a message-body. All other responses do include a

108 # message-body, although it MAY be of zero length.

109 if code >= 200 and code != 204 and code != 304:

110 content_length: int | None = self.stream.size

111 if content_length is None:

112 self.headers = self.headers.copy_set("Transfer-Encoding", "chunked")

113 else:

114 self.headers = self.headers.copy_set("Content-Length", str(content_length))

115

116 @property

117 def reason_phrase(self):

118 return _codes.get(self.code, "Unknown Status Code")

119

120 def read(self):

121 self.body = b"".join([part for part in self.stream])

122 self.stream = ByteStream(self.body)

123

124 def __repr__(self):

125 return f"<Response [{self.code} {self.reason_phrase}]>"