Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/urllib3/util/request.py: 45%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import io
4import typing
5from base64 import b64encode
6from enum import Enum
8from ..exceptions import UnrewindableBodyError
9from .util import to_bytes
11if typing.TYPE_CHECKING:
12 from typing import Final
14# Pass as a value within ``headers`` to skip
15# emitting some HTTP headers that are added automatically.
16# The only headers that are supported are ``Accept-Encoding``,
17# ``Host``, and ``User-Agent``.
18SKIP_HEADER = "@@@SKIP_HEADER@@@"
19SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"])
21ACCEPT_ENCODING = "gzip,deflate"
22try:
23 try:
24 import brotlicffi as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401
25 except ImportError:
26 import brotli as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401
27except ImportError:
28 pass
29else:
30 ACCEPT_ENCODING += ",br"
32try:
33 from compression import ( # type: ignore[import-not-found] # noqa: F401
34 zstd as _unused_module_zstd,
35 )
37 ACCEPT_ENCODING += ",zstd"
38except ImportError:
39 try:
40 import zstandard as _unused_module_zstd # noqa: F401
42 ACCEPT_ENCODING += ",zstd"
43 except ImportError:
44 pass
47class _TYPE_FAILEDTELL(Enum):
48 token = 0
51_FAILEDTELL: Final[_TYPE_FAILEDTELL] = _TYPE_FAILEDTELL.token
53_TYPE_BODY_POSITION = typing.Union[int, _TYPE_FAILEDTELL]
55# When sending a request with these methods we aren't expecting
56# a body so don't need to set an explicit 'Content-Length: 0'
57# The reason we do this in the negative instead of tracking methods
58# which 'should' have a body is because unknown methods should be
59# treated as if they were 'POST' which *does* expect a body.
60_METHODS_NOT_EXPECTING_BODY = {"GET", "HEAD", "DELETE", "TRACE", "OPTIONS", "CONNECT"}
63def make_headers(
64 keep_alive: bool | None = None,
65 accept_encoding: bool | list[str] | str | None = None,
66 user_agent: str | None = None,
67 basic_auth: str | None = None,
68 proxy_basic_auth: str | None = None,
69 disable_cache: bool | None = None,
70) -> dict[str, str]:
71 """
72 Shortcuts for generating request headers.
74 :param keep_alive:
75 If ``True``, adds 'connection: keep-alive' header.
77 :param accept_encoding:
78 Can be a boolean, list, or string.
79 ``True`` translates to 'gzip,deflate'. If the dependencies for
80 Brotli (either the ``brotli`` or ``brotlicffi`` package) and/or Zstandard
81 (the ``zstandard`` package) algorithms are installed, then their encodings are
82 included in the string ('br' and 'zstd', respectively).
83 List will get joined by comma.
84 String will be used as provided.
86 :param user_agent:
87 String representing the user-agent you want, such as
88 "python-urllib3/0.6"
90 :param basic_auth:
91 Colon-separated username:password string for 'authorization: basic ...'
92 auth header.
94 :param proxy_basic_auth:
95 Colon-separated username:password string for 'proxy-authorization: basic ...'
96 auth header.
98 :param disable_cache:
99 If ``True``, adds 'cache-control: no-cache' header.
101 Example:
103 .. code-block:: python
105 import urllib3
107 print(urllib3.util.make_headers(keep_alive=True, user_agent="Batman/1.0"))
108 # {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'}
109 print(urllib3.util.make_headers(accept_encoding=True))
110 # {'accept-encoding': 'gzip,deflate'}
111 """
112 headers: dict[str, str] = {}
113 if accept_encoding:
114 if isinstance(accept_encoding, str):
115 pass
116 elif isinstance(accept_encoding, list):
117 accept_encoding = ",".join(accept_encoding)
118 else:
119 accept_encoding = ACCEPT_ENCODING
120 headers["accept-encoding"] = accept_encoding
122 if user_agent:
123 headers["user-agent"] = user_agent
125 if keep_alive:
126 headers["connection"] = "keep-alive"
128 if basic_auth:
129 headers["authorization"] = (
130 f"Basic {b64encode(basic_auth.encode('latin-1')).decode()}"
131 )
133 if proxy_basic_auth:
134 headers["proxy-authorization"] = (
135 f"Basic {b64encode(proxy_basic_auth.encode('latin-1')).decode()}"
136 )
138 if disable_cache:
139 headers["cache-control"] = "no-cache"
141 return headers
144def set_file_position(
145 body: typing.Any, pos: _TYPE_BODY_POSITION | None
146) -> _TYPE_BODY_POSITION | None:
147 """
148 If a position is provided, move file to that point.
149 Otherwise, we'll attempt to record a position for future use.
150 """
151 if pos is not None:
152 rewind_body(body, pos)
153 elif getattr(body, "tell", None) is not None:
154 try:
155 pos = body.tell()
156 except OSError:
157 # This differentiates from None, allowing us to catch
158 # a failed `tell()` later when trying to rewind the body.
159 pos = _FAILEDTELL
161 return pos
164def rewind_body(body: typing.IO[typing.AnyStr], body_pos: _TYPE_BODY_POSITION) -> None:
165 """
166 Attempt to rewind body to a certain position.
167 Primarily used for request redirects and retries.
169 :param body:
170 File-like object that supports seek.
172 :param int pos:
173 Position to seek to in file.
174 """
175 body_seek = getattr(body, "seek", None)
176 if body_seek is not None and isinstance(body_pos, int):
177 try:
178 body_seek(body_pos)
179 except OSError as e:
180 raise UnrewindableBodyError(
181 "An error occurred when rewinding request body for redirect/retry."
182 ) from e
183 elif body_pos is _FAILEDTELL:
184 raise UnrewindableBodyError(
185 "Unable to record file position for rewinding "
186 "request body during a redirect/retry."
187 )
188 else:
189 raise ValueError(
190 f"body_pos must be of type integer, instead it was {type(body_pos)}."
191 )
194class ChunksAndContentLength(typing.NamedTuple):
195 chunks: typing.Iterable[bytes] | None
196 content_length: int | None
199def body_to_chunks(
200 body: typing.Any | None, method: str, blocksize: int
201) -> ChunksAndContentLength:
202 """Takes the HTTP request method, body, and blocksize and
203 transforms them into an iterable of chunks to pass to
204 socket.sendall() and an optional 'Content-Length' header.
206 A 'Content-Length' of 'None' indicates the length of the body
207 can't be determined so should use 'Transfer-Encoding: chunked'
208 for framing instead.
209 """
211 chunks: typing.Iterable[bytes] | None
212 content_length: int | None
214 # No body, we need to make a recommendation on 'Content-Length'
215 # based on whether that request method is expected to have
216 # a body or not.
217 if body is None:
218 chunks = None
219 if method.upper() not in _METHODS_NOT_EXPECTING_BODY:
220 content_length = 0
221 else:
222 content_length = None
224 # Bytes or strings become bytes
225 elif isinstance(body, (str, bytes)):
226 chunks = (to_bytes(body),)
227 content_length = len(chunks[0])
229 # File-like object, TODO: use seek() and tell() for length?
230 elif hasattr(body, "read"):
232 def chunk_readable() -> typing.Iterable[bytes]:
233 nonlocal body, blocksize
234 encode = isinstance(body, io.TextIOBase)
235 while True:
236 datablock = body.read(blocksize)
237 if not datablock:
238 break
239 if encode:
240 datablock = datablock.encode("utf-8")
241 yield datablock
243 chunks = chunk_readable()
244 content_length = None
246 # Otherwise we need to start checking via duck-typing.
247 else:
248 try:
249 # Check if the body implements the buffer API.
250 mv = memoryview(body)
251 except TypeError:
252 try:
253 # Check if the body is an iterable
254 chunks = iter(body)
255 content_length = None
256 except TypeError:
257 raise TypeError(
258 f"'body' must be a bytes-like object, file-like "
259 f"object, or iterable. Instead was {body!r}"
260 ) from None
261 else:
262 # Since it implements the buffer API can be passed directly to socket.sendall()
263 chunks = (body,)
264 content_length = mv.nbytes
266 return ChunksAndContentLength(chunks=chunks, content_length=content_length)