77
88__all__ = ["Response" ]
99
10- # We're using the same set as stdlib `http.HTTPStatus` here...
11- #
12- # https://github.com/python/cpython/blob/main/Lib/http/__init__.py
13- _codes = {
14- 100 : "Continue" ,
15- 101 : "Switching Protocols" ,
16- 102 : "Processing" ,
17- 103 : "Early Hints" ,
18- 200 : "OK" ,
19- 201 : "Created" ,
20- 202 : "Accepted" ,
21- 203 : "Non-Authoritative Information" ,
22- 204 : "No Content" ,
23- 205 : "Reset Content" ,
24- 206 : "Partial Content" ,
25- 207 : "Multi-Status" ,
26- 208 : "Already Reported" ,
27- 226 : "IM Used" ,
28- 300 : "Multiple Choices" ,
29- 301 : "Moved Permanently" ,
30- 302 : "Found" ,
31- 303 : "See Other" ,
32- 304 : "Not Modified" ,
33- 305 : "Use Proxy" ,
34- 307 : "Temporary Redirect" ,
35- 308 : "Permanent Redirect" ,
36- 400 : "Bad Request" ,
37- 401 : "Unauthorized" ,
38- 402 : "Payment Required" ,
39- 403 : "Forbidden" ,
40- 404 : "Not Found" ,
41- 405 : "Method Not Allowed" ,
42- 406 : "Not Acceptable" ,
43- 407 : "Proxy Authentication Required" ,
44- 408 : "Request Timeout" ,
45- 409 : "Conflict" ,
46- 410 : "Gone" ,
47- 411 : "Length Required" ,
48- 412 : "Precondition Failed" ,
49- 413 : "Content Too Large" ,
50- 414 : "URI Too Long" ,
51- 415 : "Unsupported Media Type" ,
52- 416 : "Range Not Satisfiable" ,
53- 417 : "Expectation Failed" ,
54- 418 : "I'm a Teapot" ,
55- 421 : "Misdirected Request" ,
56- 422 : "Unprocessable Content" ,
57- 423 : "Locked" ,
58- 424 : "Failed Dependency" ,
59- 425 : "Too Early" ,
60- 426 : "Upgrade Required" ,
61- 428 : "Precondition Required" ,
62- 429 : "Too Many Requests" ,
63- 431 : "Request Header Fields Too Large" ,
64- 451 : "Unavailable For Legal Reasons" ,
65- 500 : "Internal Server Error" ,
66- 501 : "Not Implemented" ,
67- 502 : "Bad Gateway" ,
68- 503 : "Service Unavailable" ,
69- 504 : "Gateway Timeout" ,
70- 505 : "HTTP Version Not Supported" ,
71- 506 : "Variant Also Negotiates" ,
72- 507 : "Insufficient Storage" ,
73- 508 : "Loop Detected" ,
74- 510 : "Not Extended" ,
75- 511 : "Network Authentication Required" ,
76- }
10+
11+ class StatusCode :
12+ # We're using the same set as stdlib `http.HTTPStatus` here...
13+ #
14+ # https://github.com/python/cpython/blob/main/Lib/http/__init__.py
15+ _codes = {
16+ 100 : "Continue" ,
17+ 101 : "Switching Protocols" ,
18+ 102 : "Processing" ,
19+ 103 : "Early Hints" ,
20+ 200 : "OK" ,
21+ 201 : "Created" ,
22+ 202 : "Accepted" ,
23+ 203 : "Non-Authoritative Information" ,
24+ 204 : "No Content" ,
25+ 205 : "Reset Content" ,
26+ 206 : "Partial Content" ,
27+ 207 : "Multi-Status" ,
28+ 208 : "Already Reported" ,
29+ 226 : "IM Used" ,
30+ 300 : "Multiple Choices" ,
31+ 301 : "Moved Permanently" ,
32+ 302 : "Found" ,
33+ 303 : "See Other" ,
34+ 304 : "Not Modified" ,
35+ 305 : "Use Proxy" ,
36+ 307 : "Temporary Redirect" ,
37+ 308 : "Permanent Redirect" ,
38+ 400 : "Bad Request" ,
39+ 401 : "Unauthorized" ,
40+ 402 : "Payment Required" ,
41+ 403 : "Forbidden" ,
42+ 404 : "Not Found" ,
43+ 405 : "Method Not Allowed" ,
44+ 406 : "Not Acceptable" ,
45+ 407 : "Proxy Authentication Required" ,
46+ 408 : "Request Timeout" ,
47+ 409 : "Conflict" ,
48+ 410 : "Gone" ,
49+ 411 : "Length Required" ,
50+ 412 : "Precondition Failed" ,
51+ 413 : "Content Too Large" ,
52+ 414 : "URI Too Long" ,
53+ 415 : "Unsupported Media Type" ,
54+ 416 : "Range Not Satisfiable" ,
55+ 417 : "Expectation Failed" ,
56+ 418 : "I'm a Teapot" ,
57+ 421 : "Misdirected Request" ,
58+ 422 : "Unprocessable Content" ,
59+ 423 : "Locked" ,
60+ 424 : "Failed Dependency" ,
61+ 425 : "Too Early" ,
62+ 426 : "Upgrade Required" ,
63+ 428 : "Precondition Required" ,
64+ 429 : "Too Many Requests" ,
65+ 431 : "Request Header Fields Too Large" ,
66+ 451 : "Unavailable For Legal Reasons" ,
67+ 500 : "Internal Server Error" ,
68+ 501 : "Not Implemented" ,
69+ 502 : "Bad Gateway" ,
70+ 503 : "Service Unavailable" ,
71+ 504 : "Gateway Timeout" ,
72+ 505 : "HTTP Version Not Supported" ,
73+ 506 : "Variant Also Negotiates" ,
74+ 507 : "Insufficient Storage" ,
75+ 508 : "Loop Detected" ,
76+ 510 : "Not Extended" ,
77+ 511 : "Network Authentication Required" ,
78+ }
79+
80+ def __init__ (self , status_code : int ):
81+ if status_code < 100 or status_code > 999 :
82+ raise ValueError ("Invalid status code {status_code!r}" )
83+ self .value = status_code
84+ self .reason_phrase = self ._codes .get (status_code , "Unknown Status Code" )
85+
86+ def is_1xx_informational (self ) -> bool :
87+ """
88+ Returns `True` for 1xx status codes, `False` otherwise.
89+ """
90+ return 100 <= int (self ) <= 199
91+
92+ def is_2xx_success (self ) -> bool :
93+ """
94+ Returns `True` for 2xx status codes, `False` otherwise.
95+ """
96+ return 200 <= int (self ) <= 299
97+
98+ def is_3xx_redirect (self ) -> bool :
99+ """
100+ Returns `True` for 3xx status codes, `False` otherwise.
101+ """
102+ return 300 <= int (self ) <= 399
103+
104+ def is_4xx_client_error (self ) -> bool :
105+ """
106+ Returns `True` for 4xx status codes, `False` otherwise.
107+ """
108+ return 400 <= int (self ) <= 499
109+
110+ def is_5xx_server_error (self ) -> bool :
111+ """
112+ Returns `True` for 5xx status codes, `False` otherwise.
113+ """
114+ return 500 <= int (self ) <= 599
115+
116+ def as_tuple (self ) -> tuple [int , bytes ]:
117+ return (self .value , self .reason_phrase .encode ('ascii' ))
118+
119+ def __eq__ (self , other ) -> bool :
120+ return int (self ) == int (other )
121+
122+ def __int__ (self ) -> int :
123+ return self .value
124+
125+ def __str__ (self ) -> str :
126+ return f"{ self .value } { self .reason_phrase } "
127+
128+ def __repr__ (self ) -> str :
129+ return f"<StatusCode [{ self .value } { self .reason_phrase } ]>"
77130
78131
79132class Response :
80133 def __init__ (
81134 self ,
82- status_code : int ,
135+ status_code : StatusCode | int ,
83136 * ,
84137 headers : Headers | typing .Mapping [str , str ] | None = None ,
85138 content : Content | Stream | bytes | None = None ,
86139 ):
87- self .status_code = status_code
140+ self .status_code = StatusCode ( status_code ) if not isinstance ( status_code , StatusCode ) else status_code
88141 self .headers = Headers (headers ) if not isinstance (headers , Headers ) else headers
89142 self .stream : Stream = ByteStream (b"" )
90143
@@ -106,17 +159,13 @@ def __init__(
106159 # All 1xx (informational), 204 (no content), and 304 (not modified) responses
107160 # MUST NOT include a message-body. All other responses do include a
108161 # message-body, although it MAY be of zero length.
109- if status_code >= 200 and status_code != 204 and status_code != 304 :
162+ if not ( self . status_code . is_1xx_informational () or self . status_code == 204 or self . status_code == 304 ) :
110163 content_length : int | None = self .stream .size
111164 if content_length is None :
112165 self .headers = self .headers .copy_set ("Transfer-Encoding" , "chunked" )
113166 else :
114167 self .headers = self .headers .copy_set ("Content-Length" , str (content_length ))
115168
116- @property
117- def reason_phrase (self ):
118- return _codes .get (self .status_code , "Unknown Status Code" )
119-
120169 @property
121170 def body (self ) -> bytes :
122171 if not hasattr (self , '_body' ):
@@ -155,4 +204,4 @@ async def __aexit__(self,
155204 await self .close ()
156205
157206 def __repr__ (self ):
158- return f"<Response [{ self .status_code } { self .reason_phrase } ]>"
207+ return f"<Response [{ int ( self .status_code ) } { self . status_code .reason_phrase } ]>"
0 commit comments