Coverage for src/httpx/_models.py: 97%
64 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-05-23 12:21 +0100
« prev ^ index » next coverage.py v7.6.12, created at 2025-05-23 12:21 +0100
1from ._content import Content
2from ._streams import ByteStream, ClosedStream, Stream
3from ._headers import Headers
4from ._urls import URL
6__all__ = [
7 "Response",
8 "Request",
9]
11# We're using the same set as stdlib `http.HTTPStatus` here...
12#
13# https://github.com/python/cpython/blob/main/Lib/http/__init__.py
14_codes = {
15 100: "Continue",
16 101: "Switching Protocols",
17 102: "Processing",
18 103: "Early Hints",
19 200: "OK",
20 201: "Created",
21 202: "Accepted",
22 203: "Non-Authoritative Information",
23 204: "No Content",
24 205: "Reset Content",
25 206: "Partial Content",
26 207: "Multi-Status",
27 208: "Already Reported",
28 226: "IM Used",
29 300: "Multiple Choices",
30 301: "Moved Permanently",
31 302: "Found",
32 303: "See Other",
33 304: "Not Modified",
34 305: "Use Proxy",
35 307: "Temporary Redirect",
36 308: "Permanent Redirect",
37 400: "Bad Request",
38 401: "Unauthorized",
39 402: "Payment Required",
40 403: "Forbidden",
41 404: "Not Found",
42 405: "Method Not Allowed",
43 406: "Not Acceptable",
44 407: "Proxy Authentication Required",
45 408: "Request Timeout",
46 409: "Conflict",
47 410: "Gone",
48 411: "Length Required",
49 412: "Precondition Failed",
50 413: "Content Too Large",
51 414: "URI Too Long",
52 415: "Unsupported Media Type",
53 416: "Range Not Satisfiable",
54 417: "Expectation Failed",
55 418: "I'm a Teapot",
56 421: "Misdirected Request",
57 422: "Unprocessable Content",
58 423: "Locked",
59 424: "Failed Dependency",
60 425: "Too Early",
61 426: "Upgrade Required",
62 428: "Precondition Required",
63 429: "Too Many Requests",
64 431: "Request Header Fields Too Large",
65 451: "Unavailable For Legal Reasons",
66 500: "Internal Server Error",
67 501: "Not Implemented",
68 502: "Bad Gateway",
69 503: "Service Unavailable",
70 504: "Gateway Timeout",
71 505: "HTTP Version Not Supported",
72 506: "Variant Also Negotiates",
73 507: "Insufficient Storage",
74 508: "Loop Detected",
75 510: "Not Extended",
76 511: "Network Authentication Required",
77}
80class Request:
81 def __init__(
82 self,
83 method: str,
84 url: URL | str,
85 headers: Headers | dict[str, str] | None = None,
86 content: Content | Stream | bytes | None = None,
87 ):
88 self.method = method
89 self.url = URL(url)
90 self.headers = Headers(headers)
91 self.stream: Stream = ByteStream(b"")
93 # https://datatracker.ietf.org/doc/html/rfc2616#section-14.23
94 # RFC 2616, Section 14.23, Host.
95 #
96 # A client MUST include a Host header field in all HTTP/1.1 request messages.
97 self.headers = self.headers.copy_setdefault("Host", self.url.netloc)
99 if content is not None:
100 if isinstance(content, bytes):
101 self.stream = ByteStream(content)
102 elif isinstance(content, Stream):
103 self.stream = content
104 elif isinstance(content, Content):
105 assert isinstance(content, Content)
106 # Eg. Request("POST", "https://www.example.com", content=Form(...))
107 stream, content_type = content.encode()
108 self.headers = self.headers.copy_set("Content-Type", content_type)
109 self.stream = stream
111 # https://datatracker.ietf.org/doc/html/rfc2616#section-4.3
112 # RFC 2616, Section 4.3, Message Body.
113 #
114 # The presence of a message-body in a request is signaled by the
115 # inclusion of a Content-Length or Transfer-Encoding header field in
116 # the request's message-headers.
117 content_length: int | None = self.stream.size
118 if content_length is None:
119 self.headers = self.headers.copy_set("Transfer-Encoding", "chunked")
120 elif content_length > 0:
121 self.headers = self.headers.copy_set("Content-Length", str(content_length))
123 def read(self):
124 self.body = b"".join([part for part in self.stream])
125 self.stream = ByteStream(self.body)
127 def close(self):
128 self.stream = ClosedStream()
130 def __repr__(self):
131 return f"<Request [{self.method} {str(self.url)!r}]>"
134class Response:
135 def __init__(
136 self,
137 code: int,
138 *,
139 headers: Headers | dict[str, str] | None = None,
140 content: Content | Stream | bytes | None = None,
141 ):
142 self.code = code
143 self.headers = Headers(headers)
144 self.stream : Stream = ByteStream(b"")
146 if content is not None:
147 if isinstance(content, bytes):
148 self.stream = ByteStream(content)
149 elif isinstance(content, Stream):
150 self.stream = content
151 elif isinstance(content, Content):
152 # Eg. Response(200, content=HTML(...))
153 stream, content_type = content.encode()
154 self.headers = self.headers.copy_set("Content-Type", content_type)
155 self.stream = stream
157 # https://datatracker.ietf.org/doc/html/rfc2616#section-4.3
158 # RFC 2616, Section 4.3, Message Body.
159 #
160 # All 1xx (informational), 204 (no content), and 304 (not modified) responses
161 # MUST NOT include a message-body. All other responses do include a
162 # message-body, although it MAY be of zero length.
163 if code >= 200 and code != 204 and code != 304:
164 content_length: int | None = self.stream.size
165 if content_length is None:
166 self.headers = self.headers.copy_set("Transfer-Encoding", "chunked")
167 else:
168 self.headers = self.headers.copy_set("Content-Length", str(content_length))
170 @property
171 def reason_phrase(self):
172 return _codes.get(self.code, "Unknown Status Code")
174 def read(self):
175 self.body = b"".join([part for part in self.stream])
176 self.stream = ByteStream(self.body)
178 def close(self):
179 self.stream = ClosedStream()
181 def __repr__(self):
182 return f"<Response [{self.code} {self.reason_phrase}]>"