Skip to content

Commit

Permalink
Release 3.12 (#198)
Browse files Browse the repository at this point in the history
3.12.0 (2025-01-01)
-------------------

**Fixed**
- Restoring the state of `AsyncSession` through pickle.
- Typing definition for query parameter not accepting `None` as values.
(#193)
- Overload incorrect definition for `AsyncSession::get`. (#192)

**Added**
- Support for `PathLike` objects for `verify` parameter when passing a
ca bundle path. (#194)
- Caching and restoring OCSP state through pickling `Session` or
`AsyncSession`.
- Caching and restoring QUIC known compatible hosts through pickling
`Session` or `AsyncSession`.
- Shortcut convenient access to `Retry` and `Timeout` configuration
objects in top-level import.
  • Loading branch information
Ousret authored Jan 1, 2025
2 parents d8b5d92 + d9115c1 commit 374c160
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 4 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.12.900', 'wassima>=1.0.1', 'idna', 'kiss_headers']
additional_dependencies: ['charset_normalizer', 'urllib3.future>=2.12.900', 'wassima>=1.0.1', 'idna', 'kiss_headers', 'qh3>=1.3']
14 changes: 14 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
Release History
===============

3.12.0 (2025-01-01)
-------------------

**Fixed**
- Restoring the state of `AsyncSession` through pickle.
- Typing definition for query parameter not accepting `None` as values. (#193)
- Overload incorrect definition for `AsyncSession::get`. (#192)

**Added**
- Support for `PathLike` objects for `verify` parameter when passing a ca bundle path. (#194)
- Caching and restoring OCSP state through pickling `Session` or `AsyncSession`.
- Caching and restoring QUIC known compatible hosts through pickling `Session` or `AsyncSession`.
- Shortcut convenient access to `Retry` and `Timeout` configuration objects in top-level import.

3.11.4 (2024-12-23)
-------------------

Expand Down
7 changes: 7 additions & 0 deletions src/niquests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,13 @@

if HAS_LEGACY_URLLIB3 is False:
from urllib3.exceptions import DependencyWarning
from urllib3 import Timeout as TimeoutConfiguration, Retry as RetryConfiguration
else:
from urllib3_future.exceptions import DependencyWarning # type: ignore[assignment]
from urllib3_future import ( # type: ignore[assignment]
Timeout as TimeoutConfiguration,
Retry as RetryConfiguration,
)

# urllib3's DependencyWarnings should be silenced.
warnings.simplefilter("ignore", DependencyWarning)
Expand Down Expand Up @@ -128,4 +133,6 @@
"codes",
"AsyncSession",
"AsyncResponse",
"TimeoutConfiguration",
"RetryConfiguration",
)
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.11.4"
__version__ = "3.12.0"

__build__: int = 0x031104
__build__: int = 0x031200
__author__: str = "Kenneth Reitz"
__author_email__: str = "me@kennethreitz.org"
__license__: str = "Apache-2.0"
Expand Down
46 changes: 46 additions & 0 deletions src/niquests/_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,52 @@ async def __aenter__(self) -> AsyncSession:
async def __aexit__(self, exc, value, tb) -> None:
await self.close()

def __setstate__(self, state):
for attr, value in state.items():
setattr(self, attr, value)

self.resolver = create_async_resolver(None)
self._own_resolver = True

self.adapters = OrderedDict()
self.mount(
"https://",
AsyncHTTPAdapter(
quic_cache_layer=self.quic_cache_layer,
max_retries=self.retries,
disable_http1=self._disable_http1,
disable_http2=self._disable_http2,
disable_http3=self._disable_http3,
source_address=self.source_address,
disable_ipv4=self._disable_ipv4,
disable_ipv6=self._disable_ipv6,
resolver=self.resolver,
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(
"http://",
AsyncHTTPAdapter(
max_retries=self.retries,
disable_http1=self._disable_http1,
disable_http2=self._disable_http2,
disable_http3=self._disable_http3,
source_address=self.source_address,
disable_ipv4=self._disable_ipv4,
disable_ipv6=self._disable_ipv6,
resolver=self.resolver,
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,
),
)

def mount(self, prefix: str, adapter: AsyncBaseAdapter) -> None: # type: ignore[override]
super().mount(prefix, adapter) # type: ignore[arg-type]

Expand Down
37 changes: 37 additions & 0 deletions src/niquests/extensions/_async_ocsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,43 @@ def __init__(self, max_size: int = 2048):
self._timings: list[datetime.datetime] = []
self.hold: bool = False

@staticmethod
def support_pickle() -> bool:
"""This gives you a hint on whether you can cache it to restore later."""
return hasattr(OCSPResponse, "serialize")

def __getstate__(self) -> dict[str, typing.Any]:
return {
"_max_size": self._max_size,
"_store": {k: v.serialize() for k, v in self._store.items()},
"_issuers_map": {k: v.serialize() for k, v in self._issuers_map.items()},
}

def __setstate__(self, state: dict[str, typing.Any]) -> None:
if (
"_store" not in state
or "_issuers_map" not in state
or "_max_size" not in state
):
raise OSError("unrecoverable state for InMemoryRevocationStatus")

self.hold = False
self._timings = []

self._max_size = state["_max_size"]

self._store = {}
self._semaphores = {}

for k, v in state["_store"].items():
self._store[k] = OCSPResponse.deserialize(v)
self._semaphores[k] = asyncio.Semaphore()

self._issuers_map = {}

for k, v in state["_issuers_map"].items():
self._issuers_map[k] = Certificate.deserialize(v)

def get_issuer_of(self, peer_certificate: Certificate) -> Certificate | None:
fingerprint: str = _str_fingerprint_of(peer_certificate)

Expand Down
37 changes: 37 additions & 0 deletions src/niquests/extensions/_ocsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import socket
import ssl
import threading
import typing
import warnings
from hashlib import sha256
from random import randint
Expand Down Expand Up @@ -204,6 +205,42 @@ def __init__(self, max_size: int = 2048):
self._access_lock = threading.RLock()
self.hold: bool = False

@staticmethod
def support_pickle() -> bool:
"""This gives you a hint on whether you can cache it to restore later."""
return hasattr(OCSPResponse, "serialize")

def __getstate__(self) -> dict[str, typing.Any]:
return {
"_max_size": self._max_size,
"_store": {k: v.serialize() for k, v in self._store.items()},
"_issuers_map": {k: v.serialize() for k, v in self._issuers_map.items()},
}

def __setstate__(self, state: dict[str, typing.Any]) -> None:
if (
"_store" not in state
or "_issuers_map" not in state
or "_max_size" not in state
):
raise OSError("unrecoverable state for InMemoryRevocationStatus")

self._access_lock = threading.RLock()
self.hold = False
self._timings = []

self._max_size = state["_max_size"]

self._store = {}

for k, v in state["_store"].items():
self._store[k] = OCSPResponse.deserialize(v)

self._issuers_map = {}

for k, v in state["_issuers_map"].items():
self._issuers_map[k] = Certificate.deserialize(v)

def get_issuer_of(self, peer_certificate: Certificate) -> Certificate | None:
with self._access_lock:
fingerprint: str = _str_fingerprint_of(peer_certificate)
Expand Down
1 change: 1 addition & 0 deletions src/niquests/extensions/_picotls.py
Original file line number Diff line number Diff line change
Expand Up @@ -789,4 +789,5 @@ async def async_send_tls(s, rec_type, msg):
"HANDSHAKE",
"ALERT",
"CHANGE_CIPHER",
"PicoTLSException",
)
8 changes: 7 additions & 1 deletion src/niquests/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ class Session:
"_keepalive_delay",
"_keepalive_idle_window",
"base_url",
"quic_cache_layer",
]

def __init__(
Expand Down Expand Up @@ -1476,13 +1477,18 @@ def mount(self, prefix: str, adapter: BaseAdapter) -> None:

def __getstate__(self):
state = {attr: getattr(self, attr, None) for attr in self.__attrs__}
if (
self._ocsp_cache is not None
and hasattr(self._ocsp_cache, "support_pickle")
and self._ocsp_cache.support_pickle() is True
):
state["_ocsp_cache"] = self._ocsp_cache
return state

def __setstate__(self, state):
for attr, value in state.items():
setattr(self, attr, value)

self.quic_cache_layer = QuicSharedCache(max_size=12_288)
self.resolver = create_resolver(None)
self._own_resolver = True

Expand Down
13 changes: 13 additions & 0 deletions src/niquests/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ def __init__(self, max_size: int | None) -> None:
self._max_size = max_size
self._lock: threading.RLock | DummyLock = threading.RLock()

def __getstate__(self) -> dict[str, typing.Any]:
return {"_store": self._store, "_max_size": self._max_size}

def __setstate__(self, state: dict[str, typing.Any]) -> None:
self._lock = threading.RLock()
self._store = state["_store"]
self._max_size = state["_max_size"]

def __delitem__(self, __key) -> None:
with self._lock:
del self._store[__key]
Expand Down Expand Up @@ -248,6 +256,11 @@ def __init__(self, max_size: int | None) -> None:
super().__init__(max_size)
self._lock = DummyLock()

def __setstate__(self, state: dict[str, typing.Any]) -> None:
self._lock = DummyLock()
self._store = state["_store"]
self._max_size = state["_max_size"]


class DummyLock:
def __enter__(self):
Expand Down

0 comments on commit 374c160

Please sign in to comment.