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