|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- from __future__ import annotations
-
- import time
- import typing
- from enum import Enum
- from socket import getdefaulttimeout
-
- from ..exceptions import TimeoutStateError
-
- if typing.TYPE_CHECKING:
- from typing_extensions import Final
-
-
- class _TYPE_DEFAULT(Enum):
- # This value should never be passed to socket.settimeout() so for safety we use a -1.
- # socket.settimout() raises a ValueError for negative values.
- token = -1
-
-
- _DEFAULT_TIMEOUT: Final[_TYPE_DEFAULT] = _TYPE_DEFAULT.token
-
- _TYPE_TIMEOUT = typing.Optional[typing.Union[float, _TYPE_DEFAULT]]
-
-
- class Timeout:
- """Timeout configuration.
-
- Timeouts can be defined as a default for a pool:
-
- .. code-block:: python
-
- import urllib3
-
- timeout = urllib3.util.Timeout(connect=2.0, read=7.0)
-
- http = urllib3.PoolManager(timeout=timeout)
-
- resp = http.request("GET", "https://example.com/")
-
- print(resp.status)
-
- Or per-request (which overrides the default for the pool):
-
- .. code-block:: python
-
- response = http.request("GET", "https://example.com/", timeout=Timeout(10))
-
- Timeouts can be disabled by setting all the parameters to ``None``:
-
- .. code-block:: python
-
- no_timeout = Timeout(connect=None, read=None)
- response = http.request("GET", "https://example.com/", timeout=no_timeout)
-
-
- :param total:
- This combines the connect and read timeouts into one; the read timeout
- will be set to the time leftover from the connect attempt. In the
- event that both a connect timeout and a total are specified, or a read
- timeout and a total are specified, the shorter timeout will be applied.
-
- Defaults to None.
-
- :type total: int, float, or None
-
- :param connect:
- The maximum amount of time (in seconds) to wait for a connection
- attempt to a server to succeed. Omitting the parameter will default the
- connect timeout to the system default, probably `the global default
- timeout in socket.py
- <http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_.
- None will set an infinite timeout for connection attempts.
-
- :type connect: int, float, or None
-
- :param read:
- The maximum amount of time (in seconds) to wait between consecutive
- read operations for a response from the server. Omitting the parameter
- will default the read timeout to the system default, probably `the
- global default timeout in socket.py
- <http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_.
- None will set an infinite timeout.
-
- :type read: int, float, or None
-
- .. note::
-
- Many factors can affect the total amount of time for urllib3 to return
- an HTTP response.
-
- For example, Python's DNS resolver does not obey the timeout specified
- on the socket. Other factors that can affect total request time include
- high CPU load, high swap, the program running at a low priority level,
- or other behaviors.
-
- In addition, the read and total timeouts only measure the time between
- read operations on the socket connecting the client and the server,
- not the total amount of time for the request to return a complete
- response. For most requests, the timeout is raised because the server
- has not sent the first byte in the specified time. This is not always
- the case; if a server streams one byte every fifteen seconds, a timeout
- of 20 seconds will not trigger, even though the request will take
- several minutes to complete.
-
- If your goal is to cut off any request after a set amount of wall clock
- time, consider having a second "watcher" thread to cut off a slow
- request.
- """
-
- #: A sentinel object representing the default timeout value
- DEFAULT_TIMEOUT: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT
-
- def __init__(
- self,
- total: _TYPE_TIMEOUT = None,
- connect: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT,
- read: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT,
- ) -> None:
- self._connect = self._validate_timeout(connect, "connect")
- self._read = self._validate_timeout(read, "read")
- self.total = self._validate_timeout(total, "total")
- self._start_connect: float | None = None
-
- def __repr__(self) -> str:
- return f"{type(self).__name__}(connect={self._connect!r}, read={self._read!r}, total={self.total!r})"
-
- # __str__ provided for backwards compatibility
- __str__ = __repr__
-
- @staticmethod
- def resolve_default_timeout(timeout: _TYPE_TIMEOUT) -> float | None:
- return getdefaulttimeout() if timeout is _DEFAULT_TIMEOUT else timeout
-
- @classmethod
- def _validate_timeout(cls, value: _TYPE_TIMEOUT, name: str) -> _TYPE_TIMEOUT:
- """Check that a timeout attribute is valid.
-
- :param value: The timeout value to validate
- :param name: The name of the timeout attribute to validate. This is
- used to specify in error messages.
- :return: The validated and casted version of the given value.
- :raises ValueError: If it is a numeric value less than or equal to
- zero, or the type is not an integer, float, or None.
- """
- if value is None or value is _DEFAULT_TIMEOUT:
- return value
-
- if isinstance(value, bool):
- raise ValueError(
- "Timeout cannot be a boolean value. It must "
- "be an int, float or None."
- )
- try:
- float(value)
- except (TypeError, ValueError):
- raise ValueError(
- "Timeout value %s was %s, but it must be an "
- "int, float or None." % (name, value)
- ) from None
-
- try:
- if value <= 0:
- raise ValueError(
- "Attempted to set %s timeout to %s, but the "
- "timeout cannot be set to a value less "
- "than or equal to 0." % (name, value)
- )
- except TypeError:
- raise ValueError(
- "Timeout value %s was %s, but it must be an "
- "int, float or None." % (name, value)
- ) from None
-
- return value
-
- @classmethod
- def from_float(cls, timeout: _TYPE_TIMEOUT) -> Timeout:
- """Create a new Timeout from a legacy timeout value.
-
- The timeout value used by httplib.py sets the same timeout on the
- connect(), and recv() socket requests. This creates a :class:`Timeout`
- object that sets the individual timeouts to the ``timeout`` value
- passed to this function.
-
- :param timeout: The legacy timeout value.
- :type timeout: integer, float, :attr:`urllib3.util.Timeout.DEFAULT_TIMEOUT`, or None
- :return: Timeout object
- :rtype: :class:`Timeout`
- """
- return Timeout(read=timeout, connect=timeout)
-
- def clone(self) -> Timeout:
- """Create a copy of the timeout object
-
- Timeout properties are stored per-pool but each request needs a fresh
- Timeout object to ensure each one has its own start/stop configured.
-
- :return: a copy of the timeout object
- :rtype: :class:`Timeout`
- """
- # We can't use copy.deepcopy because that will also create a new object
- # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to
- # detect the user default.
- return Timeout(connect=self._connect, read=self._read, total=self.total)
-
- def start_connect(self) -> float:
- """Start the timeout clock, used during a connect() attempt
-
- :raises urllib3.exceptions.TimeoutStateError: if you attempt
- to start a timer that has been started already.
- """
- if self._start_connect is not None:
- raise TimeoutStateError("Timeout timer has already been started.")
- self._start_connect = time.monotonic()
- return self._start_connect
-
- def get_connect_duration(self) -> float:
- """Gets the time elapsed since the call to :meth:`start_connect`.
-
- :return: Elapsed time in seconds.
- :rtype: float
- :raises urllib3.exceptions.TimeoutStateError: if you attempt
- to get duration for a timer that hasn't been started.
- """
- if self._start_connect is None:
- raise TimeoutStateError(
- "Can't get connect duration for timer that has not started."
- )
- return time.monotonic() - self._start_connect
-
- @property
- def connect_timeout(self) -> _TYPE_TIMEOUT:
- """Get the value to use when setting a connection timeout.
-
- This will be a positive float or integer, the value None
- (never timeout), or the default system timeout.
-
- :return: Connect timeout.
- :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None
- """
- if self.total is None:
- return self._connect
-
- if self._connect is None or self._connect is _DEFAULT_TIMEOUT:
- return self.total
-
- return min(self._connect, self.total) # type: ignore[type-var]
-
- @property
- def read_timeout(self) -> float | None:
- """Get the value for the read timeout.
-
- This assumes some time has elapsed in the connection timeout and
- computes the read timeout appropriately.
-
- If self.total is set, the read timeout is dependent on the amount of
- time taken by the connect timeout. If the connection time has not been
- established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be
- raised.
-
- :return: Value to use for the read timeout.
- :rtype: int, float or None
- :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect`
- has not yet been called on this object.
- """
- if (
- self.total is not None
- and self.total is not _DEFAULT_TIMEOUT
- and self._read is not None
- and self._read is not _DEFAULT_TIMEOUT
- ):
- # In case the connect timeout has not yet been established.
- if self._start_connect is None:
- return self._read
- return max(0, min(self.total - self.get_connect_duration(), self._read))
- elif self.total is not None and self.total is not _DEFAULT_TIMEOUT:
- return max(0, self.total - self.get_connect_duration())
- else:
- return self.resolve_default_timeout(self._read)
|