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