Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-httpcore2 for 
openSUSE:Factory checked in at 2026-10-27 16:47:07
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-httpcore2 (Old)
 and      /work/SRC/openSUSE:Factory/.python-httpcore2.new.2252680 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-httpcore2"

Tue Oct 27 16:47:07 2026 rev:5 rq:1392446 version:2.16.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-httpcore2/python-httpcore2.changes        
2026-09-28 10:37:03.526346233 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-httpcore2.new.2252680/python-httpcore2.changes
   2026-10-27 16:47:38.599168830 +0100
@@ -1,0 +2,19 @@
+Sun Oct 25 14:54:04 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 2.16.0:
+  * No changes since `2.15.1`. Version bumped to stay in lockstep
+    with `httpx2`.
+  * Configure the default SSL context once on Linux, instead of
+    on every handshake. With OpenSSL older than 3.4, CPU usage
+    grew over the lifetime of a reused client.
+  * Drop support for Python 3.10.
+  * Keep `CONNECT` and absolute-form proxy targets when the
+    request sets the `target` extension.
+  * Stop blocking reads when an HTTP/2 server lowers
+    `SETTINGS_MAX_CONCURRENT_STREAMS` while requests are in
+    flight.
+  * Raise `WriteError` instead of `anyio.EndOfStream` when
+    writing after the peer closes a TLS connection without
+    `close_notify`.
+
+-------------------------------------------------------------------

Old:
----
  httpcore2-2.13.1.tar.gz

New:
----
  httpcore2-2.16.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-httpcore2.spec ++++++
--- /var/tmp/diff_new_pack.pnHb3o/_old  2026-10-27 16:47:39.463205065 +0100
+++ /var/tmp/diff_new_pack.pnHb3o/_new  2026-10-27 16:47:39.467205232 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           python-httpcore2
-Version:        2.13.1
+Version:        2.16.0
 Release:        0
 Summary:        A minimal low-level HTTP client
 License:        BSD-3-Clause

++++++ httpcore2-2.13.1.tar.gz -> httpcore2-2.16.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.13.1/CHANGELOG.md 
new/httpcore2-2.16.0/CHANGELOG.md
--- old/httpcore2-2.13.1/CHANGELOG.md   2020-02-02 01:00:00.000000000 +0100
+++ new/httpcore2-2.16.0/CHANGELOG.md   2020-02-02 01:00:00.000000000 +0100
@@ -4,6 +4,34 @@
 
 The format is based on [Keep a 
Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## 2.16.0 (October 21st, 2026)
+
+No changes since `2.15.1`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.15.1 (October 19th, 2026)
+
+### Fixed
+
+* Configure the default SSL context once on Linux, instead of on every 
handshake. With OpenSSL older than 3.4, CPU usage grew over the lifetime of a 
reused client.
+  ([#1253](https://github.com/pydantic/httpx2/pull/1253))
+
+## 2.15.0 (October 19th, 2026)
+
+### Removed
+
+* Drop support for Python 3.10. 
([#1274](https://github.com/pydantic/httpx2/pull/1274))
+
+## 2.14.0 (October 18th, 2026)
+
+### Fixed
+
+* Keep `CONNECT` and absolute-form proxy targets when the request sets the 
`target` extension.
+  ([#1262](https://github.com/pydantic/httpx2/pull/1262))
+* Stop blocking reads when an HTTP/2 server lowers 
`SETTINGS_MAX_CONCURRENT_STREAMS` while requests are in flight.
+  ([#1217](https://github.com/pydantic/httpx2/pull/1217))
+* Raise `WriteError` instead of `anyio.EndOfStream` when writing after the 
peer closes a TLS connection without `close_notify`.
+  ([#1263](https://github.com/pydantic/httpx2/pull/1263))
+
 ## 2.13.1 (September 23rd, 2026)
 
 ### Fixed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.13.1/PKG-INFO 
new/httpcore2-2.16.0/PKG-INFO
--- old/httpcore2-2.13.1/PKG-INFO       2020-02-02 01:00:00.000000000 +0100
+++ new/httpcore2-2.16.0/PKG-INFO       2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.5
 Name: httpcore2
-Version: 2.13.1
+Version: 2.16.0
 Summary: A minimal low-level HTTP client.
 Project-URL: Changelog, 
https://github.com/pydantic/httpx2/blob/main/src/httpcore2/CHANGELOG.md
 Project-URL: Homepage, https://github.com/pydantic/httpx2
@@ -19,14 +19,13 @@
 Classifier: Operating System :: OS Independent
 Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 3 :: Only
-Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
 Classifier: Programming Language :: Python :: 3.14
 Classifier: Programming Language :: Python :: 3.15
 Classifier: Topic :: Internet :: WWW/HTTP
-Requires-Python: >=3.10
+Requires-Python: >=3.11
 Requires-Dist: h11>=0.16
 Requires-Dist: truststore>=0.10
 Provides-Extra: asyncio
@@ -152,6 +151,34 @@
 
 The format is based on [Keep a 
Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## 2.16.0 (October 21st, 2026)
+
+No changes since `2.15.1`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.15.1 (October 19th, 2026)
+
+### Fixed
+
+* Configure the default SSL context once on Linux, instead of on every 
handshake. With OpenSSL older than 3.4, CPU usage grew over the lifetime of a 
reused client.
+  ([#1253](https://github.com/pydantic/httpx2/pull/1253))
+
+## 2.15.0 (October 19th, 2026)
+
+### Removed
+
+* Drop support for Python 3.10. 
([#1274](https://github.com/pydantic/httpx2/pull/1274))
+
+## 2.14.0 (October 18th, 2026)
+
+### Fixed
+
+* Keep `CONNECT` and absolute-form proxy targets when the request sets the 
`target` extension.
+  ([#1262](https://github.com/pydantic/httpx2/pull/1262))
+* Stop blocking reads when an HTTP/2 server lowers 
`SETTINGS_MAX_CONCURRENT_STREAMS` while requests are in flight.
+  ([#1217](https://github.com/pydantic/httpx2/pull/1217))
+* Raise `WriteError` instead of `anyio.EndOfStream` when writing after the 
peer closes a TLS connection without `close_notify`.
+  ([#1263](https://github.com/pydantic/httpx2/pull/1263))
+
 ## 2.13.1 (September 23rd, 2026)
 
 ### Fixed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.13.1/httpcore2/_async/http2.py 
new/httpcore2-2.16.0/httpcore2/_async/http2.py
--- old/httpcore2-2.13.1/httpcore2/_async/http2.py      2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.16.0/httpcore2/_async/http2.py      2020-02-02 
01:00:00.000000000 +0100
@@ -105,13 +105,23 @@
                 # its max_concurrent_streams value
                 self._max_streams = 1
 
+                # Revoked permits still held by active or queued requests.
+                self._max_streams_deficit = 0
+
                 local_settings_max_streams = 
self._h2_state.local_settings.max_concurrent_streams
                 self._max_streams_semaphore = 
AsyncSemaphore(local_settings_max_streams)
 
                 for _ in range(local_settings_max_streams - self._max_streams):
                     await self._max_streams_semaphore.acquire()
 
-        await self._max_streams_semaphore.acquire()
+        # A queued request may receive a permit just before the limit drops.
+        while True:
+            await self._max_streams_semaphore.acquire()
+            with AsyncShieldCancellation():
+                async with self._state_lock:
+                    if not self._max_streams_deficit:
+                        break
+                    self._max_streams_deficit -= 1
 
         try:
             stream_id = self._h2_state.get_next_available_stream_id()
@@ -338,7 +348,6 @@
                 for event in events:
                     if isinstance(event, h2.events.RemoteSettingsChanged):
                         async with Trace("receive_remote_settings", logger, 
request) as trace:
-                            await self._receive_remote_settings_change(event)
                             trace.return_value = event
 
                     elif isinstance(
@@ -366,17 +375,25 @@
                 self._h2_state.local_settings.max_concurrent_streams,
             )
             if new_max_streams and new_max_streams != self._max_streams:
+                # Never wait for permits while holding the read lock.
                 while new_max_streams > self._max_streams:
-                    await self._max_streams_semaphore.release()
+                    if self._max_streams_deficit > 0:
+                        self._max_streams_deficit -= 1
+                    else:
+                        await self._max_streams_semaphore.release()
                     self._max_streams += 1
                 while new_max_streams < self._max_streams:
-                    await self._max_streams_semaphore.acquire()
+                    if not self._max_streams_semaphore.acquire_if_available():
+                        self._max_streams_deficit += 1
                     self._max_streams -= 1
 
     async def _response_closed(self, stream_id: int) -> None:
         async with self._state_lock:
             if stream_id in self._events:
-                await self._max_streams_semaphore.release()
+                if self._max_streams_deficit > 0:
+                    self._max_streams_deficit -= 1
+                else:
+                    await self._max_streams_semaphore.release()
                 del self._events[stream_id]
             if self._connection_terminated and not self._events:
                 await self.aclose()
@@ -422,7 +439,13 @@
             self._connection_error = True
             raise exc
 
-        events: list[h2.events.Event] = self._h2_state.receive_data(data)
+        # Apply settings and stream admission limits atomically.
+        with AsyncShieldCancellation():
+            async with self._state_lock:
+                events: list[h2.events.Event] = 
self._h2_state.receive_data(data)
+                for event in events:
+                    if isinstance(event, h2.events.RemoteSettingsChanged):
+                        await self._receive_remote_settings_change(event)
 
         return events
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.13.1/httpcore2/_async/http_proxy.py 
new/httpcore2-2.16.0/httpcore2/_async/http_proxy.py
--- old/httpcore2-2.13.1/httpcore2/_async/http_proxy.py 2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.16.0/httpcore2/_async/http_proxy.py 2020-02-02 
01:00:00.000000000 +0100
@@ -184,14 +184,16 @@
             scheme=self._proxy_origin.scheme,
             host=self._proxy_origin.host,
             port=self._proxy_origin.port,
-            target=bytes(request.url),
+            target=bytes(request.url) if request.url.target.startswith(b"/") 
else request.url.target,
         )
         proxy_request = Request(
             method=request.method,
             url=url,
             headers=headers,
             content=request.stream,
-            extensions={key: value for key, value in 
request.extensions.items() if key != "sni_hostname"},
+            extensions={
+                key: value for key, value in request.extensions.items() if key 
not in ("sni_hostname", "target")
+            },
         )
         return await self._connection.handle_async_request(proxy_request)
 
@@ -274,7 +276,9 @@
                     method=b"CONNECT",
                     url=connect_url,
                     headers=connect_headers,
-                    extensions={key: value for key, value in 
request.extensions.items() if key != "sni_hostname"},
+                    extensions={
+                        key: value for key, value in 
request.extensions.items() if key not in ("sni_hostname", "target")
+                    },
                 )
                 connect_response = await 
self._connection.handle_async_request(connect_request)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.13.1/httpcore2/_backends/anyio.py 
new/httpcore2-2.16.0/httpcore2/_backends/anyio.py
--- old/httpcore2-2.13.1/httpcore2/_backends/anyio.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.16.0/httpcore2/_backends/anyio.py   2020-02-02 
01:00:00.000000000 +0100
@@ -46,6 +46,7 @@
             TimeoutError: WriteTimeout,
             anyio.BrokenResourceError: WriteError,
             anyio.ClosedResourceError: WriteError,
+            anyio.EndOfStream: WriteError,
         }
         with map_exceptions(exc_map):
             with anyio.fail_after(timeout):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.13.1/httpcore2/_ssl.py 
new/httpcore2-2.16.0/httpcore2/_ssl.py
--- old/httpcore2-2.13.1/httpcore2/_ssl.py      2020-02-02 01:00:00.000000000 
+0100
+++ new/httpcore2-2.16.0/httpcore2/_ssl.py      2020-02-02 01:00:00.000000000 
+0100
@@ -1,12 +1,40 @@
 import os
+import platform
+import re
 import ssl
 
 import truststore
 
+# Mirrors `truststore`'s Linux fallbacks for when OpenSSL's compiled-in paths 
are unusable:
+# 
https://github.com/sethmlarson/truststore/blob/0714f72a739d182cdb8502f4e1bb4cf7ebfe4eb5/src/truststore/_openssl.py#L8-L19
+CA_FILE_CANDIDATES = [
+    "/etc/ssl/cert.pem",
+    "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
+    "/etc/pki/tls/cert.pem",
+    "/etc/ssl/certs/ca-certificates.crt",
+    "/etc/ssl/ca-bundle.pem",
+]
 
-def default_ssl_context() -> ssl.SSLContext:
-    if cafile := os.environ.get("SSL_CERT_FILE"):  # pragma: no cover
+
+# `_trust_env` is internal to `httpx2`; drop it with the Linux branch below 
once sethmlarson/truststore#213 ships.
+def default_ssl_context(*, _trust_env: bool = True) -> ssl.SSLContext:
+    if _trust_env and (cafile := os.environ.get("SSL_CERT_FILE")):  # pragma: 
no cover
         return ssl.create_default_context(cafile=cafile)
-    if capath := os.environ.get("SSL_CERT_DIR"):  # pragma: no cover
+    if _trust_env and (capath := os.environ.get("SSL_CERT_DIR")):  # pragma: 
no cover
         return ssl.create_default_context(capath=capath)
-    return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+    if platform.system() in ("Windows", "Darwin"):  # pragma: no cover
+        return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+    # `truststore` re-adds verify paths per handshake, which OpenSSL < 3.4 
accumulates (sethmlarson/truststore#212).
+    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+    # Compiled-in paths, since `set_default_verify_paths()` would read 
`SSL_CERT_*`.
+    paths = ssl.get_default_verify_paths()
+    cafile = paths.openssl_cafile if os.path.isfile(paths.openssl_cafile) else 
None
+    capath = paths.openssl_capath if os.path.isdir(paths.openssl_capath) else 
None
+    if capath and not cafile:  # pragma: no cover
+        if not any(re.fullmatch(r"[0-9a-fA-F]{8}\.\d", f) for f in 
os.listdir(capath)):
+            capath = None
+    if not cafile and not capath:  # pragma: no cover
+        cafile = next(filter(os.path.isfile, CA_FILE_CANDIDATES), None)
+    if cafile or capath:
+        ctx.load_verify_locations(cafile, capath)
+    return ctx
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.13.1/httpcore2/_sync/http2.py 
new/httpcore2-2.16.0/httpcore2/_sync/http2.py
--- old/httpcore2-2.13.1/httpcore2/_sync/http2.py       2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.16.0/httpcore2/_sync/http2.py       2020-02-02 
01:00:00.000000000 +0100
@@ -105,13 +105,23 @@
                 # its max_concurrent_streams value
                 self._max_streams = 1
 
+                # Revoked permits still held by active or queued requests.
+                self._max_streams_deficit = 0
+
                 local_settings_max_streams = 
self._h2_state.local_settings.max_concurrent_streams
                 self._max_streams_semaphore = 
Semaphore(local_settings_max_streams)
 
                 for _ in range(local_settings_max_streams - self._max_streams):
                     self._max_streams_semaphore.acquire()
 
-        self._max_streams_semaphore.acquire()
+        # A queued request may receive a permit just before the limit drops.
+        while True:
+            self._max_streams_semaphore.acquire()
+            with ShieldCancellation():
+                with self._state_lock:
+                    if not self._max_streams_deficit:
+                        break
+                    self._max_streams_deficit -= 1
 
         try:
             stream_id = self._h2_state.get_next_available_stream_id()
@@ -338,7 +348,6 @@
                 for event in events:
                     if isinstance(event, h2.events.RemoteSettingsChanged):
                         with Trace("receive_remote_settings", logger, request) 
as trace:
-                            self._receive_remote_settings_change(event)
                             trace.return_value = event
 
                     elif isinstance(
@@ -366,17 +375,25 @@
                 self._h2_state.local_settings.max_concurrent_streams,
             )
             if new_max_streams and new_max_streams != self._max_streams:
+                # Never wait for permits while holding the read lock.
                 while new_max_streams > self._max_streams:
-                    self._max_streams_semaphore.release()
+                    if self._max_streams_deficit > 0:
+                        self._max_streams_deficit -= 1
+                    else:
+                        self._max_streams_semaphore.release()
                     self._max_streams += 1
                 while new_max_streams < self._max_streams:
-                    self._max_streams_semaphore.acquire()
+                    if not self._max_streams_semaphore.acquire_if_available():
+                        self._max_streams_deficit += 1
                     self._max_streams -= 1
 
     def _response_closed(self, stream_id: int) -> None:
         with self._state_lock:
             if stream_id in self._events:
-                self._max_streams_semaphore.release()
+                if self._max_streams_deficit > 0:
+                    self._max_streams_deficit -= 1
+                else:
+                    self._max_streams_semaphore.release()
                 del self._events[stream_id]
             if self._connection_terminated and not self._events:
                 self.close()
@@ -422,7 +439,13 @@
             self._connection_error = True
             raise exc
 
-        events: list[h2.events.Event] = self._h2_state.receive_data(data)
+        # Apply settings and stream admission limits atomically.
+        with ShieldCancellation():
+            with self._state_lock:
+                events: list[h2.events.Event] = 
self._h2_state.receive_data(data)
+                for event in events:
+                    if isinstance(event, h2.events.RemoteSettingsChanged):
+                        self._receive_remote_settings_change(event)
 
         return events
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.13.1/httpcore2/_sync/http_proxy.py 
new/httpcore2-2.16.0/httpcore2/_sync/http_proxy.py
--- old/httpcore2-2.13.1/httpcore2/_sync/http_proxy.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.16.0/httpcore2/_sync/http_proxy.py  2020-02-02 
01:00:00.000000000 +0100
@@ -184,14 +184,16 @@
             scheme=self._proxy_origin.scheme,
             host=self._proxy_origin.host,
             port=self._proxy_origin.port,
-            target=bytes(request.url),
+            target=bytes(request.url) if request.url.target.startswith(b"/") 
else request.url.target,
         )
         proxy_request = Request(
             method=request.method,
             url=url,
             headers=headers,
             content=request.stream,
-            extensions={key: value for key, value in 
request.extensions.items() if key != "sni_hostname"},
+            extensions={
+                key: value for key, value in request.extensions.items() if key 
not in ("sni_hostname", "target")
+            },
         )
         return self._connection.handle_request(proxy_request)
 
@@ -274,7 +276,9 @@
                     method=b"CONNECT",
                     url=connect_url,
                     headers=connect_headers,
-                    extensions={key: value for key, value in 
request.extensions.items() if key != "sni_hostname"},
+                    extensions={
+                        key: value for key, value in 
request.extensions.items() if key not in ("sni_hostname", "target")
+                    },
                 )
                 connect_response = 
self._connection.handle_request(connect_request)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.13.1/httpcore2/_synchronization.py 
new/httpcore2-2.16.0/httpcore2/_synchronization.py
--- old/httpcore2-2.13.1/httpcore2/_synchronization.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.16.0/httpcore2/_synchronization.py  2020-02-02 
01:00:00.000000000 +0100
@@ -175,6 +175,28 @@
         elif self._backend == "asyncio":
             await self._anyio_semaphore.acquire()
 
+    def acquire_if_available(self) -> bool:
+        """
+        Acquire a permit if one is immediately available, without blocking.
+        Returns True if a permit was acquired.
+
+        Like `release()`, this requires `setup()` to have run, which any
+        prior `acquire()` call ensures.
+        """
+        if self._backend == "trio":
+            try:
+                self._trio_semaphore.acquire_nowait()
+            except trio.WouldBlock:
+                return False
+            return True
+        elif self._backend == "asyncio":
+            try:
+                self._anyio_semaphore.acquire_nowait()
+            except anyio.WouldBlock:
+                return False
+            return True
+        return False  # pragma: no cover
+
     async def release(self) -> None:
         if self._backend == "trio":
             self._trio_semaphore.release()
@@ -293,6 +315,9 @@
     def acquire(self) -> None:
         self._semaphore.acquire()
 
+    def acquire_if_available(self) -> bool:
+        return self._semaphore.acquire(blocking=False)
+
     def release(self) -> None:
         self._semaphore.release()
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.13.1/pyproject.toml 
new/httpcore2-2.16.0/pyproject.toml
--- old/httpcore2-2.13.1/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
+++ new/httpcore2-2.16.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
@@ -20,7 +20,7 @@
 dynamic = ["readme", "version"]
 description = "A minimal low-level HTTP client."
 license = "BSD-3-Clause"
-requires-python = ">=3.10"
+requires-python = ">=3.11"
 authors = [{ name = "Tom Christie", email = "[email protected]" }]
 maintainers = [
     { name = "Pydantic Services Inc.", email = "[email protected]" },
@@ -36,7 +36,6 @@
     "Operating System :: OS Independent",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3 :: Only",
-    "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
     "Programming Language :: Python :: 3.13",

Reply via email to