diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6268424a6..3e88d425d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,7 +28,7 @@ repos: - --jobs=0 additional_dependencies: - - httpx~=0.24.1 + - httpx~=0.25.0 - tornado~=6.3.3 - APScheduler~=3.10.4 - cachetools~=5.3.1 @@ -44,7 +44,7 @@ repos: - types-pytz - types-cryptography - types-cachetools - - httpx~=0.24.1 + - httpx~=0.25.0 - tornado~=6.3.3 - APScheduler~=3.10.4 - cachetools~=5.3.1 @@ -83,7 +83,7 @@ repos: name: ruff files: ^(telegram|examples|tests)/.*\.py$ additional_dependencies: - - httpx~=0.24.1 + - httpx~=0.25.0 - tornado~=6.3.3 - APScheduler~=3.10.4 - cachetools~=5.3.1 diff --git a/telegram/request/_httpxrequest.py b/telegram/request/_httpxrequest.py index 09898b65e..ae2673e5d 100644 --- a/telegram/request/_httpxrequest.py +++ b/telegram/request/_httpxrequest.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains methods to make POST and GET requests using the httpx library.""" -from typing import Optional, Tuple +from typing import Collection, Optional, Tuple, Union import httpx @@ -35,6 +35,12 @@ from telegram.request._requestdata import RequestData _LOGGER = get_logger(__name__, "HTTPXRequest") +_SocketOpt = Union[ + Tuple[int, int, int], + Tuple[int, int, Union[bytes, bytearray]], + Tuple[int, int, None, int], +] + class HTTPXRequest(BaseRequest): """Implementation of :class:`~telegram.request.BaseRequest` using the library @@ -91,6 +97,16 @@ class HTTPXRequest(BaseRequest): .. versionchanged:: 20.5 Accept ``"2"`` as a valid value. + socket_options (Collection[:obj:`tuple`], optional): Socket options to be passed to the + underlying `library \ + `_. + + Note: + The values accepted by this parameter depend on the operating system. + This is a low-level parameter and should only be used if you are familiar with + these concepts. + + .. versionadded:: NEXT.VERSION """ @@ -105,6 +121,7 @@ class HTTPXRequest(BaseRequest): connect_timeout: Optional[float] = 5.0, pool_timeout: Optional[float] = 1.0, http_version: HTTPVersion = "1.1", + socket_options: Optional[Collection[_SocketOpt]] = None, ): self._http_version = http_version timeout = httpx.Timeout( @@ -122,16 +139,21 @@ class HTTPXRequest(BaseRequest): raise ValueError("`http_version` must be either '1.1', '2.0' or '2'.") http1 = http_version == "1.1" - - # See https://github.com/python-telegram-bot/python-telegram-bot/pull/3542 - # for why we need to use `dict()` here. - self._client_kwargs = dict( # pylint: disable=use-dict-literal # noqa: C408 - timeout=timeout, - proxies=proxy_url, - limits=limits, - http1=http1, - http2=not http1, + http_kwargs = {"http1": http1, "http2": not http1} + transport = ( + httpx.AsyncHTTPTransport( + socket_options=socket_options, + ) + if socket_options + else None ) + self._client_kwargs = { + "timeout": timeout, + "proxies": proxy_url, + "limits": limits, + "transport": transport, + **http_kwargs, + } try: self._client = self._build_client() @@ -206,12 +228,6 @@ class HTTPXRequest(BaseRequest): pool=pool_timeout, ) - # TODO p0: On Linux, use setsockopt to properly set socket level keepalive. - # (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 120) - # (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 30) - # (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 8) - # TODO p4: Support setsockopt on lesser platforms than Linux. - files = request_data.multipart_data if request_data else None data = request_data.json_parameters if request_data else None diff --git a/tests/ext/test_applicationbuilder.py b/tests/ext/test_applicationbuilder.py index 0f9eb29ad..6f13b8178 100644 --- a/tests/ext/test_applicationbuilder.py +++ b/tests/ext/test_applicationbuilder.py @@ -91,6 +91,7 @@ class TestApplicationBuilder: limits: object http1: object http2: object + transport: object = None monkeypatch.setattr(httpx, "AsyncClient", Client) @@ -322,6 +323,7 @@ class TestApplicationBuilder: limits: object http1: object http2: object + transport: object = None monkeypatch.setattr(httpx, "AsyncClient", Client) diff --git a/tests/request/test_request.py b/tests/request/test_request.py index 9334f7b76..44c1dddbf 100644 --- a/tests/request/test_request.py +++ b/tests/request/test_request.py @@ -28,6 +28,7 @@ from typing import Any, Callable, Coroutine, Tuple import httpx import pytest +from httpx import AsyncHTTPTransport from telegram._utils.defaultvalue import DEFAULT_NONE from telegram.error import ( @@ -366,6 +367,7 @@ class TestHTTPXRequestWithoutRequest: limits: object http1: object http2: object + transport: object = None monkeypatch.setattr(httpx, "AsyncClient", Client) @@ -618,6 +620,24 @@ class TestHTTPXRequestWithoutRequest: assert exc_info.value.__cause__ is pool_timeout + async def test_socket_opts(self, monkeypatch): + transport_kwargs = {} + transport_init = AsyncHTTPTransport.__init__ + + def init_transport(*args, **kwargs): + nonlocal transport_kwargs + transport_kwargs = kwargs.copy() + transport_init(*args, **kwargs) + + monkeypatch.setattr(AsyncHTTPTransport, "__init__", init_transport) + + HTTPXRequest() + assert "socket_options" not in transport_kwargs + + transport_kwargs = {} + HTTPXRequest(socket_options=((1, 2, 3),)) + assert transport_kwargs["socket_options"] == ((1, 2, 3),) + @pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="No need to run this twice") class TestHTTPXRequestWithRequest: