Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-elastic-transport for
openSUSE:Factory checked in at 2026-07-02 20:05:52
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-elastic-transport (Old)
and /work/SRC/openSUSE:Factory/.python-elastic-transport.new.1982 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-elastic-transport"
Thu Jul 2 20:05:52 2026 rev:21 rq:1362531 version:9.4.2
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-elastic-transport/python-elastic-transport.changes
2026-03-31 15:49:28.044402501 +0200
+++
/work/SRC/openSUSE:Factory/.python-elastic-transport.new.1982/python-elastic-transport.changes
2026-07-02 20:06:07.626477504 +0200
@@ -1,0 +2,8 @@
+Mon Jun 29 20:33:56 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 9.4.2:
+ * Escape percent characters in logging output (#284)
+ * Optional backoff delays between retries
+ * Add support for an httpx synchronous node
+
+-------------------------------------------------------------------
Old:
----
elastic-transport-python-9.2.1.tar.gz
New:
----
elastic-transport-python-9.4.2.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-elastic-transport.spec ++++++
--- /var/tmp/diff_new_pack.VgJGBY/_old 2026-07-02 20:06:08.338502036 +0200
+++ /var/tmp/diff_new_pack.VgJGBY/_new 2026-07-02 20:06:08.338502036 +0200
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-elastic-transport
-Version: 9.2.1
+Version: 9.4.2
Release: 0
Summary: Transport classes and utilities shared among Python Elastic
client libraries
License: Apache-2.0
++++++ elastic-transport-python-9.2.1.tar.gz ->
elastic-transport-python-9.4.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/elastic-transport-python-9.2.1/CHANGELOG.md
new/elastic-transport-python-9.4.2/CHANGELOG.md
--- old/elastic-transport-python-9.2.1/CHANGELOG.md 2025-12-23
12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/CHANGELOG.md 2026-06-16
17:26:02.000000000 +0200
@@ -1,5 +1,18 @@
# Changelog
+## 9.4.2 (2026-06-16)
+
+* Escape percent characters in logging output
([#284](https://github.com/elastic/elastic-transport-python/pull/284)) (#286)
+
+## 9.4.1 (2026-05-25)
+
+* Optional backoff delays between retries
([#279](https://github.com/elastic/elastic-transport-python/pull/279))
+
+## 9.4.0 (2026-05-05)
+
+* Add support for an httpx synchronous node
([#262](https://github.com/elastic/elastic-transport-python/pull/262))
+
+
## 9.2.1 (2025-12-23)
* Use `path_prefix` to compute the base URL in the httpx node class
([#243](https://github.com/elastic/elastic-transport-python/pull/243))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/docs/sphinx/installation.rst
new/elastic-transport-python-9.4.2/docs/sphinx/installation.rst
--- old/elastic-transport-python-9.2.1/docs/sphinx/installation.rst
2025-12-23 12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/docs/sphinx/installation.rst
2026-06-16 17:26:02.000000000 +0200
@@ -11,4 +11,4 @@
Install the ``aiohttp`` package to use
:class:`elastic_transport.AiohttpHttpNode`.
-Install the ``httpx`` package to use
:class:`elastic_transport.HttpxAsyncHttpNode`.
+Install the ``httpx`` package to use :class:`elastic_transport.HttpxHttpNode`
or :class:`elastic_transport.HttpxAsyncHttpNode`.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/elastic-transport-python-9.2.1/docs/sphinx/nodes.rst
new/elastic-transport-python-9.4.2/docs/sphinx/nodes.rst
--- old/elastic-transport-python-9.2.1/docs/sphinx/nodes.rst 2025-12-23
12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/docs/sphinx/nodes.rst 2026-06-16
17:26:02.000000000 +0200
@@ -22,6 +22,9 @@
.. autoclass:: AiohttpHttpNode
:members:
+.. autoclass:: HttpxHttpNode
+ :members:
+
.. autoclass:: HttpxAsyncHttpNode
:members:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/elastic_transport/__init__.py
new/elastic-transport-python-9.4.2/elastic_transport/__init__.py
--- old/elastic-transport-python-9.2.1/elastic_transport/__init__.py
2025-12-23 12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/elastic_transport/__init__.py
2026-06-16 17:26:02.000000000 +0200
@@ -37,6 +37,7 @@
BaseAsyncNode,
BaseNode,
HttpxAsyncHttpNode,
+ HttpxHttpNode,
RequestsHttpNode,
Urllib3HttpNode,
)
@@ -74,6 +75,7 @@
"HeadApiResponse",
"HttpHeaders",
"HttpxAsyncHttpNode",
+ "HttpxHttpNode",
"JsonSerializer",
"ListApiResponse",
"NdjsonSerializer",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/elastic_transport/_async_transport.py
new/elastic-transport-python-9.4.2/elastic_transport/_async_transport.py
--- old/elastic-transport-python-9.2.1/elastic_transport/_async_transport.py
2025-12-23 12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/elastic_transport/_async_transport.py
2026-06-16 17:26:02.000000000 +0200
@@ -50,6 +50,7 @@
NOT_DEAD_NODE_HTTP_STATUSES,
Transport,
TransportApiResponse,
+ backoff_time,
validate_sniffing_options,
)
from .client_utils import resolve_default
@@ -79,6 +80,8 @@
max_retries: int = 3,
retry_on_status: Collection[int] = (429, 502, 503, 504),
retry_on_timeout: bool = False,
+ retry_backoff_base: float = 0,
+ retry_backoff_cap: float = 0,
sniff_on_start: bool = False,
sniff_before_requests: bool = False,
sniff_on_node_failure: bool = False,
@@ -114,6 +117,18 @@
on a different node. defaults to ``(429, 502, 503, 504)``
:arg retry_on_timeout: should timeout trigger a retry on different
node? (default ``False``)
+ :arg retry_backoff_base: the "base" argument for the full jitter
backoff
+ algorithm, in seconds. To enable backoff delays between retry
attempts,
+ set this argument to a value greater than 0. Note that
+ ``retry_backoff_base`` and ``retry_backoff_cap`` must both be
greater
+ than zero for backoff delays to be used. The default value for this
+ argument is 0.
+ :arg retry_backoff_cap: the "cap" argument for the full jitter backoff
+ algorithm, in seconds. To enable backoff delays between retry
attempts,
+ set this argument to a positive number that is greater or equal
than
+ ``retry_backoff_base``. Note that ``retry_backoff_base`` and
+ ``retry_backoff_cap`` must both be greater than zero for backoff
delays
+ to be used. The default value for this argument is 0.
:arg sniff_on_start: If ``True`` will sniff for additional nodes as
soon
as possible, guaranteed before the first request.
:arg sniff_on_node_failure: If ``True`` will sniff for additional
nodees
@@ -154,6 +169,8 @@
max_retries=max_retries,
retry_on_status=retry_on_status,
retry_on_timeout=retry_on_timeout,
+ retry_backoff_base=retry_backoff_base,
+ retry_backoff_cap=retry_backoff_cap,
sniff_timeout=sniff_timeout,
min_delay_between_sniffing=min_delay_between_sniffing,
meta_header=meta_header,
@@ -188,6 +205,8 @@
max_retries: Union[int, DefaultType] = DEFAULT,
retry_on_status: Union[Collection[int], DefaultType] = DEFAULT,
retry_on_timeout: Union[bool, DefaultType] = DEFAULT,
+ retry_backoff_base: Union[float, DefaultType] = DEFAULT,
+ retry_backoff_cap: Union[float, DefaultType] = DEFAULT,
request_timeout: Union[Optional[float], DefaultType] = DEFAULT,
client_meta: Union[Tuple[Tuple[str, str], ...], DefaultType] = DEFAULT,
otel_span: Union[OpenTelemetrySpan, DefaultType] = DEFAULT,
@@ -226,6 +245,10 @@
max_retries = resolve_default(max_retries, self.max_retries)
retry_on_timeout = resolve_default(retry_on_timeout,
self.retry_on_timeout)
retry_on_status = resolve_default(retry_on_status,
self.retry_on_status)
+ retry_backoff_base = resolve_default(
+ retry_backoff_base, self.retry_backoff_base
+ )
+ retry_backoff_cap = resolve_default(retry_backoff_cap,
self.retry_backoff_cap)
otel_span = resolve_default(otel_span, OpenTelemetrySpan(None))
if self.meta_header:
@@ -340,6 +363,15 @@
e.errors = tuple(errors)
raise
else:
+ sleep_time = backoff_time(
+ attempt, retry_backoff_base, retry_backoff_cap
+ )
+ if sleep_time:
+ _logger.warning(
+ "Request failure, sleeping for %.1fs before
retrying",
+ sleep_time,
+ )
+ await asyncio.sleep(sleep_time)
_logger.warning(
"Retrying request after failure (attempt %d of %d)",
attempt,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/elastic_transport/_node/__init__.py
new/elastic-transport-python-9.4.2/elastic_transport/_node/__init__.py
--- old/elastic-transport-python-9.2.1/elastic_transport/_node/__init__.py
2025-12-23 12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/elastic_transport/_node/__init__.py
2026-06-16 17:26:02.000000000 +0200
@@ -18,7 +18,7 @@
from ._base import BaseNode, NodeApiResponse
from ._base_async import BaseAsyncNode
from ._http_aiohttp import AiohttpHttpNode
-from ._http_httpx import HttpxAsyncHttpNode
+from ._http_httpx import HttpxAsyncHttpNode, HttpxHttpNode
from ._http_requests import RequestsHttpNode
from ._http_urllib3 import Urllib3HttpNode
@@ -29,5 +29,6 @@
"NodeApiResponse",
"RequestsHttpNode",
"Urllib3HttpNode",
+ "HttpxHttpNode",
"HttpxAsyncHttpNode",
]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/elastic_transport/_node/_base.py
new/elastic-transport-python-9.4.2/elastic_transport/_node/_base.py
--- old/elastic-transport-python-9.2.1/elastic_transport/_node/_base.py
2025-12-23 12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/elastic_transport/_node/_base.py
2026-06-16 17:26:02.000000000 +0200
@@ -240,6 +240,9 @@
log_args.extend((http_version, meta.status))
if meta.headers:
for header, value in sorted(meta.headers.items()):
+ # escape any % characters in the value, to avoid them
being
+ # misinterpreted as a logging template placeholder
+ value = value.replace("%", "%%")
lines.append(f"< {header.title()}: {value}")
if response:
try:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/elastic_transport/_node/_http_httpx.py
new/elastic-transport-python-9.4.2/elastic_transport/_node/_http_httpx.py
--- old/elastic-transport-python-9.2.1/elastic_transport/_node/_http_httpx.py
2025-12-23 12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/elastic_transport/_node/_http_httpx.py
2026-06-16 17:26:02.000000000 +0200
@@ -30,6 +30,7 @@
BUILTIN_EXCEPTIONS,
DEFAULT_CA_CERTS,
RERAISE_EXCEPTIONS,
+ BaseNode,
NodeApiResponse,
ssl_context_from_node_config,
)
@@ -45,6 +46,165 @@
_HTTPX_META_VERSION = ""
+class HttpxHttpNode(BaseNode):
+ """
+ HTTP node using httpx.
+ """
+
+ _CLIENT_META_HTTP_CLIENT = ("hx", _HTTPX_META_VERSION)
+
+ def __init__(self, config: NodeConfig):
+ if not _HTTPX_AVAILABLE: # pragma: nocover
+ raise ValueError("You must have 'httpx' installed to use
HttpxNode")
+ super().__init__(config)
+
+ if config.ssl_assert_fingerprint:
+ raise ValueError(
+ "httpx does not support certificate pinning.
https://github.com/encode/httpx/issues/761"
+ )
+
+ ssl_context: Union[ssl.SSLContext, Literal[False]] = False
+ if config.scheme == "https":
+ if config.ssl_context is not None:
+ ssl_context = ssl_context_from_node_config(config)
+ else:
+ ssl_context = ssl_context_from_node_config(config)
+
+ ca_certs = (
+ DEFAULT_CA_CERTS if config.ca_certs is None else
config.ca_certs
+ )
+ if config.verify_certs:
+ if not ca_certs:
+ raise ValueError(
+ "Root certificates are missing for certificate "
+ "validation. Either pass them in using the
ca_certs parameter or "
+ "install certifi to use it automatically."
+ )
+ else:
+ if config.ssl_show_warn:
+ warnings.warn(
+ f"Connecting to {self.base_url!r} using TLS with
verify_certs=False is insecure",
+ stacklevel=warn_stacklevel(),
+ category=SecurityWarning,
+ )
+
+ if ca_certs is not None:
+ if os.path.isfile(ca_certs):
+ ssl_context.load_verify_locations(cafile=ca_certs)
+ elif os.path.isdir(ca_certs):
+ ssl_context.load_verify_locations(capath=ca_certs)
+ else:
+ raise ValueError("ca_certs parameter is not a path")
+
+ # Use client_cert and client_key variables for SSL certificate
configuration.
+ if config.client_cert and not
os.path.isfile(config.client_cert):
+ raise ValueError("client_cert is not a path to a file")
+ if config.client_key and not os.path.isfile(config.client_key):
+ raise ValueError("client_key is not a path to a file")
+ if config.client_cert and config.client_key:
+ ssl_context.load_cert_chain(config.client_cert,
config.client_key)
+ elif config.client_cert:
+ ssl_context.load_cert_chain(config.client_cert)
+
+ self.client = httpx.Client(
+
base_url=f"{config.scheme}://{config.host}:{config.port}{config.path_prefix}",
+ limits=httpx.Limits(max_connections=config.connections_per_node),
+ verify=ssl_context or False,
+ timeout=config.request_timeout,
+ )
+
+ def perform_request(
+ self,
+ method: str,
+ target: str,
+ body: Optional[bytes] = None,
+ headers: Optional[HttpHeaders] = None,
+ request_timeout: Union[DefaultType, Optional[float]] = DEFAULT,
+ ) -> NodeApiResponse:
+ resolved_headers = self._headers.copy()
+ if headers:
+ resolved_headers.update(headers)
+
+ if body:
+ if self._http_compress:
+ resolved_body = gzip.compress(body)
+ resolved_headers["content-encoding"] = "gzip"
+ else:
+ resolved_body = body
+ else:
+ resolved_body = None
+
+ try:
+ start = time.perf_counter()
+ if request_timeout is DEFAULT:
+ resp = self.client.request(
+ method,
+ target,
+ content=resolved_body,
+ headers=dict(resolved_headers),
+ )
+ else:
+ resp = self.client.request(
+ method,
+ target,
+ content=resolved_body,
+ headers=dict(resolved_headers),
+ timeout=request_timeout,
+ )
+ response_body = resp.read()
+ duration = time.perf_counter() - start
+ except RERAISE_EXCEPTIONS + BUILTIN_EXCEPTIONS:
+ raise
+ except Exception as e:
+ err: Exception
+ if isinstance(e, (TimeoutError, httpx.TimeoutException)):
+ err = ConnectionTimeout(
+ "Connection timed out during request", errors=(e,)
+ )
+ elif isinstance(e, ssl.SSLError):
+ err = TlsError(str(e), errors=(e,))
+ # Detect SSL errors for httpx v0.28.0+
+ # Needed until https://github.com/encode/httpx/issues/3350 is fixed
+ elif isinstance(e, httpx.ConnectError) and e.__cause__:
+ context = e.__cause__.__context__
+ if isinstance(context, ssl.SSLError):
+ err = TlsError(str(context), errors=(e,))
+ else:
+ err = ConnectionError(str(e), errors=(e,))
+ else:
+ err = ConnectionError(str(e), errors=(e,))
+ self._log_request(
+ method=method,
+ target=target,
+ headers=resolved_headers,
+ body=body,
+ exception=err,
+ )
+ raise err from e
+
+ meta = ApiResponseMeta(
+ resp.status_code,
+ resp.http_version.lstrip("HTTP/"),
+ HttpHeaders(resp.headers),
+ duration,
+ self.config,
+ )
+
+ self._log_request(
+ method=method,
+ target=target,
+ headers=resolved_headers,
+ body=body,
+ meta=meta,
+ response=response_body,
+ )
+
+ return NodeApiResponse(meta, response_body)
+
+ def close(self) -> None:
+ self.client.close()
+
+
class HttpxAsyncHttpNode(BaseAsyncNode):
"""
Async HTTP node using httpx. Supports both Trio and asyncio.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/elastic_transport/_node/_http_requests.py
new/elastic-transport-python-9.4.2/elastic_transport/_node/_http_requests.py
---
old/elastic-transport-python-9.2.1/elastic_transport/_node/_http_requests.py
2025-12-23 12:43:38.000000000 +0100
+++
new/elastic-transport-python-9.4.2/elastic_transport/_node/_http_requests.py
2026-06-16 17:26:02.000000000 +0200
@@ -81,7 +81,7 @@
pool_kwargs["cert_reqs"] = "CERT_NONE"
pool_kwargs["assert_hostname"] = False
- super().init_poolmanager(connections, maxsize, block=block,
**pool_kwargs) # type: ignore [no-untyped-call]
+ super().init_poolmanager(connections, maxsize, block=block,
**pool_kwargs)
self.poolmanager.pool_classes_by_scheme["https"] =
HTTPSConnectionPool
except ImportError: # pragma: nocover
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/elastic_transport/_node/_http_urllib3.py
new/elastic-transport-python-9.4.2/elastic_transport/_node/_http_urllib3.py
--- old/elastic-transport-python-9.2.1/elastic_transport/_node/_http_urllib3.py
2025-12-23 12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/elastic_transport/_node/_http_urllib3.py
2026-06-16 17:26:02.000000000 +0200
@@ -24,7 +24,7 @@
try:
from importlib import metadata
except ImportError:
- import importlib_metadata as metadata # type: ignore[no-redef]
+ import importlib_metadata as metadata # type: ignore[no-redef,
import-not-found]
import urllib3
from urllib3.exceptions import ConnectTimeoutError, NewConnectionError,
ReadTimeoutError
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/elastic_transport/_transport.py
new/elastic-transport-python-9.4.2/elastic_transport/_transport.py
--- old/elastic-transport-python-9.2.1/elastic_transport/_transport.py
2025-12-23 12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/elastic_transport/_transport.py
2026-06-16 17:26:02.000000000 +0200
@@ -18,6 +18,7 @@
import dataclasses
import inspect
import logging
+import random
import time
import warnings
from platform import python_version
@@ -56,6 +57,7 @@
AiohttpHttpNode,
BaseNode,
HttpxAsyncHttpNode,
+ HttpxHttpNode,
RequestsHttpNode,
Urllib3HttpNode,
)
@@ -70,6 +72,7 @@
"urllib3": Urllib3HttpNode,
"requests": RequestsHttpNode,
"aiohttp": AiohttpHttpNode,
+ "httpx": HttpxHttpNode,
"httpxasync": HttpxAsyncHttpNode,
}
# These are HTTP status errors that shouldn't be considered
@@ -87,6 +90,12 @@
body: Any
+def backoff_time(attempts: int, base: float = 1, cap: float = 60) -> float:
+ # Equal Jitter from
https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
+ temp: float = min(cap, base * 2 ** (attempts - 1)) / 2
+ return temp + random.uniform(0, temp)
+
+
class Transport:
"""
Encapsulation of transport-related to logic. Handles instantiation of the
@@ -109,6 +118,8 @@
max_retries: int = 3,
retry_on_status: Collection[int] = (429, 502, 503, 504),
retry_on_timeout: bool = False,
+ retry_backoff_base: float = 0,
+ retry_backoff_cap: float = 0,
sniff_on_start: bool = False,
sniff_before_requests: bool = False,
sniff_on_node_failure: bool = False,
@@ -144,6 +155,18 @@
on a different node. defaults to ``(429, 502, 503, 504)``
:arg retry_on_timeout: should timeout trigger a retry on different
node? (default ``False``)
+ :arg retry_backoff_base: the "base" argument for the full jitter
backoff
+ algorithm, in seconds. To enable backoff delays between retry
attempts,
+ set this argument to a value greater than 0. Note that
+ ``retry_backoff_base`` and ``retry_backoff_cap`` must both be
greater
+ than zero for backoff delays to be used. The default value for this
+ argument is 0.
+ :arg retry_backoff_cap: the "cap" argument for the full jitter backoff
+ algorithm, in seconds. To enable backoff delays between retry
attempts,
+ set this argument to a positive number that is greater or equal
than
+ ``retry_backoff_base``. Note that ``retry_backoff_base`` and
+ ``retry_backoff_cap`` must both be greater than zero for backoff
delays
+ to be used. The default value for this argument is 0.
:arg sniff_on_start: If ``True`` will sniff for additional nodes as
soon
as possible, guaranteed before the first request.
:arg sniff_on_node_failure: If ``True`` will sniff for additional
nodees
@@ -225,6 +248,8 @@
self.max_retries = max_retries
self.retry_on_status = retry_on_status
self.retry_on_timeout = retry_on_timeout
+ self.retry_backoff_base = retry_backoff_base
+ self.retry_backoff_cap = retry_backoff_cap
# Build the NodePool from all the options
node_pool_kwargs: Dict[str, Any] = {}
@@ -263,6 +288,8 @@
max_retries: Union[int, DefaultType] = DEFAULT,
retry_on_status: Union[Collection[int], DefaultType] = DEFAULT,
retry_on_timeout: Union[bool, DefaultType] = DEFAULT,
+ retry_backoff_base: Union[float, DefaultType] = DEFAULT,
+ retry_backoff_cap: Union[float, DefaultType] = DEFAULT,
request_timeout: Union[Optional[float], DefaultType] = DEFAULT,
client_meta: Union[Tuple[Tuple[str, str], ...], DefaultType] = DEFAULT,
otel_span: Union[OpenTelemetrySpan, DefaultType] = DEFAULT,
@@ -300,6 +327,10 @@
max_retries = resolve_default(max_retries, self.max_retries)
retry_on_timeout = resolve_default(retry_on_timeout,
self.retry_on_timeout)
retry_on_status = resolve_default(retry_on_status,
self.retry_on_status)
+ retry_backoff_base = resolve_default(
+ retry_backoff_base, self.retry_backoff_base
+ )
+ retry_backoff_cap = resolve_default(retry_backoff_cap,
self.retry_backoff_cap)
otel_span = resolve_default(otel_span, OpenTelemetrySpan(None))
if self.meta_header:
@@ -414,6 +445,15 @@
e.errors = tuple(errors)
raise
else:
+ sleep_time = backoff_time(
+ attempt, retry_backoff_base, retry_backoff_cap
+ )
+ if sleep_time:
+ _logger.warning(
+ "Request failure, sleeping for %.1fs before
retrying",
+ sleep_time,
+ )
+ time.sleep(sleep_time)
_logger.warning(
"Retrying request after failure (attempt %d of %d)",
attempt,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/elastic_transport/_version.py
new/elastic-transport-python-9.4.2/elastic_transport/_version.py
--- old/elastic-transport-python-9.2.1/elastic_transport/_version.py
2025-12-23 12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/elastic_transport/_version.py
2026-06-16 17:26:02.000000000 +0200
@@ -15,4 +15,4 @@
# specific language governing permissions and limitations
# under the License.
-__version__ = "9.2.1"
+__version__ = "9.4.2"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/elastic_transport/client_utils.py
new/elastic-transport-python-9.4.2/elastic_transport/client_utils.py
--- old/elastic-transport-python-9.2.1/elastic_transport/client_utils.py
2025-12-23 12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/elastic_transport/client_utils.py
2026-06-16 17:26:02.000000000 +0200
@@ -150,12 +150,6 @@
return value
-# Python 3.7 added '~' to the safe list for urllib.parse.quote()
-_QUOTE_ALWAYS_SAFE = frozenset(
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-~"
-)
-
-
def percent_encode(
string: Union[bytes, str],
safe: str = "/",
@@ -163,9 +157,8 @@
errors: Optional[str] = None,
) -> str:
"""Percent-encodes a string so it can be used in an HTTP request target"""
- # Redefines 'urllib.parse.quote()' to always have the '~' character
- # within the 'ALWAYS_SAFE' list. The character was added in Python 3.7
- safe = "".join(_QUOTE_ALWAYS_SAFE.union(set(safe)))
+ # This function used to add `~` to unreserverd characters, but this was
fixed in Python 3.7.
+ # Keeping the function here as it is part of the public API.
return _quote(string, safe, encoding=encoding, errors=errors) # type:
ignore[arg-type]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/elastic-transport-python-9.2.1/requirements-min.txt
new/elastic-transport-python-9.4.2/requirements-min.txt
--- old/elastic-transport-python-9.2.1/requirements-min.txt 2025-12-23
12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/requirements-min.txt 2026-06-16
17:26:02.000000000 +0200
@@ -1,4 +1,4 @@
-requests==2.26.0
-urllib3==1.26.2
+requests==2.30.0
+urllib3==2.0.3
aiohttp==3.8.0
httpx==0.27.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/elastic-transport-python-9.2.1/setup.py
new/elastic-transport-python-9.4.2/setup.py
--- old/elastic-transport-python-9.2.1/setup.py 2025-12-23 12:43:38.000000000
+0100
+++ new/elastic-transport-python-9.4.2/setup.py 2026-06-16 17:26:02.000000000
+0200
@@ -50,7 +50,7 @@
package_data={"elastic_transport": ["py.typed"]},
packages=packages,
install_requires=[
- "urllib3>=1.26.2, <3",
+ "urllib3>=2, <3",
"certifi",
"sniffio",
],
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/tests/async_/test_async_transport.py
new/elastic-transport-python-9.4.2/tests/async_/test_async_transport.py
--- old/elastic-transport-python-9.2.1/tests/async_/test_async_transport.py
2025-12-23 12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/tests/async_/test_async_transport.py
2026-06-16 17:26:02.000000000 +0200
@@ -240,6 +240,34 @@
@pytest.mark.anyio
+async def test_request_retry_backoff():
+ t = AsyncTransport(
+ [
+ NodeConfig(
+ "http",
+ "localhost",
+ 80,
+ _extras={"exception": ConnectionError("abandon ship")},
+ )
+ ],
+ node_class=AsyncDummyNode,
+ retry_backoff_base=1,
+ retry_backoff_cap=2,
+ )
+
+ with mock.patch("elastic_transport._async_transport.asyncio.sleep") as
mock_sleep:
+ with pytest.raises(ConnectionError) as e:
+ await t.perform_request("GET", "/")
+
+ assert 4 == len(t.node_pool.get().calls)
+ assert len(e.value.errors) == 3
+ assert all(isinstance(error, ConnectionError) for error in e.value.errors)
+
+ assert mock_sleep.await_count == 3
+ assert all(0 < arg[0][0] <= 2 for arg in mock_sleep.await_args_list)
+
+
[email protected]
async def test_failed_connection_will_be_marked_as_dead():
t = AsyncTransport(
[
@@ -322,7 +350,7 @@
AsyncTransport([NodeConfig("http", "localhost", 80)],
node_class="huh?")
assert str(e.value) == (
"Unknown option for node_class: 'huh?'. "
- "Available options are: 'aiohttp', 'httpxasync', 'requests', 'urllib3'"
+ "Available options are: 'aiohttp', 'httpx', 'httpxasync', 'requests',
'urllib3'"
)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/tests/node/test_base.py
new/elastic-transport-python-9.4.2/tests/node/test_base.py
--- old/elastic-transport-python-9.2.1/tests/node/test_base.py 2025-12-23
12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/tests/node/test_base.py 2026-06-16
17:26:02.000000000 +0200
@@ -20,6 +20,7 @@
from elastic_transport import (
AiohttpHttpNode,
HttpxAsyncHttpNode,
+ HttpxHttpNode,
NodeConfig,
RequestsHttpNode,
Urllib3HttpNode,
@@ -28,7 +29,14 @@
@pytest.mark.parametrize(
- "node_cls", [Urllib3HttpNode, RequestsHttpNode, AiohttpHttpNode,
HttpxAsyncHttpNode]
+ "node_cls",
+ [
+ Urllib3HttpNode,
+ RequestsHttpNode,
+ AiohttpHttpNode,
+ HttpxHttpNode,
+ HttpxAsyncHttpNode,
+ ],
)
def test_unknown_parameter(node_cls):
with pytest.raises(TypeError):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/tests/node/test_http_httpx.py
new/elastic-transport-python-9.4.2/tests/node/test_http_httpx.py
--- old/elastic-transport-python-9.2.1/tests/node/test_http_httpx.py
2025-12-23 12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/tests/node/test_http_httpx.py
2026-06-16 17:26:02.000000000 +0200
@@ -22,11 +22,141 @@
import pytest
import respx
-from elastic_transport import HttpxAsyncHttpNode, NodeConfig
+from elastic_transport import HttpxAsyncHttpNode, HttpxHttpNode, NodeConfig
from elastic_transport._node._base import DEFAULT_USER_AGENT
-def create_node(node_config: NodeConfig):
+def create_sync_node(node_config: NodeConfig):
+ return HttpxHttpNode(node_config)
+
+
+class TestHttpxNodeCreation:
+ def test_ssl_context(self):
+ ssl_context = ssl.create_default_context()
+ with warnings.catch_warnings(record=True) as w:
+ node = create_sync_node(
+ NodeConfig(
+ scheme="https",
+ host="localhost",
+ port=80,
+ ssl_context=ssl_context,
+ )
+ )
+ assert node.client._transport._pool._ssl_context is ssl_context
+ assert len(w) == 0
+
+ def test_uses_https_if_verify_certs_is_off(self):
+ with warnings.catch_warnings(record=True) as w:
+ _ = create_sync_node(
+ NodeConfig("https", "localhost", 443, verify_certs=False)
+ )
+ assert (
+ str(w[0].message)
+ == "Connecting to 'https://localhost:443' using TLS with
verify_certs=False is insecure"
+ )
+
+ def test_no_warn_when_uses_https_if_verify_certs_is_off(self):
+ with warnings.catch_warnings(record=True) as w:
+ _ = create_sync_node(
+ NodeConfig(
+ "https",
+ "localhost",
+ 443,
+ verify_certs=False,
+ ssl_show_warn=False,
+ )
+ )
+ assert 0 == len(w)
+
+ def test_ca_certs_with_verify_ssl_false_raises_error(self):
+ with pytest.raises(ValueError) as exc:
+ create_sync_node(
+ NodeConfig(
+ "https",
+ "localhost",
+ 443,
+ ca_certs="/ca/certs",
+ verify_certs=False,
+ )
+ )
+ assert (
+ str(exc.value) == "You cannot use 'ca_certs' when
'verify_certs=False'"
+ )
+
+ def test_path_prefix(self):
+ node = create_sync_node(
+ NodeConfig(
+ "http",
+ "localhost",
+ 9200,
+ path_prefix="/test",
+ )
+ )
+ assert node.base_url == "http://localhost:9200/test"
+ assert node.client.base_url == "http://localhost:9200/test/"
+
+
+class TestHttpxNode:
+ @respx.mock
+ def test_simple_request(self):
+ node = create_sync_node(NodeConfig(scheme="http", host="localhost",
port=80))
+ respx.get("http://localhost/index")
+ node.perform_request("GET", "/index", b"hello world", headers={"key":
"value"})
+ request = respx.calls.last.request
+ assert request.content == b"hello world"
+ assert {
+ "key": "value",
+ "connection": "keep-alive",
+ "user-agent": DEFAULT_USER_AGENT,
+ }.items() <= request.headers.items()
+
+ @respx.mock
+ def test_compression(self):
+ node = create_sync_node(
+ NodeConfig(scheme="http", host="localhost", port=80,
http_compress=True)
+ )
+ respx.get("http://localhost/index")
+ node.perform_request("GET", "/index", b"hello world")
+ request = respx.calls.last.request
+ assert gzip.decompress(request.content) == b"hello world"
+ assert {"content-encoding": "gzip"}.items() <= request.headers.items()
+
+ @respx.mock
+ def test_default_timeout(self):
+ node = create_sync_node(
+ NodeConfig(scheme="http", host="localhost", port=80,
request_timeout=10)
+ )
+ respx.get("http://localhost/index")
+ node.perform_request("GET", "/index", b"hello world")
+ request = respx.calls.last.request
+ assert request.extensions["timeout"]["connect"] == 10
+
+ @respx.mock
+ def test_overwritten_timeout(self):
+ node = create_sync_node(
+ NodeConfig(scheme="http", host="localhost", port=80,
request_timeout=10)
+ )
+ respx.get("http://localhost/index")
+ node.perform_request("GET", "/index", b"hello world",
request_timeout=15)
+ request = respx.calls.last.request
+ assert request.extensions["timeout"]["connect"] == 15
+
+ @respx.mock
+ def test_merge_headers(self):
+ node = create_sync_node(
+ NodeConfig("http", "localhost", 80, headers={"h1": "v1", "h2":
"v2"})
+ )
+ respx.get("http://localhost/index")
+ node.perform_request(
+ "GET", "/index", b"hello world", headers={"h2": "v2p", "h3": "v3"}
+ )
+ request = respx.calls.last.request
+ assert request.headers["h1"] == "v1"
+ assert request.headers["h2"] == "v2p"
+ assert request.headers["h3"] == "v3"
+
+
+def create_async_node(node_config: NodeConfig):
return HttpxAsyncHttpNode(node_config)
@@ -34,7 +164,7 @@
def test_ssl_context(self):
ssl_context = ssl.create_default_context()
with warnings.catch_warnings(record=True) as w:
- node = create_node(
+ node = create_async_node(
NodeConfig(
scheme="https",
host="localhost",
@@ -47,7 +177,9 @@
def test_uses_https_if_verify_certs_is_off(self):
with warnings.catch_warnings(record=True) as w:
- _ = create_node(NodeConfig("https", "localhost", 443,
verify_certs=False))
+ _ = create_async_node(
+ NodeConfig("https", "localhost", 443, verify_certs=False)
+ )
assert (
str(w[0].message)
== "Connecting to 'https://localhost:443' using TLS with
verify_certs=False is insecure"
@@ -55,7 +187,7 @@
def test_no_warn_when_uses_https_if_verify_certs_is_off(self):
with warnings.catch_warnings(record=True) as w:
- _ = create_node(
+ _ = create_async_node(
NodeConfig(
"https",
"localhost",
@@ -68,7 +200,7 @@
def test_ca_certs_with_verify_ssl_false_raises_error(self):
with pytest.raises(ValueError) as exc:
- create_node(
+ create_async_node(
NodeConfig(
"https",
"localhost",
@@ -82,7 +214,7 @@
)
def test_path_prefix(self):
- node = create_node(
+ node = create_async_node(
NodeConfig(
"http",
"localhost",
@@ -98,7 +230,7 @@
class TestHttpxAsyncNode:
@respx.mock
async def test_simple_request(self):
- node = create_node(NodeConfig(scheme="http", host="localhost",
port=80))
+ node = create_async_node(NodeConfig(scheme="http", host="localhost",
port=80))
respx.get("http://localhost/index")
await node.perform_request(
"GET", "/index", b"hello world", headers={"key": "value"}
@@ -113,7 +245,7 @@
@respx.mock
async def test_compression(self):
- node = create_node(
+ node = create_async_node(
NodeConfig(scheme="http", host="localhost", port=80,
http_compress=True)
)
respx.get("http://localhost/index")
@@ -124,7 +256,7 @@
@respx.mock
async def test_default_timeout(self):
- node = create_node(
+ node = create_async_node(
NodeConfig(scheme="http", host="localhost", port=80,
request_timeout=10)
)
respx.get("http://localhost/index")
@@ -134,7 +266,7 @@
@respx.mock
async def test_overwritten_timeout(self):
- node = create_node(
+ node = create_async_node(
NodeConfig(scheme="http", host="localhost", port=80,
request_timeout=10)
)
respx.get("http://localhost/index")
@@ -144,7 +276,7 @@
@respx.mock
async def test_merge_headers(self):
- node = create_node(
+ node = create_async_node(
NodeConfig("http", "localhost", 80, headers={"h1": "v1", "h2":
"v2"})
)
respx.get("http://localhost/index")
@@ -157,9 +289,10 @@
assert request.headers["h3"] == "v3"
-def test_ssl_assert_fingerprint(cert_fingerprint, httpbin_secure):
[email protected]("node_class", [HttpxHttpNode, HttpxAsyncHttpNode])
+def test_ssl_assert_fingerprint(node_class, cert_fingerprint, httpbin_secure):
with pytest.raises(ValueError, match="httpx does not support certificate
pinning"):
- HttpxAsyncHttpNode(
+ node_class(
NodeConfig(
scheme="https",
host=httpbin_secure.host,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/tests/node/test_tls_versions.py
new/elastic-transport-python-9.4.2/tests/node/test_tls_versions.py
--- old/elastic-transport-python-9.2.1/tests/node/test_tls_versions.py
2025-12-23 12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/tests/node/test_tls_versions.py
2026-06-16 17:26:02.000000000 +0200
@@ -25,6 +25,7 @@
AiohttpHttpNode,
ConnectionError,
HttpxAsyncHttpNode,
+ HttpxHttpNode,
NodeConfig,
RequestsHttpNode,
TlsError,
@@ -39,7 +40,13 @@
node_classes = pytest.mark.parametrize(
"node_class",
- [AiohttpHttpNode, Urllib3HttpNode, RequestsHttpNode, HttpxAsyncHttpNode],
+ [
+ AiohttpHttpNode,
+ Urllib3HttpNode,
+ RequestsHttpNode,
+ HttpxHttpNode,
+ HttpxAsyncHttpNode,
+ ],
)
supported_version_params = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/elastic-transport-python-9.2.1/tests/test_httpbin.py
new/elastic-transport-python-9.4.2/tests/test_httpbin.py
--- old/elastic-transport-python-9.2.1/tests/test_httpbin.py 2025-12-23
12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/tests/test_httpbin.py 2026-06-16
17:26:02.000000000 +0200
@@ -25,7 +25,7 @@
from elastic_transport._transport import NODE_CLASS_NAMES
[email protected]("node_class", ["urllib3", "requests"])
[email protected]("node_class", ["urllib3", "requests", "httpx"])
def test_simple_request(node_class, httpbin_node_config, httpbin):
t = Transport([httpbin_node_config], node_class=node_class)
@@ -58,7 +58,7 @@
assert all(v == data["headers"][k] for k, v in request_headers.items())
[email protected]("node_class", ["urllib3", "requests"])
[email protected]("node_class", ["urllib3", "requests", "httpx"])
def test_node(node_class, httpbin_node_config, httpbin):
def new_node(**kwargs):
return NODE_CLASS_NAMES[node_class](
@@ -70,8 +70,12 @@
assert resp.status == 200
parsed = parse_httpbin(data)
assert parsed == {
- "headers": {
- "Accept-Encoding": "identity",
+ "headers": (
+ {"Accept": "*/*", "Accept-Encoding": "gzip, deflate, br"}
+ if node_class == "httpx"
+ else {"Accept-Encoding": "identity"}
+ )
+ | {
"Connection": "keep-alive",
"Host": f"{httpbin.host}:{httpbin.port}",
"User-Agent": DEFAULT_USER_AGENT,
@@ -85,7 +89,8 @@
assert resp.status == 200
parsed = parse_httpbin(data)
assert parsed == {
- "headers": {
+ "headers": ({"Accept": "*/*"} if node_class == "httpx" else {})
+ | {
"Accept-Encoding": "gzip",
"Connection": "keep-alive",
"Host": f"{httpbin.host}:{httpbin.port}",
@@ -99,7 +104,8 @@
assert resp.status == 200
parsed = parse_httpbin(data)
assert parsed == {
- "headers": {
+ "headers": ({"Accept": "*/*"} if node_class == "httpx" else {})
+ | {
"Accept-Encoding": "gzip",
"Content-Encoding": "gzip",
"Content-Length": "33",
@@ -120,7 +126,8 @@
assert resp.status == 200
parsed = parse_httpbin(data)
assert parsed == {
- "headers": {
+ "headers": ({"Accept": "*/*"} if node_class == "httpx" else {})
+ | {
"Accept-Encoding": "gzip",
"Content-Encoding": "gzip",
"Content-Length": "36",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/tests/test_httpserver.py
new/elastic-transport-python-9.4.2/tests/test_httpserver.py
--- old/elastic-transport-python-9.2.1/tests/test_httpserver.py 2025-12-23
12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/tests/test_httpserver.py 2026-06-16
17:26:02.000000000 +0200
@@ -18,27 +18,14 @@
import warnings
import pytest
-import requests
-import urllib3
from elastic_transport import Transport
@pytest.mark.parametrize("node_class", ["urllib3", "requests"])
def test_simple_request(node_class, https_server_ip_node_config):
- # when testing minimum urllib3 and requests dependencies, we disable
- # the deprecation warning for ssl.match_hostname()
- silence_ssl_deprecation = (
- node_class == "urllib3" and urllib3.__version__ == "1.26.2"
- ) or (node_class == "requests" and requests.__version__ == "2.26.0")
-
with warnings.catch_warnings():
warnings.simplefilter("error")
- if silence_ssl_deprecation:
- warnings.filterwarnings(
- "ignore", ".*match_hostname.*deprecated", DeprecationWarning
- )
-
t = Transport([https_server_ip_node_config], node_class=node_class)
resp, data = t.perform_request("GET", "/foobar")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/elastic-transport-python-9.2.1/tests/test_logging.py
new/elastic-transport-python-9.4.2/tests/test_logging.py
--- old/elastic-transport-python-9.2.1/tests/test_logging.py 2025-12-23
12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/tests/test_logging.py 2026-06-16
17:26:02.000000000 +0200
@@ -25,6 +25,7 @@
ConnectionError,
HttpHeaders,
HttpxAsyncHttpNode,
+ HttpxHttpNode,
RequestsHttpNode,
Urllib3HttpNode,
debug_logging,
@@ -34,7 +35,13 @@
node_class = pytest.mark.parametrize(
"node_class",
- [Urllib3HttpNode, RequestsHttpNode, AiohttpHttpNode, HttpxAsyncHttpNode],
+ [
+ Urllib3HttpNode,
+ RequestsHttpNode,
+ AiohttpHttpNode,
+ HttpxAsyncHttpNode,
+ HttpxHttpNode,
+ ],
)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/elastic-transport-python-9.2.1/tests/test_transport.py
new/elastic-transport-python-9.4.2/tests/test_transport.py
--- old/elastic-transport-python-9.2.1/tests/test_transport.py 2025-12-23
12:43:38.000000000 +0100
+++ new/elastic-transport-python-9.4.2/tests/test_transport.py 2026-06-16
17:26:02.000000000 +0200
@@ -29,6 +29,7 @@
AiohttpHttpNode,
ConnectionError,
ConnectionTimeout,
+ HttpxHttpNode,
NodeConfig,
RequestsHttpNode,
SniffingError,
@@ -38,6 +39,7 @@
TransportWarning,
Urllib3HttpNode,
)
+from elastic_transport._transport import backoff_time
from elastic_transport.client_utils import DEFAULT
from tests.conftest import DummyNode
@@ -249,6 +251,34 @@
]
+def test_request_retry_backoff():
+ t = Transport(
+ [
+ NodeConfig(
+ "http",
+ "localhost",
+ 80,
+ _extras={"exception": ConnectionError("abandon ship")},
+ )
+ ],
+ node_class=DummyNode,
+ max_retries=3,
+ retry_backoff_base=1,
+ retry_backoff_cap=2,
+ )
+
+ with mock.patch("elastic_transport._transport.time.sleep") as mock_sleep:
+ with pytest.raises(ConnectionError) as e:
+ t.perform_request("GET", "/")
+
+ assert 4 == len(t.node_pool.get().calls)
+ assert len(e.value.errors) == 3
+ assert all(isinstance(error, ConnectionError) for error in e.value.errors)
+
+ assert mock_sleep.call_count == 3
+ assert all(0 < arg[0][0] <= 2 for arg in mock_sleep.call_args_list)
+
+
def test_failed_connection_will_be_marked_as_dead():
t = Transport(
[
@@ -329,7 +359,7 @@
Transport([NodeConfig("http", "localhost", 80)], node_class="huh?")
assert str(e.value) == (
"Unknown option for node_class: 'huh?'. "
- "Available options are: 'aiohttp', 'httpxasync', 'requests', 'urllib3'"
+ "Available options are: 'aiohttp', 'httpx', 'httpxasync', 'requests',
'urllib3'"
)
@@ -358,16 +388,16 @@
@pytest.mark.parametrize(
"node_class",
- ["urllib3", "requests", Urllib3HttpNode, RequestsHttpNode],
+ ["urllib3", "requests", "httpx", Urllib3HttpNode, RequestsHttpNode,
HttpxHttpNode],
)
def test_transport_client_meta_node_class(node_class):
t = Transport([NodeConfig("http", "localhost", 80)], node_class=node_class)
assert (
t._transport_client_meta[3] ==
t.node_pool.node_class._CLIENT_META_HTTP_CLIENT
)
- assert t._transport_client_meta[3][0] in ("ur", "rq")
+ assert t._transport_client_meta[3][0] in ("ur", "rq", "hx")
assert re.match(
- r"^et=[0-9.]+p?,py=[0-9.]+p?,t=[0-9.]+p?,(?:ur|rq)=[0-9.]+p?$",
+ r"^et=[0-9.]+p?,py=[0-9.]+p?,t=[0-9.]+p?,(?:ur|rq|hx)=[0-9.]+p?$",
",".join(f"{k}={v}" for k, v in t._transport_client_meta),
)
@@ -670,3 +700,23 @@
resp = t.perform_request("GET", "/anything")
assert resp.meta.status == 200
assert isinstance(resp.body, dict)
+
+
+def test_backoff_time():
+ for i in range(1, 11):
+ assert backoff_time(i, 0, 0) == 0
+ for i in range(1, 11):
+ assert backoff_time(i, 0, 1) == 0
+ exp = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
+ for i in range(10):
+ ceiling = min(1, 0.1 * exp[i])
+ assert ceiling / 2 <= backoff_time(i + 1, 0.1, 1) <= ceiling
+ for i in range(10):
+ ceiling = min(1, exp[i])
+ assert ceiling / 2 <= backoff_time(i + 1, 1, 1) <= ceiling
+ for i in range(10):
+ ceiling = min(100, exp[i])
+ assert ceiling / 2 <= backoff_time(i + 1, 1, 100) <= ceiling
+ for i in range(10):
+ ceiling = min(600, 60 * exp[i])
+ assert ceiling / 2 <= backoff_time(i + 1, 60, 600) <= ceiling