Skip to content

Commit

Permalink
Release 3.10 (#159)
Browse files Browse the repository at this point in the history
3.10.0 (2024-10-21)
------------------

**Added**
- Automatic Advanced Keep-Alive for HTTP/2 and HTTP/3 over QUIC by
sending PING frames.
New Session, and Adapter parameters are now available:
`keepalive_delay`, and `keepalive_idle_window`.
This greatly improves your daily experience working with HTTP/2+ remote
peers.

**Fixed**
- Unshielded picotls assertion error in Python < 3.10 when trying to
fetch the peer intermediate certificate. (#157)
  • Loading branch information
Ousret authored Oct 21, 2024
2 parents 409718b + 6d19929 commit 1933840
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 27 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ repos:
- id: mypy
args: [--check-untyped-defs]
exclude: 'tests/|noxfile.py'
additional_dependencies: ['charset_normalizer', 'urllib3.future>=2.10.903', 'wassima>=1.0.1', 'idna', 'kiss_headers']
additional_dependencies: ['charset_normalizer', 'urllib3.future>=2.11.900', 'wassima>=1.0.1', 'idna', 'kiss_headers']
11 changes: 11 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Release History
===============

3.10.0 (2024-10-21)
------------------

**Added**
- Automatic Advanced Keep-Alive for HTTP/2 and HTTP/3 over QUIC by sending PING frames.
New Session, and Adapter parameters are now available: `keepalive_delay`, and `keepalive_idle_window`.
This greatly improves your daily experience working with HTTP/2+ remote peers.

**Fixed**
- Unshielded picotls assertion error in Python < 3.10 when trying to fetch the peer intermediate certificate.

3.9.1 (2024-10-13)
------------------

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Niquests, is the “**Safest**, **Fastest[^10]**, **Easiest**, and **Most advanc
| `Early Responses` |||||
| `WebSocket over HTTP/1` ||[^14] |[^14] ||
| `WebSocket over HTTP/2 and HTTP/3` |[^13] ||||
| `Automatic Ping for HTTP/2+` || N/A || N/A |
| `Automatic Connection Upgrade / Downgrade` || N/A || N/A |
</details>

Expand Down
26 changes: 26 additions & 0 deletions docs/user/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,32 @@ See::

.. note:: The given example are really basic ones. You may adjust at will the settings and algorithm to match your requisites.

Keep-Alive
----------

.. note:: Available since Niquests v3.10 and before this only HTTP/1.1 were kept alive properly.

Niquests can automatically make sure that your HTTP connection is kept alive
no matter the used protocol using a discrete scheduled task for each host.

.. code-block:: python
import niquests
sess = niquests.Session(keepalive_delay=300, keepalive_idle_window=60) # already the defaults!, you don't need to specify anything
In that example, we indicate that we wish to keep a connection alive for 5 minutes and
eventually send ping every 60s after the connection was idle. (Those values are the default ones!)

The pings are only sent when using HTTP/2 or HTTP/3 over QUIC. Any connection activity is considered as used, therefor
making the ping only 60s after zero activity. If the connection receive unsolicited data, it is also considered used.

.. note:: Setting either keepalive_delay or keepalive_idle_window to None disable this feature.

.. warning:: We do not recommend setting anything lower than 30s for keepalive_idle_window. Anything lower than 1s is considered to be 1s. High frequency ping will lower the performance of your connection pool. And probably end up by getting kicked out by the server.

Once the ``keepalive_delay`` passed, we do not close the connection, we simply cease to ensure it is alive. This is purely for backward compatibility with our predecessor, as some host may retain the connection for hours.

-----------------------

Ready for more? Check out the :ref:`advanced <advanced>` section.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ dynamic = ["version"]
dependencies = [
"charset_normalizer>=2,<4",
"idna>=2.5,<4",
"urllib3.future>=2.10.904,<3",
"urllib3.future>=2.11.900,<3",
"wassima>=1.0.1,<2",
"kiss_headers>=2,<4",
]
Expand Down
4 changes: 2 additions & 2 deletions src/niquests/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
__url__: str = "https://niquests.readthedocs.io"

__version__: str
__version__ = "3.9.1"
__version__ = "3.10.0"

__build__: int = 0x030901
__build__: int = 0x031000
__author__: str = "Kenneth Reitz"
__author_email__: str = "[email protected]"
__license__: str = "Apache-2.0"
Expand Down
13 changes: 13 additions & 0 deletions src/niquests/_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ def __init__(
pool_connections: int = DEFAULT_POOLSIZE,
pool_maxsize: int = DEFAULT_POOLSIZE,
happy_eyeballs: bool | int = False,
keepalive_delay: float | int | None = 300.0,
keepalive_idle_window: float | int | None = 60.0,
):
if [disable_ipv4, disable_ipv6].count(True) == 2:
raise RuntimeError("Cannot disable both IPv4 and IPv6")
Expand Down Expand Up @@ -195,6 +197,9 @@ def __init__(

self._happy_eyeballs = happy_eyeballs

self._keepalive_delay = keepalive_delay
self._keepalive_idle_window = keepalive_idle_window

#: SSL Verification default.
#: Defaults to `True`, requiring requests to verify the TLS certificate at the
#: remote end.
Expand Down Expand Up @@ -253,6 +258,8 @@ def __init__(
pool_connections=pool_connections,
pool_maxsize=pool_maxsize,
happy_eyeballs=happy_eyeballs,
keepalive_delay=keepalive_delay,
keepalive_idle_window=keepalive_idle_window,
),
)
self.mount(
Expand All @@ -269,6 +276,8 @@ def __init__(
pool_connections=pool_connections,
pool_maxsize=pool_maxsize,
happy_eyeballs=happy_eyeballs,
keepalive_delay=keepalive_delay,
keepalive_idle_window=keepalive_idle_window,
),
)

Expand Down Expand Up @@ -436,6 +445,8 @@ async def on_early_response(early_response: Response) -> None:
pool_connections=self._pool_connections,
pool_maxsize=self._pool_maxsize,
happy_eyeballs=self._happy_eyeballs,
keepalive_delay=self._keepalive_delay,
keepalive_idle_window=self._keepalive_idle_window,
),
)
self.mount(
Expand All @@ -452,6 +463,8 @@ async def on_early_response(early_response: Response) -> None:
pool_connections=self._pool_connections,
pool_maxsize=self._pool_maxsize,
happy_eyeballs=self._happy_eyeballs,
keepalive_delay=self._keepalive_delay,
keepalive_idle_window=self._keepalive_idle_window,
),
)

Expand Down
22 changes: 21 additions & 1 deletion src/niquests/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,8 @@ class HTTPAdapter(BaseAdapter):
"_disable_ipv4",
"_disable_ipv6",
"_happy_eyeballs",
"_keepalive_delay",
"_keepalive_idle_window",
]

def __init__(
Expand All @@ -341,7 +343,7 @@ def __init__(
pool_maxsize: int = DEFAULT_POOLSIZE,
max_retries: RetryType = DEFAULT_RETRIES,
pool_block: bool = DEFAULT_POOLBLOCK,
*, # todo: revert if any complaint about it... :s
*,
quic_cache_layer: CacheLayerAltSvcType | None = None,
disable_http1: bool = False,
disable_http2: bool = False,
Expand All @@ -352,6 +354,8 @@ def __init__(
disable_ipv4: bool = False,
disable_ipv6: bool = False,
happy_eyeballs: bool | int = False,
keepalive_delay: float | int | None = 300.0,
keepalive_idle_window: float | int | None = 60.0,
):
if isinstance(max_retries, bool):
self.max_retries: RetryType = False
Expand Down Expand Up @@ -383,6 +387,8 @@ def __init__(
self._disable_ipv4 = disable_ipv4
self._disable_ipv6 = disable_ipv6
self._happy_eyeballs = happy_eyeballs
self._keepalive_delay = keepalive_delay
self._keepalive_idle_window = keepalive_idle_window

#: we keep a list of pending (lazy) response
self._promises: dict[str, Response] = {}
Expand Down Expand Up @@ -413,6 +419,8 @@ def __init__(
source_address=source_address,
socket_family=resolve_socket_family(disable_ipv4, disable_ipv6),
happy_eyeballs=happy_eyeballs,
keepalive_delay=keepalive_delay,
keepalive_idle_window=keepalive_idle_window,
)

def __getstate__(self) -> dict[str, typing.Any | None]:
Expand Down Expand Up @@ -447,6 +455,8 @@ def __setstate__(self, state):
source_address=self._source_address,
socket_family=resolve_socket_family(self._disable_ipv4, self._disable_ipv6),
happy_eyeballs=self._happy_eyeballs,
keepalive_delay=self._keepalive_delay,
keepalive_idle_window=self._keepalive_idle_window,
)

def init_poolmanager(
Expand Down Expand Up @@ -1335,6 +1345,8 @@ class AsyncHTTPAdapter(AsyncBaseAdapter):
"_disable_ipv4",
"_disable_ipv6",
"_happy_eyeballs",
"_keepalive_delay",
"_keepalive_idle_window",
]

def __init__(
Expand All @@ -1354,6 +1366,8 @@ def __init__(
disable_ipv4: bool = False,
disable_ipv6: bool = False,
happy_eyeballs: bool | int = False,
keepalive_delay: float | int | None = 300.0,
keepalive_idle_window: float | int | None = 60.0,
):
if isinstance(max_retries, bool):
self.max_retries: RetryType = False
Expand Down Expand Up @@ -1386,6 +1400,8 @@ def __init__(
self._disable_ipv4 = disable_ipv4
self._disable_ipv6 = disable_ipv6
self._happy_eyeballs = happy_eyeballs
self._keepalive_delay = keepalive_delay
self._keepalive_idle_window = keepalive_idle_window

#: we keep a list of pending (lazy) response
self._promises: dict[str, Response | AsyncResponse] = {}
Expand Down Expand Up @@ -1415,6 +1431,8 @@ def __init__(
source_address=source_address,
socket_family=resolve_socket_family(disable_ipv4, disable_ipv6),
happy_eyeballs=happy_eyeballs,
keepalive_delay=keepalive_delay,
keepalive_idle_window=keepalive_idle_window,
)

def __getstate__(self) -> dict[str, typing.Any | None]:
Expand Down Expand Up @@ -1449,6 +1467,8 @@ def __setstate__(self, state):
source_address=self._source_address,
socket_family=resolve_socket_family(self._disable_ipv4, self._disable_ipv6),
happy_eyeballs=self._happy_eyeballs,
keepalive_delay=self._keepalive_delay,
keepalive_idle_window=self._keepalive_idle_window,
)

def init_poolmanager(
Expand Down
20 changes: 14 additions & 6 deletions src/niquests/extensions/_async_ocsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
async_recv_tls,
async_recv_tls_and_decrypt,
async_send_tls,
PicoTLSException,
)
from ._ocsp import (
_str_fingerprint_of,
Expand All @@ -73,7 +74,11 @@ async def _ask_nicely_for_issuer(
sock = AsyncSocket(socket.AF_INET6, socket.SOCK_STREAM)

sock.settimeout(timeout)
await sock.connect(dst_address)

try:
await sock.connect(dst_address)
except (OSError, socket.timeout, TimeoutError, ConnectionError) as e:
raise PicoTLSException from e

SECP256R1_P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
SECP256R1_A = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
Expand Down Expand Up @@ -408,11 +413,14 @@ async def verify(
raise ValueError

if not proxies:
issuer_certificate = await _ask_nicely_for_issuer(
url_parsed.hostname,
conn_info.destination_address,
timeout,
)
try:
issuer_certificate = await _ask_nicely_for_issuer(
url_parsed.hostname,
conn_info.destination_address,
timeout,
)
except PicoTLSException:
issuer_certificate = None
else:
issuer_certificate = None

Expand Down
19 changes: 13 additions & 6 deletions src/niquests/extensions/_ocsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
recv_tls,
recv_tls_and_decrypt,
send_tls,
PicoTLSException,
)


Expand Down Expand Up @@ -80,8 +81,11 @@ def _ask_nicely_for_issuer(
else:
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)

sock.connect(dst_address)
sock.settimeout(timeout)
try:
sock.connect(dst_address)
except (OSError, socket.timeout, TimeoutError, ConnectionError) as e:
raise PicoTLSException from e

SECP256R1_P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
SECP256R1_A = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
Expand Down Expand Up @@ -402,11 +406,14 @@ def verify(
raise ValueError

if not proxies:
issuer_certificate = _ask_nicely_for_issuer(
url_parsed.hostname,
conn_info.destination_address,
timeout,
)
try:
issuer_certificate = _ask_nicely_for_issuer(
url_parsed.hostname,
conn_info.destination_address,
timeout,
)
except PicoTLSException:
issuer_certificate = None
else:
issuer_certificate = None

Expand Down
Loading

0 comments on commit 1933840

Please sign in to comment.