jpayne@7
|
1 from __future__ import annotations
|
jpayne@7
|
2
|
jpayne@7
|
3 import time
|
jpayne@7
|
4 import typing
|
jpayne@7
|
5 from enum import Enum
|
jpayne@7
|
6 from socket import getdefaulttimeout
|
jpayne@7
|
7
|
jpayne@7
|
8 from ..exceptions import TimeoutStateError
|
jpayne@7
|
9
|
jpayne@7
|
10 if typing.TYPE_CHECKING:
|
jpayne@7
|
11 from typing import Final
|
jpayne@7
|
12
|
jpayne@7
|
13
|
jpayne@7
|
14 class _TYPE_DEFAULT(Enum):
|
jpayne@7
|
15 # This value should never be passed to socket.settimeout() so for safety we use a -1.
|
jpayne@7
|
16 # socket.settimout() raises a ValueError for negative values.
|
jpayne@7
|
17 token = -1
|
jpayne@7
|
18
|
jpayne@7
|
19
|
jpayne@7
|
20 _DEFAULT_TIMEOUT: Final[_TYPE_DEFAULT] = _TYPE_DEFAULT.token
|
jpayne@7
|
21
|
jpayne@7
|
22 _TYPE_TIMEOUT = typing.Optional[typing.Union[float, _TYPE_DEFAULT]]
|
jpayne@7
|
23
|
jpayne@7
|
24
|
jpayne@7
|
25 class Timeout:
|
jpayne@7
|
26 """Timeout configuration.
|
jpayne@7
|
27
|
jpayne@7
|
28 Timeouts can be defined as a default for a pool:
|
jpayne@7
|
29
|
jpayne@7
|
30 .. code-block:: python
|
jpayne@7
|
31
|
jpayne@7
|
32 import urllib3
|
jpayne@7
|
33
|
jpayne@7
|
34 timeout = urllib3.util.Timeout(connect=2.0, read=7.0)
|
jpayne@7
|
35
|
jpayne@7
|
36 http = urllib3.PoolManager(timeout=timeout)
|
jpayne@7
|
37
|
jpayne@7
|
38 resp = http.request("GET", "https://example.com/")
|
jpayne@7
|
39
|
jpayne@7
|
40 print(resp.status)
|
jpayne@7
|
41
|
jpayne@7
|
42 Or per-request (which overrides the default for the pool):
|
jpayne@7
|
43
|
jpayne@7
|
44 .. code-block:: python
|
jpayne@7
|
45
|
jpayne@7
|
46 response = http.request("GET", "https://example.com/", timeout=Timeout(10))
|
jpayne@7
|
47
|
jpayne@7
|
48 Timeouts can be disabled by setting all the parameters to ``None``:
|
jpayne@7
|
49
|
jpayne@7
|
50 .. code-block:: python
|
jpayne@7
|
51
|
jpayne@7
|
52 no_timeout = Timeout(connect=None, read=None)
|
jpayne@7
|
53 response = http.request("GET", "https://example.com/", timeout=no_timeout)
|
jpayne@7
|
54
|
jpayne@7
|
55
|
jpayne@7
|
56 :param total:
|
jpayne@7
|
57 This combines the connect and read timeouts into one; the read timeout
|
jpayne@7
|
58 will be set to the time leftover from the connect attempt. In the
|
jpayne@7
|
59 event that both a connect timeout and a total are specified, or a read
|
jpayne@7
|
60 timeout and a total are specified, the shorter timeout will be applied.
|
jpayne@7
|
61
|
jpayne@7
|
62 Defaults to None.
|
jpayne@7
|
63
|
jpayne@7
|
64 :type total: int, float, or None
|
jpayne@7
|
65
|
jpayne@7
|
66 :param connect:
|
jpayne@7
|
67 The maximum amount of time (in seconds) to wait for a connection
|
jpayne@7
|
68 attempt to a server to succeed. Omitting the parameter will default the
|
jpayne@7
|
69 connect timeout to the system default, probably `the global default
|
jpayne@7
|
70 timeout in socket.py
|
jpayne@7
|
71 <http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_.
|
jpayne@7
|
72 None will set an infinite timeout for connection attempts.
|
jpayne@7
|
73
|
jpayne@7
|
74 :type connect: int, float, or None
|
jpayne@7
|
75
|
jpayne@7
|
76 :param read:
|
jpayne@7
|
77 The maximum amount of time (in seconds) to wait between consecutive
|
jpayne@7
|
78 read operations for a response from the server. Omitting the parameter
|
jpayne@7
|
79 will default the read timeout to the system default, probably `the
|
jpayne@7
|
80 global default timeout in socket.py
|
jpayne@7
|
81 <http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_.
|
jpayne@7
|
82 None will set an infinite timeout.
|
jpayne@7
|
83
|
jpayne@7
|
84 :type read: int, float, or None
|
jpayne@7
|
85
|
jpayne@7
|
86 .. note::
|
jpayne@7
|
87
|
jpayne@7
|
88 Many factors can affect the total amount of time for urllib3 to return
|
jpayne@7
|
89 an HTTP response.
|
jpayne@7
|
90
|
jpayne@7
|
91 For example, Python's DNS resolver does not obey the timeout specified
|
jpayne@7
|
92 on the socket. Other factors that can affect total request time include
|
jpayne@7
|
93 high CPU load, high swap, the program running at a low priority level,
|
jpayne@7
|
94 or other behaviors.
|
jpayne@7
|
95
|
jpayne@7
|
96 In addition, the read and total timeouts only measure the time between
|
jpayne@7
|
97 read operations on the socket connecting the client and the server,
|
jpayne@7
|
98 not the total amount of time for the request to return a complete
|
jpayne@7
|
99 response. For most requests, the timeout is raised because the server
|
jpayne@7
|
100 has not sent the first byte in the specified time. This is not always
|
jpayne@7
|
101 the case; if a server streams one byte every fifteen seconds, a timeout
|
jpayne@7
|
102 of 20 seconds will not trigger, even though the request will take
|
jpayne@7
|
103 several minutes to complete.
|
jpayne@7
|
104 """
|
jpayne@7
|
105
|
jpayne@7
|
106 #: A sentinel object representing the default timeout value
|
jpayne@7
|
107 DEFAULT_TIMEOUT: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT
|
jpayne@7
|
108
|
jpayne@7
|
109 def __init__(
|
jpayne@7
|
110 self,
|
jpayne@7
|
111 total: _TYPE_TIMEOUT = None,
|
jpayne@7
|
112 connect: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT,
|
jpayne@7
|
113 read: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT,
|
jpayne@7
|
114 ) -> None:
|
jpayne@7
|
115 self._connect = self._validate_timeout(connect, "connect")
|
jpayne@7
|
116 self._read = self._validate_timeout(read, "read")
|
jpayne@7
|
117 self.total = self._validate_timeout(total, "total")
|
jpayne@7
|
118 self._start_connect: float | None = None
|
jpayne@7
|
119
|
jpayne@7
|
120 def __repr__(self) -> str:
|
jpayne@7
|
121 return f"{type(self).__name__}(connect={self._connect!r}, read={self._read!r}, total={self.total!r})"
|
jpayne@7
|
122
|
jpayne@7
|
123 # __str__ provided for backwards compatibility
|
jpayne@7
|
124 __str__ = __repr__
|
jpayne@7
|
125
|
jpayne@7
|
126 @staticmethod
|
jpayne@7
|
127 def resolve_default_timeout(timeout: _TYPE_TIMEOUT) -> float | None:
|
jpayne@7
|
128 return getdefaulttimeout() if timeout is _DEFAULT_TIMEOUT else timeout
|
jpayne@7
|
129
|
jpayne@7
|
130 @classmethod
|
jpayne@7
|
131 def _validate_timeout(cls, value: _TYPE_TIMEOUT, name: str) -> _TYPE_TIMEOUT:
|
jpayne@7
|
132 """Check that a timeout attribute is valid.
|
jpayne@7
|
133
|
jpayne@7
|
134 :param value: The timeout value to validate
|
jpayne@7
|
135 :param name: The name of the timeout attribute to validate. This is
|
jpayne@7
|
136 used to specify in error messages.
|
jpayne@7
|
137 :return: The validated and casted version of the given value.
|
jpayne@7
|
138 :raises ValueError: If it is a numeric value less than or equal to
|
jpayne@7
|
139 zero, or the type is not an integer, float, or None.
|
jpayne@7
|
140 """
|
jpayne@7
|
141 if value is None or value is _DEFAULT_TIMEOUT:
|
jpayne@7
|
142 return value
|
jpayne@7
|
143
|
jpayne@7
|
144 if isinstance(value, bool):
|
jpayne@7
|
145 raise ValueError(
|
jpayne@7
|
146 "Timeout cannot be a boolean value. It must "
|
jpayne@7
|
147 "be an int, float or None."
|
jpayne@7
|
148 )
|
jpayne@7
|
149 try:
|
jpayne@7
|
150 float(value)
|
jpayne@7
|
151 except (TypeError, ValueError):
|
jpayne@7
|
152 raise ValueError(
|
jpayne@7
|
153 "Timeout value %s was %s, but it must be an "
|
jpayne@7
|
154 "int, float or None." % (name, value)
|
jpayne@7
|
155 ) from None
|
jpayne@7
|
156
|
jpayne@7
|
157 try:
|
jpayne@7
|
158 if value <= 0:
|
jpayne@7
|
159 raise ValueError(
|
jpayne@7
|
160 "Attempted to set %s timeout to %s, but the "
|
jpayne@7
|
161 "timeout cannot be set to a value less "
|
jpayne@7
|
162 "than or equal to 0." % (name, value)
|
jpayne@7
|
163 )
|
jpayne@7
|
164 except TypeError:
|
jpayne@7
|
165 raise ValueError(
|
jpayne@7
|
166 "Timeout value %s was %s, but it must be an "
|
jpayne@7
|
167 "int, float or None." % (name, value)
|
jpayne@7
|
168 ) from None
|
jpayne@7
|
169
|
jpayne@7
|
170 return value
|
jpayne@7
|
171
|
jpayne@7
|
172 @classmethod
|
jpayne@7
|
173 def from_float(cls, timeout: _TYPE_TIMEOUT) -> Timeout:
|
jpayne@7
|
174 """Create a new Timeout from a legacy timeout value.
|
jpayne@7
|
175
|
jpayne@7
|
176 The timeout value used by httplib.py sets the same timeout on the
|
jpayne@7
|
177 connect(), and recv() socket requests. This creates a :class:`Timeout`
|
jpayne@7
|
178 object that sets the individual timeouts to the ``timeout`` value
|
jpayne@7
|
179 passed to this function.
|
jpayne@7
|
180
|
jpayne@7
|
181 :param timeout: The legacy timeout value.
|
jpayne@7
|
182 :type timeout: integer, float, :attr:`urllib3.util.Timeout.DEFAULT_TIMEOUT`, or None
|
jpayne@7
|
183 :return: Timeout object
|
jpayne@7
|
184 :rtype: :class:`Timeout`
|
jpayne@7
|
185 """
|
jpayne@7
|
186 return Timeout(read=timeout, connect=timeout)
|
jpayne@7
|
187
|
jpayne@7
|
188 def clone(self) -> Timeout:
|
jpayne@7
|
189 """Create a copy of the timeout object
|
jpayne@7
|
190
|
jpayne@7
|
191 Timeout properties are stored per-pool but each request needs a fresh
|
jpayne@7
|
192 Timeout object to ensure each one has its own start/stop configured.
|
jpayne@7
|
193
|
jpayne@7
|
194 :return: a copy of the timeout object
|
jpayne@7
|
195 :rtype: :class:`Timeout`
|
jpayne@7
|
196 """
|
jpayne@7
|
197 # We can't use copy.deepcopy because that will also create a new object
|
jpayne@7
|
198 # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to
|
jpayne@7
|
199 # detect the user default.
|
jpayne@7
|
200 return Timeout(connect=self._connect, read=self._read, total=self.total)
|
jpayne@7
|
201
|
jpayne@7
|
202 def start_connect(self) -> float:
|
jpayne@7
|
203 """Start the timeout clock, used during a connect() attempt
|
jpayne@7
|
204
|
jpayne@7
|
205 :raises urllib3.exceptions.TimeoutStateError: if you attempt
|
jpayne@7
|
206 to start a timer that has been started already.
|
jpayne@7
|
207 """
|
jpayne@7
|
208 if self._start_connect is not None:
|
jpayne@7
|
209 raise TimeoutStateError("Timeout timer has already been started.")
|
jpayne@7
|
210 self._start_connect = time.monotonic()
|
jpayne@7
|
211 return self._start_connect
|
jpayne@7
|
212
|
jpayne@7
|
213 def get_connect_duration(self) -> float:
|
jpayne@7
|
214 """Gets the time elapsed since the call to :meth:`start_connect`.
|
jpayne@7
|
215
|
jpayne@7
|
216 :return: Elapsed time in seconds.
|
jpayne@7
|
217 :rtype: float
|
jpayne@7
|
218 :raises urllib3.exceptions.TimeoutStateError: if you attempt
|
jpayne@7
|
219 to get duration for a timer that hasn't been started.
|
jpayne@7
|
220 """
|
jpayne@7
|
221 if self._start_connect is None:
|
jpayne@7
|
222 raise TimeoutStateError(
|
jpayne@7
|
223 "Can't get connect duration for timer that has not started."
|
jpayne@7
|
224 )
|
jpayne@7
|
225 return time.monotonic() - self._start_connect
|
jpayne@7
|
226
|
jpayne@7
|
227 @property
|
jpayne@7
|
228 def connect_timeout(self) -> _TYPE_TIMEOUT:
|
jpayne@7
|
229 """Get the value to use when setting a connection timeout.
|
jpayne@7
|
230
|
jpayne@7
|
231 This will be a positive float or integer, the value None
|
jpayne@7
|
232 (never timeout), or the default system timeout.
|
jpayne@7
|
233
|
jpayne@7
|
234 :return: Connect timeout.
|
jpayne@7
|
235 :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None
|
jpayne@7
|
236 """
|
jpayne@7
|
237 if self.total is None:
|
jpayne@7
|
238 return self._connect
|
jpayne@7
|
239
|
jpayne@7
|
240 if self._connect is None or self._connect is _DEFAULT_TIMEOUT:
|
jpayne@7
|
241 return self.total
|
jpayne@7
|
242
|
jpayne@7
|
243 return min(self._connect, self.total) # type: ignore[type-var]
|
jpayne@7
|
244
|
jpayne@7
|
245 @property
|
jpayne@7
|
246 def read_timeout(self) -> float | None:
|
jpayne@7
|
247 """Get the value for the read timeout.
|
jpayne@7
|
248
|
jpayne@7
|
249 This assumes some time has elapsed in the connection timeout and
|
jpayne@7
|
250 computes the read timeout appropriately.
|
jpayne@7
|
251
|
jpayne@7
|
252 If self.total is set, the read timeout is dependent on the amount of
|
jpayne@7
|
253 time taken by the connect timeout. If the connection time has not been
|
jpayne@7
|
254 established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be
|
jpayne@7
|
255 raised.
|
jpayne@7
|
256
|
jpayne@7
|
257 :return: Value to use for the read timeout.
|
jpayne@7
|
258 :rtype: int, float or None
|
jpayne@7
|
259 :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect`
|
jpayne@7
|
260 has not yet been called on this object.
|
jpayne@7
|
261 """
|
jpayne@7
|
262 if (
|
jpayne@7
|
263 self.total is not None
|
jpayne@7
|
264 and self.total is not _DEFAULT_TIMEOUT
|
jpayne@7
|
265 and self._read is not None
|
jpayne@7
|
266 and self._read is not _DEFAULT_TIMEOUT
|
jpayne@7
|
267 ):
|
jpayne@7
|
268 # In case the connect timeout has not yet been established.
|
jpayne@7
|
269 if self._start_connect is None:
|
jpayne@7
|
270 return self._read
|
jpayne@7
|
271 return max(0, min(self.total - self.get_connect_duration(), self._read))
|
jpayne@7
|
272 elif self.total is not None and self.total is not _DEFAULT_TIMEOUT:
|
jpayne@7
|
273 return max(0, self.total - self.get_connect_duration())
|
jpayne@7
|
274 else:
|
jpayne@7
|
275 return self.resolve_default_timeout(self._read)
|