-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path_request.py
More file actions
98 lines (83 loc) · 3.63 KB
/
_request.py
File metadata and controls
98 lines (83 loc) · 3.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import types
import typing
from ._content import Content
from ._streams import ByteStream, Stream
from ._headers import Headers
from ._urls import URL
__all__ = ["Request"]
class Request:
def __init__(
self,
method: str,
url: URL | str,
headers: Headers | typing.Mapping[str, str] | None = None,
content: Content | Stream | bytes | None = None,
):
self.method = method
self.url = URL(url)
self.headers = Headers(headers)
self.stream: Stream = ByteStream(b"")
# https://datatracker.ietf.org/doc/html/rfc2616#section-14.23
# RFC 2616, Section 14.23, Host.
#
# A client MUST include a Host header field in all HTTP/1.1 request messages.
if "Host" not in self.headers:
self.headers = self.headers.copy_set("Host", self.url.netloc)
if self.url.scheme not in ('http', 'https'):
raise ValueError(f'Invalid scheme for URL {str(self.url)!r}.')
if not self.url.netloc:
raise ValueError(f'Missing host for URL {str(self.url)!r}.')
if content is not None:
if isinstance(content, bytes):
self.stream = ByteStream(content)
elif isinstance(content, Stream):
self.stream = content
elif isinstance(content, Content):
ct = content.content_type()
self.stream = content.encode()
self.headers = self.headers.copy_set("Content-Type", ct)
else:
raise TypeError(f'Expected `Content | Stream | bytes | None` got {type(content)}')
# https://datatracker.ietf.org/doc/html/rfc2616#section-4.3
# RFC 2616, Section 4.3, Message Body.
#
# The presence of a message-body in a request is signaled by the
# inclusion of a Content-Length or Transfer-Encoding header field in
# the request's message-headers.
content_length: int | None = self.stream.size
if content_length is None:
self.headers = self.headers.copy_set("Transfer-Encoding", "chunked")
elif content_length > 0:
self.headers = self.headers.copy_set("Content-Length", str(content_length))
elif method in ("POST", "PUT", "PATCH"):
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
# RFC 7230, Section 3.3.2, Content Length.
#
# A user agent SHOULD send a Content-Length in a request message when no
# Transfer-Encoding is sent and the request method defines a meaning for
# an enclosed payload body. For example, a Content-Length header field is
# normally sent in a POST request even when the value is 0.
# (indicating an empty payload body).
self.headers = self.headers.copy_set("Content-Length", "0")
@property
def body(self) -> bytes:
if not hasattr(self, '_body'):
raise RuntimeError("'.body' cannot be accessed without calling '.read()'")
return self._body
def read(self) -> bytes:
if not hasattr(self, '_body'):
self._body = self.stream.read()
self.stream = ByteStream(self._body)
return self._body
def close(self) -> None:
self.stream.close()
def __enter__(self):
return self
def __exit__(self,
exc_type: type[BaseException] | None = None,
exc_value: BaseException | None = None,
traceback: types.TracebackType | None = None
):
self.close()
def __repr__(self):
return f"<Request [{self.method} {str(self.url)!r}]>"