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-08-27 18:50:38
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-httpcore2 (Old)
 and      /work/SRC/openSUSE:Factory/.python-httpcore2.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-httpcore2"

Thu Aug 27 18:50:38 2026 rev:3 rq:1373646 version:2.12.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-httpcore2/python-httpcore2.changes        
2026-07-06 12:27:33.189440892 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-httpcore2.new.1265/python-httpcore2.changes  
    2026-08-27 18:53:14.884475761 +0200
@@ -1,0 +2,20 @@
+Tue Aug 25 15:19:19 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 2.12.0:
+  * No changes since `2.11.0`. Version bumped to stay in lockstep
+    with `httpx2`.
+  * Cache sniffio availability instead of importing it on every
+    synchronization call.
+  * Add support for Python 3.15.
+  * Avoid quadratic copying when sending large HTTP/2 request
+    bodies.
+  * Propagate the original exception instead of raising
+    `KeyError` when an HTTP/2 stream fails.
+  * Start TLS for the `wss` scheme in SOCKS5 proxy connections.
+  * Assign each released connection to a single queued request,
+    eliminating multi-assignment churn in the connection pool.
+  * Clean up garbage connections on cancellation in
+    `AsyncConnectionPool`.
+  * Read `_expire_at` only once in `has_expired()`.
+
+-------------------------------------------------------------------

Old:
----
  httpcore2-2.5.0.tar.gz

New:
----
  httpcore2-2.12.0.tar.gz

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

Other differences:
------------------
++++++ python-httpcore2.spec ++++++
--- /var/tmp/diff_new_pack.sCMkD7/_old  2026-08-27 18:53:15.791507364 +0200
+++ /var/tmp/diff_new_pack.sCMkD7/_new  2026-08-27 18:53:15.793507434 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           python-httpcore2
-Version:        2.5.0
+Version:        2.12.0
 Release:        0
 Summary:        A minimal low-level HTTP client
 License:        BSD-3-Clause
@@ -26,7 +26,7 @@
 BuildRequires:  %{python_module hatch-fancy-pypi-readme}
 BuildRequires:  %{python_module hatchling}
 BuildRequires:  %{python_module pip}
-BuildRequires:  %{python_module uv-dynamic-versioning >= 0.8.0}
+BuildRequires:  %{python_module uv-dynamic-versioning >= 0.14.0}
 BuildRequires:  python-rpm-macros
 # SECTION test requirements
 BuildRequires:  %{python_module h11 >= 0.16}
@@ -37,7 +37,7 @@
 Requires:       python-truststore >= 0.10
 Suggests:       python-h2 >= 3
 Suggests:       python-socksio >= 1.0
-Suggests:       python-trio >= 0.22.0
+Suggests:       python-trio >= 0.33.0
 Suggests:       python-anyio >= 4.5
 BuildArch:      noarch
 %python_subpackages

++++++ httpcore2-2.5.0.tar.gz -> httpcore2-2.12.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/CHANGELOG.md 
new/httpcore2-2.12.0/CHANGELOG.md
--- old/httpcore2-2.5.0/CHANGELOG.md    2020-02-02 01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/CHANGELOG.md   2020-02-02 01:00:00.000000000 +0100
@@ -4,6 +4,57 @@
 
 The format is based on [Keep a 
Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## 2.12.0 (August 18th, 2026)
+
+No changes since `2.11.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.11.0 (August 18th, 2026)
+
+### Changed
+
+* Cache sniffio availability instead of importing it on every synchronization 
call.
+  ([#1132](https://github.com/pydantic/httpx2/pull/1132))
+
+## 2.10.0 (August 9th, 2026)
+
+### Added
+
+* Add support for Python 3.15. 
([#1090](https://github.com/pydantic/httpx2/pull/1090))
+
+### Changed
+
+* Avoid quadratic copying when sending large HTTP/2 request bodies. 
([#1127](https://github.com/pydantic/httpx2/pull/1127))
+
+### Fixed
+
+* Propagate the original exception instead of raising `KeyError` when an 
HTTP/2 stream fails. ([#1093](https://github.com/pydantic/httpx2/pull/1093))
+* Start TLS for the `wss` scheme in SOCKS5 proxy connections. 
([#1104](https://github.com/pydantic/httpx2/pull/1104))
+
+## 2.9.1 (July 24th, 2026)
+
+No changes since `2.9.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.9.0 (July 23rd, 2026)
+
+No changes since `2.8.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.8.0 (July 23rd, 2026)
+
+### Changed
+
+* Assign each released connection to a single queued request, eliminating 
multi-assignment churn in the connection pool. 
([#1075](https://github.com/pydantic/httpx2/pull/1075))
+
+## 2.7.0 (July 14th, 2026)
+
+No changes since `2.6.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.6.0 (July 14th, 2026)
+
+### Fixed
+
+* Clean up garbage connections on cancellation in `AsyncConnectionPool`. 
([#983](https://github.com/pydantic/httpx2/pull/983))
+* Read `_expire_at` only once in `has_expired()`. 
([#1045](https://github.com/pydantic/httpx2/pull/1045))
+
 ## 2.5.0 (June 25th, 2026)
 
 ### Fixed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/PKG-INFO 
new/httpcore2-2.12.0/PKG-INFO
--- old/httpcore2-2.5.0/PKG-INFO        2020-02-02 01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/PKG-INFO       2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
-Metadata-Version: 2.4
+Metadata-Version: 2.5
 Name: httpcore2
-Version: 2.5.0
+Version: 2.12.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
@@ -24,6 +24,7 @@
 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-Dist: h11>=0.16
@@ -35,17 +36,17 @@
 Provides-Extra: socks
 Requires-Dist: socksio==1.*; extra == 'socks'
 Provides-Extra: trio
-Requires-Dist: trio<1.0,>=0.22.0; extra == 'trio'
+Requires-Dist: trio<1.0,>=0.33.0; extra == 'trio'
 Description-Content-Type: text/markdown
 
-# HTTP Core
+# HTTPCore 2
 
-[![Test 
Suite](https://github.com/encode/httpcore/workflows/Test%20Suite/badge.svg)](https://github.com/encode/httpcore/actions)
-[![Package 
version](https://badge.fury.io/py/httpcore.svg)](https://pypi.org/project/httpcore/)
+[![Test 
Suite](https://github.com/pydantic/httpx2/workflows/Test%20Suite/badge.svg)](https://github.com/pydantic/httpx2/actions)
+[![Package 
version](https://badge.fury.io/py/httpcore2.svg)](https://pypi.org/project/httpcore2/)
 
 > *Do one thing, and do it well.*
 
-The HTTP Core package provides a minimal low-level HTTP client, which does
+The HTTPCore 2 package provides a minimal low-level HTTP client, which does
 one thing only. Sending HTTP requests.
 
 It does not provide any high level model abstractions over the API,
@@ -54,7 +55,7 @@
 content or charset decoding, handling JSON, environment based configuration
 defaults, or any of that Jazz.
 
-Some things HTTP Core does do:
+Some things HTTPCore 2 does do:
 
 * Sending HTTP requests.
 * Thread-safe / task-safe connection pooling.
@@ -68,13 +69,13 @@
 For HTTP/1.1 only support, install with:
 
 ```shell
-pip install httpcore
+pip install httpcore2
 ```
 
 There are also a number of optional extras available...
 
 ```shell
-pip install httpcore['asyncio,trio,http2,socks']
+pip install 'httpcore2[asyncio,trio,http2,socks]'
 ```
 
 ## Sending requests
@@ -82,9 +83,9 @@
 Send an HTTP request:
 
 ```python
-import httpcore
+import httpcore2
 
-response = httpcore.request("GET", "https://www.example.com/";)
+response = httpcore2.request("GET", "https://www.example.com/";)
 
 print(response)
 # <Response [200]>
@@ -96,25 +97,25 @@
 # b'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>\n\n<meta 
charset="utf-8"/>\n ...'
 ```
 
-The top-level `httpcore.request()` function is provided for convenience. In 
practice whenever you're working with `httpcore` you'll want to use the 
connection pooling functionality that it provides.
+The top-level `httpcore2.request()` function is provided for convenience. In 
practice whenever you're working with `httpcore2` you'll want to use the 
connection pooling functionality that it provides.
 
 ```python
-import httpcore
+import httpcore2
 
-http = httpcore.ConnectionPool()
+http = httpcore2.ConnectionPool()
 response = http.request("GET", "https://www.example.com/";)
 ```
 
-Once you're ready to get going, [head over to the 
documentation](https://www.encode.io/httpcore/).
+Once you're ready to get going, [head over to the 
documentation](https://github.com/pydantic/httpx2/tree/main/src/httpcore2/docs).
 
 ## Motivation
 
-You *probably* don't want to be using HTTP Core directly. It might make sense 
if
+You *probably* don't want to be using HTTPCore 2 directly. It might make sense 
if
 you're writing something like a proxy service in Python, and you just want
 something at the lowest possible level, but more typically you'll want to use
-a higher level client library, such as `httpx`.
+a higher level client library, such as `httpx2`.
 
-The motivation for `httpcore` is:
+The motivation for `httpcore2` is:
 
 * To provide a reusable low-level client library, that other packages can then 
build on top of.
 * To provide a *really clear interface split* between the networking code and 
client logic,
@@ -122,17 +123,17 @@
 
 ## Dependencies
 
-The `httpcore` package has the following dependencies...
+The `httpcore2` package has the following dependencies...
 
 * `h11`
 * `truststore`
 
 And the following optional extras...
 
-* `anyio` - Required by `pip install httpcore['asyncio']`.
-* `trio` - Required by `pip install httpcore['trio']`.
-* `h2` - Required by `pip install httpcore['http2']`.
-* `socksio` - Required by `pip install httpcore['socks']`.
+* `anyio` - Required by `pip install 'httpcore2[asyncio]'`.
+* `trio` - Required by `pip install 'httpcore2[trio]'`.
+* `h2` - Required by `pip install 'httpcore2[http2]'`.
+* `socksio` - Required by `pip install 'httpcore2[socks]'`.
 
 ## Versioning
 
@@ -142,8 +143,8 @@
 
 We recommend pinning your requirements either the most current major version, 
or a more specific version range:
 
-```python
-pip install 'httpcore==1.*'
+```shell
+pip install 'httpcore2==2.*'
 ```
 # Changelog
 
@@ -151,6 +152,57 @@
 
 The format is based on [Keep a 
Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## 2.12.0 (August 18th, 2026)
+
+No changes since `2.11.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.11.0 (August 18th, 2026)
+
+### Changed
+
+* Cache sniffio availability instead of importing it on every synchronization 
call.
+  ([#1132](https://github.com/pydantic/httpx2/pull/1132))
+
+## 2.10.0 (August 9th, 2026)
+
+### Added
+
+* Add support for Python 3.15. 
([#1090](https://github.com/pydantic/httpx2/pull/1090))
+
+### Changed
+
+* Avoid quadratic copying when sending large HTTP/2 request bodies. 
([#1127](https://github.com/pydantic/httpx2/pull/1127))
+
+### Fixed
+
+* Propagate the original exception instead of raising `KeyError` when an 
HTTP/2 stream fails. ([#1093](https://github.com/pydantic/httpx2/pull/1093))
+* Start TLS for the `wss` scheme in SOCKS5 proxy connections. 
([#1104](https://github.com/pydantic/httpx2/pull/1104))
+
+## 2.9.1 (July 24th, 2026)
+
+No changes since `2.9.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.9.0 (July 23rd, 2026)
+
+No changes since `2.8.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.8.0 (July 23rd, 2026)
+
+### Changed
+
+* Assign each released connection to a single queued request, eliminating 
multi-assignment churn in the connection pool. 
([#1075](https://github.com/pydantic/httpx2/pull/1075))
+
+## 2.7.0 (July 14th, 2026)
+
+No changes since `2.6.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.6.0 (July 14th, 2026)
+
+### Fixed
+
+* Clean up garbage connections on cancellation in `AsyncConnectionPool`. 
([#983](https://github.com/pydantic/httpx2/pull/983))
+* Read `_expire_at` only once in `has_expired()`. 
([#1045](https://github.com/pydantic/httpx2/pull/1045))
+
 ## 2.5.0 (June 25th, 2026)
 
 ### Fixed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/README.md 
new/httpcore2-2.12.0/README.md
--- old/httpcore2-2.5.0/README.md       2020-02-02 01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/README.md      2020-02-02 01:00:00.000000000 +0100
@@ -1,11 +1,11 @@
-# HTTP Core
+# HTTPCore 2
 
-[![Test 
Suite](https://github.com/encode/httpcore/workflows/Test%20Suite/badge.svg)](https://github.com/encode/httpcore/actions)
-[![Package 
version](https://badge.fury.io/py/httpcore.svg)](https://pypi.org/project/httpcore/)
+[![Test 
Suite](https://github.com/pydantic/httpx2/workflows/Test%20Suite/badge.svg)](https://github.com/pydantic/httpx2/actions)
+[![Package 
version](https://badge.fury.io/py/httpcore2.svg)](https://pypi.org/project/httpcore2/)
 
 > *Do one thing, and do it well.*
 
-The HTTP Core package provides a minimal low-level HTTP client, which does
+The HTTPCore 2 package provides a minimal low-level HTTP client, which does
 one thing only. Sending HTTP requests.
 
 It does not provide any high level model abstractions over the API,
@@ -14,7 +14,7 @@
 content or charset decoding, handling JSON, environment based configuration
 defaults, or any of that Jazz.
 
-Some things HTTP Core does do:
+Some things HTTPCore 2 does do:
 
 * Sending HTTP requests.
 * Thread-safe / task-safe connection pooling.
@@ -28,13 +28,13 @@
 For HTTP/1.1 only support, install with:
 
 ```shell
-pip install httpcore
+pip install httpcore2
 ```
 
 There are also a number of optional extras available...
 
 ```shell
-pip install httpcore['asyncio,trio,http2,socks']
+pip install 'httpcore2[asyncio,trio,http2,socks]'
 ```
 
 ## Sending requests
@@ -42,9 +42,9 @@
 Send an HTTP request:
 
 ```python
-import httpcore
+import httpcore2
 
-response = httpcore.request("GET", "https://www.example.com/";)
+response = httpcore2.request("GET", "https://www.example.com/";)
 
 print(response)
 # <Response [200]>
@@ -56,25 +56,25 @@
 # b'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>\n\n<meta 
charset="utf-8"/>\n ...'
 ```
 
-The top-level `httpcore.request()` function is provided for convenience. In 
practice whenever you're working with `httpcore` you'll want to use the 
connection pooling functionality that it provides.
+The top-level `httpcore2.request()` function is provided for convenience. In 
practice whenever you're working with `httpcore2` you'll want to use the 
connection pooling functionality that it provides.
 
 ```python
-import httpcore
+import httpcore2
 
-http = httpcore.ConnectionPool()
+http = httpcore2.ConnectionPool()
 response = http.request("GET", "https://www.example.com/";)
 ```
 
-Once you're ready to get going, [head over to the 
documentation](https://www.encode.io/httpcore/).
+Once you're ready to get going, [head over to the 
documentation](https://github.com/pydantic/httpx2/tree/main/src/httpcore2/docs).
 
 ## Motivation
 
-You *probably* don't want to be using HTTP Core directly. It might make sense 
if
+You *probably* don't want to be using HTTPCore 2 directly. It might make sense 
if
 you're writing something like a proxy service in Python, and you just want
 something at the lowest possible level, but more typically you'll want to use
-a higher level client library, such as `httpx`.
+a higher level client library, such as `httpx2`.
 
-The motivation for `httpcore` is:
+The motivation for `httpcore2` is:
 
 * To provide a reusable low-level client library, that other packages can then 
build on top of.
 * To provide a *really clear interface split* between the networking code and 
client logic,
@@ -82,17 +82,17 @@
 
 ## Dependencies
 
-The `httpcore` package has the following dependencies...
+The `httpcore2` package has the following dependencies...
 
 * `h11`
 * `truststore`
 
 And the following optional extras...
 
-* `anyio` - Required by `pip install httpcore['asyncio']`.
-* `trio` - Required by `pip install httpcore['trio']`.
-* `h2` - Required by `pip install httpcore['http2']`.
-* `socksio` - Required by `pip install httpcore['socks']`.
+* `anyio` - Required by `pip install 'httpcore2[asyncio]'`.
+* `trio` - Required by `pip install 'httpcore2[trio]'`.
+* `h2` - Required by `pip install 'httpcore2[http2]'`.
+* `socksio` - Required by `pip install 'httpcore2[socks]'`.
 
 ## Versioning
 
@@ -102,6 +102,6 @@
 
 We recommend pinning your requirements either the most current major version, 
or a more specific version range:
 
-```python
-pip install 'httpcore==1.*'
+```shell
+pip install 'httpcore2==2.*'
 ```
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_async/connection.py 
new/httpcore2-2.12.0/httpcore2/_async/connection.py
--- old/httpcore2-2.5.0/httpcore2/_async/connection.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_async/connection.py 2020-02-02 
01:00:00.000000000 +0100
@@ -156,6 +156,9 @@
             async with Trace("close", logger, None, {}):
                 await self._connection.aclose()
 
+    def is_connected(self) -> bool:
+        return self._connection is not None and self._connection.is_connected()
+
     def is_available(self) -> bool:
         if self._connection is None:
             # If HTTP/2 support is enabled, and the resulting connection could
@@ -174,6 +177,9 @@
             return self._connect_failed
         return self._connection.is_idle()
 
+    def can_multiplex(self) -> bool:
+        return self._connection is not None and 
self._connection.can_multiplex()
+
     def is_closed(self) -> bool:
         if self._connection is None:
             return self._connect_failed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_async/connection_pool.py 
new/httpcore2-2.12.0/httpcore2/_async/connection_pool.py
--- old/httpcore2-2.5.0/httpcore2/_async/connection_pool.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_async/connection_pool.py    2020-02-02 
01:00:00.000000000 +0100
@@ -264,25 +264,40 @@
         closing_connections: list[AsyncConnectionInterface] = []
         retained_connections: list[AsyncConnectionInterface] = []
 
+        # Connections currently referenced by an in-flight request, including
+        # connections that are in the process of being established and idle
+        # connections reserved by an assigned-but-not-yet-sent request.
+        request_connections = {r.connection for r in self._requests}
+
         # First we handle cleaning up any connections that are closed
-        # or have expired their keep-alive, in a single pass.
+        # or have expired their keep-alive, in a single pass. Reserved
+        # connections skip the expiry check: they were checked when assigned,
+        # and `has_expired()` on an idle connection probes the socket.
         for connection in self._connections:
+            reserved = connection in request_connections
             if connection.is_closed():
                 continue
-            elif connection.has_expired():
+            elif not (connection.is_connected() or reserved):
+                # Garbage: a NEW-state connection whose request was cancelled
+                # before the TCP handshake completed.  Drop it without closing
+                # (there is no socket to close yet).
+                continue
+            elif not reserved and connection.has_expired():
                 closing_connections.append(connection)
             else:
                 retained_connections.append(connection)
 
         # Then we close any surplus idle connections, to enforce the
-        # max_keepalive_connections setting.
+        # max_keepalive_connections setting. Reserved connections are not
+        # surplus: a request is about to be sent on them.
         idle_surplus = (
-            sum(connection.is_idle() for connection in retained_connections) - 
self._max_keepalive_connections
+            sum(connection.is_idle() and connection not in request_connections 
for connection in retained_connections)
+            - self._max_keepalive_connections
         )
         if idle_surplus > 0:
             kept: list[AsyncConnectionInterface] = []
             for connection in retained_connections:
-                if idle_surplus > 0 and connection.is_idle():
+                if idle_surplus > 0 and connection.is_idle() and connection 
not in request_connections:
                     closing_connections.append(connection)
                     idle_surplus -= 1
                 else:
@@ -294,11 +309,27 @@
         # Snapshot the set of reusable connections once, rather than rebuilding
         # it per queued request — this is what brings the loop from O(N*M) to
         # O(N+M) in the common case.
-        available_connections = [connection for connection in 
self._connections if connection.is_available()]
+        #
+        # An idle connection already assigned to an in-flight request is
+        # reserved: it stays IDLE until the winning task sends on it, so
+        # without this exclusion the next pass would assign it again and the
+        # loser would churn through `ConnectionNotAvailable`. Multiplexing
+        # connections are exempt: they can take further requests while idle.
+        available_connections = [
+            connection
+            for connection in self._connections
+            if connection.is_available()
+            and not (connection.is_idle() and connection in 
request_connections and not connection.can_multiplex())
+        ]
         new_connection_budget = self._max_connections - len(self._connections)
 
-        # Assign queued requests to connections.
+        # Assign queued requests to connections. Once no connection is
+        # available and no new connection may be created, no queued request
+        # can be assigned, so the scan stops early: this keeps a pass on a
+        # saturated pool O(connections) rather than O(in-flight requests).
         for pool_request in self._requests:
+            if not available_connections and new_connection_budget <= 0:
+                break
             if not pool_request.is_queued():
                 continue
             origin = pool_request.request.url.origin
@@ -309,9 +340,13 @@
             # 2. We can create a new connection to handle the request.
             # 3. We can close an idle connection and then create a new 
connection
             #    to handle the request.
-            for connection in available_connections:
+            for idx, connection in enumerate(available_connections):
                 if connection.can_handle_request(origin):
                     pool_request.assign_to_connection(connection)
+                    if connection.is_idle() and not connection.can_multiplex():
+                        # An idle HTTP/1.1 connection can only take this
+                        # single request until it is released.
+                        del available_connections[idx]
                     break
             else:
                 if new_connection_budget > 0:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_async/http11.py 
new/httpcore2-2.12.0/httpcore2/_async/http11.py
--- old/httpcore2-2.5.0/httpcore2/_async/http11.py      2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_async/http11.py     2020-02-02 
01:00:00.000000000 +0100
@@ -242,6 +242,9 @@
     def can_handle_request(self, origin: Origin) -> bool:
         return origin == self._origin
 
+    def is_connected(self) -> bool:
+        return not self.is_closed()
+
     def is_available(self) -> bool:
         # Note that HTTP/1.1 connections in the "NEW" state are not treated as
         # being "available". The control flow which created the connection will
@@ -251,7 +254,10 @@
 
     def has_expired(self) -> bool:
         now = time.monotonic()
-        keepalive_expired = self._expire_at is not None and now > 
self._expire_at
+        # Read `_expire_at` once into a local: on free-threaded builds another
+        # thread may reset it to `None` between the check and the comparison.
+        expire_at = self._expire_at
+        keepalive_expired = expire_at is not None and now > expire_at
 
         # If the HTTP connection is idle but the socket is readable, then the
         # only valid state is that the socket is about to return b"", 
indicating
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_async/http2.py 
new/httpcore2-2.12.0/httpcore2/_async/http2.py
--- old/httpcore2-2.5.0/httpcore2/_async/http2.py       2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_async/http2.py      2020-02-02 
01:00:00.000000000 +0100
@@ -246,10 +246,11 @@
         """
         Send a single chunk of data in one or more data frames.
         """
-        while data:
+        position = 0
+        while position < len(data):
             max_flow = await self._wait_for_outgoing_flow(request, stream_id)
-            chunk_size = min(len(data), max_flow)
-            chunk, data = data[:chunk_size], data[chunk_size:]
+            chunk = data[position : position + max_flow]
+            position += len(chunk)
             self._h2_state.send_data(stream_id, chunk)
             await self._write_outgoing_data(request)
 
@@ -373,9 +374,10 @@
                     self._max_streams -= 1
 
     async def _response_closed(self, stream_id: int) -> None:
-        await self._max_streams_semaphore.release()
         async with self._state_lock:
-            del self._events[stream_id]
+            if stream_id in self._events:
+                await self._max_streams_semaphore.release()
+                del self._events[stream_id]
             if self._connection_terminated and not self._events:
                 await self.aclose()
 
@@ -474,6 +476,9 @@
     def can_handle_request(self, origin: Origin) -> bool:
         return origin == self._origin
 
+    def is_connected(self) -> bool:
+        return not self.is_closed()
+
     def is_available(self) -> bool:
         return (
             self._state != HTTPConnectionState.CLOSED
@@ -484,11 +489,17 @@
 
     def has_expired(self) -> bool:
         now = time.monotonic()
-        return self._expire_at is not None and now > self._expire_at
+        # Read `_expire_at` once into a local: on free-threaded builds another
+        # thread may reset it to `None` between the check and the comparison.
+        expire_at = self._expire_at
+        return expire_at is not None and now > expire_at
 
     def is_idle(self) -> bool:
         return self._state == HTTPConnectionState.IDLE
 
+    def can_multiplex(self) -> bool:
+        return True
+
     def is_closed(self) -> bool:
         return self._state == HTTPConnectionState.CLOSED
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_async/http_proxy.py 
new/httpcore2-2.12.0/httpcore2/_async/http_proxy.py
--- old/httpcore2-2.5.0/httpcore2/_async/http_proxy.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_async/http_proxy.py 2020-02-02 
01:00:00.000000000 +0100
@@ -204,6 +204,9 @@
     def info(self) -> str:
         return self._connection.info()
 
+    def is_connected(self) -> bool:
+        return self._connection.is_connected()
+
     def is_available(self) -> bool:
         return self._connection.is_available()
 
@@ -330,6 +333,9 @@
     def info(self) -> str:
         return self._connection.info()
 
+    def is_connected(self) -> bool:
+        return self._connection.is_connected()
+
     def is_available(self) -> bool:
         return self._connection.is_available()
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_async/interfaces.py 
new/httpcore2-2.12.0/httpcore2/_async/interfaces.py
--- old/httpcore2-2.5.0/httpcore2/_async/interfaces.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_async/interfaces.py 2020-02-02 
01:00:00.000000000 +0100
@@ -95,6 +95,18 @@
     def can_handle_request(self, origin: Origin) -> bool:
         raise NotImplementedError()  # pragma: no cover
 
+    def is_connected(self) -> bool:
+        """
+        Return `True` if the connection is open (the underlying socket has been
+        established).  A connection in the NEW state (just created but not yet
+        connected) returns `False`.
+
+        Note: for some implementations `is_connected() != not is_closed()`.
+        The default implementation returns `not self.is_closed()`, which is
+        correct for connections that are never in the NEW (pre-TCP) state.
+        """
+        return not self.is_closed()  # pragma: no cover
+
     def is_available(self) -> bool:
         """
         Return `True` if the connection is currently able to accept an
@@ -128,6 +140,16 @@
         """
         raise NotImplementedError()  # pragma: no cover
 
+    def can_multiplex(self) -> bool:
+        """
+        Return `True` if the connection can serve multiple requests
+        concurrently, such as an established HTTP/2 connection.
+
+        The default covers HTTP/1.1-style implementations, which serve a
+        single request at a time.
+        """
+        return False
+
     def is_closed(self) -> bool:
         """
         Return `True` if the connection has been closed.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_async/socks_proxy.py 
new/httpcore2-2.12.0/httpcore2/_async/socks_proxy.py
--- old/httpcore2-2.5.0/httpcore2/_async/socks_proxy.py 2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_async/socks_proxy.py        2020-02-02 
01:00:00.000000000 +0100
@@ -240,7 +240,7 @@
                         trace.return_value = stream
 
                     # Upgrade the stream to SSL
-                    if self._remote_origin.scheme == b"https":
+                    if self._remote_origin.scheme in (b"https", b"wss"):
                         ssl_context = default_ssl_context() if 
self._ssl_context is None else self._ssl_context
                         alpn_protocols = ["http/1.1", "h2"] if self._http2 
else ["http/1.1"]
                         ssl_context.set_alpn_protocols(alpn_protocols)
@@ -288,6 +288,9 @@
         if self._connection is not None:
             await self._connection.aclose()
 
+    def is_connected(self) -> bool:
+        return self._connection is not None and self._connection.is_connected()
+
     def is_available(self) -> bool:
         if self._connection is None:  # pragma: no cover
             # If HTTP/2 support is enabled, and the resulting connection could
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_sync/connection.py 
new/httpcore2-2.12.0/httpcore2/_sync/connection.py
--- old/httpcore2-2.5.0/httpcore2/_sync/connection.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_sync/connection.py  2020-02-02 
01:00:00.000000000 +0100
@@ -156,6 +156,9 @@
             with Trace("close", logger, None, {}):
                 self._connection.close()
 
+    def is_connected(self) -> bool:
+        return self._connection is not None and self._connection.is_connected()
+
     def is_available(self) -> bool:
         if self._connection is None:
             # If HTTP/2 support is enabled, and the resulting connection could
@@ -174,6 +177,9 @@
             return self._connect_failed
         return self._connection.is_idle()
 
+    def can_multiplex(self) -> bool:
+        return self._connection is not None and 
self._connection.can_multiplex()
+
     def is_closed(self) -> bool:
         if self._connection is None:
             return self._connect_failed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_sync/connection_pool.py 
new/httpcore2-2.12.0/httpcore2/_sync/connection_pool.py
--- old/httpcore2-2.5.0/httpcore2/_sync/connection_pool.py      2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_sync/connection_pool.py     2020-02-02 
01:00:00.000000000 +0100
@@ -264,25 +264,40 @@
         closing_connections: list[ConnectionInterface] = []
         retained_connections: list[ConnectionInterface] = []
 
+        # Connections currently referenced by an in-flight request, including
+        # connections that are in the process of being established and idle
+        # connections reserved by an assigned-but-not-yet-sent request.
+        request_connections = {r.connection for r in self._requests}
+
         # First we handle cleaning up any connections that are closed
-        # or have expired their keep-alive, in a single pass.
+        # or have expired their keep-alive, in a single pass. Reserved
+        # connections skip the expiry check: they were checked when assigned,
+        # and `has_expired()` on an idle connection probes the socket.
         for connection in self._connections:
+            reserved = connection in request_connections
             if connection.is_closed():
                 continue
-            elif connection.has_expired():
+            elif not (connection.is_connected() or reserved):
+                # Garbage: a NEW-state connection whose request was cancelled
+                # before the TCP handshake completed.  Drop it without closing
+                # (there is no socket to close yet).
+                continue
+            elif not reserved and connection.has_expired():
                 closing_connections.append(connection)
             else:
                 retained_connections.append(connection)
 
         # Then we close any surplus idle connections, to enforce the
-        # max_keepalive_connections setting.
+        # max_keepalive_connections setting. Reserved connections are not
+        # surplus: a request is about to be sent on them.
         idle_surplus = (
-            sum(connection.is_idle() for connection in retained_connections) - 
self._max_keepalive_connections
+            sum(connection.is_idle() and connection not in request_connections 
for connection in retained_connections)
+            - self._max_keepalive_connections
         )
         if idle_surplus > 0:
             kept: list[ConnectionInterface] = []
             for connection in retained_connections:
-                if idle_surplus > 0 and connection.is_idle():
+                if idle_surplus > 0 and connection.is_idle() and connection 
not in request_connections:
                     closing_connections.append(connection)
                     idle_surplus -= 1
                 else:
@@ -294,11 +309,27 @@
         # Snapshot the set of reusable connections once, rather than rebuilding
         # it per queued request — this is what brings the loop from O(N*M) to
         # O(N+M) in the common case.
-        available_connections = [connection for connection in 
self._connections if connection.is_available()]
+        #
+        # An idle connection already assigned to an in-flight request is
+        # reserved: it stays IDLE until the winning task sends on it, so
+        # without this exclusion the next pass would assign it again and the
+        # loser would churn through `ConnectionNotAvailable`. Multiplexing
+        # connections are exempt: they can take further requests while idle.
+        available_connections = [
+            connection
+            for connection in self._connections
+            if connection.is_available()
+            and not (connection.is_idle() and connection in 
request_connections and not connection.can_multiplex())
+        ]
         new_connection_budget = self._max_connections - len(self._connections)
 
-        # Assign queued requests to connections.
+        # Assign queued requests to connections. Once no connection is
+        # available and no new connection may be created, no queued request
+        # can be assigned, so the scan stops early: this keeps a pass on a
+        # saturated pool O(connections) rather than O(in-flight requests).
         for pool_request in self._requests:
+            if not available_connections and new_connection_budget <= 0:
+                break
             if not pool_request.is_queued():
                 continue
             origin = pool_request.request.url.origin
@@ -309,9 +340,13 @@
             # 2. We can create a new connection to handle the request.
             # 3. We can close an idle connection and then create a new 
connection
             #    to handle the request.
-            for connection in available_connections:
+            for idx, connection in enumerate(available_connections):
                 if connection.can_handle_request(origin):
                     pool_request.assign_to_connection(connection)
+                    if connection.is_idle() and not connection.can_multiplex():
+                        # An idle HTTP/1.1 connection can only take this
+                        # single request until it is released.
+                        del available_connections[idx]
                     break
             else:
                 if new_connection_budget > 0:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_sync/http11.py 
new/httpcore2-2.12.0/httpcore2/_sync/http11.py
--- old/httpcore2-2.5.0/httpcore2/_sync/http11.py       2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_sync/http11.py      2020-02-02 
01:00:00.000000000 +0100
@@ -242,6 +242,9 @@
     def can_handle_request(self, origin: Origin) -> bool:
         return origin == self._origin
 
+    def is_connected(self) -> bool:
+        return not self.is_closed()
+
     def is_available(self) -> bool:
         # Note that HTTP/1.1 connections in the "NEW" state are not treated as
         # being "available". The control flow which created the connection will
@@ -251,7 +254,10 @@
 
     def has_expired(self) -> bool:
         now = time.monotonic()
-        keepalive_expired = self._expire_at is not None and now > 
self._expire_at
+        # Read `_expire_at` once into a local: on free-threaded builds another
+        # thread may reset it to `None` between the check and the comparison.
+        expire_at = self._expire_at
+        keepalive_expired = expire_at is not None and now > expire_at
 
         # If the HTTP connection is idle but the socket is readable, then the
         # only valid state is that the socket is about to return b"", 
indicating
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_sync/http2.py 
new/httpcore2-2.12.0/httpcore2/_sync/http2.py
--- old/httpcore2-2.5.0/httpcore2/_sync/http2.py        2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_sync/http2.py       2020-02-02 
01:00:00.000000000 +0100
@@ -246,10 +246,11 @@
         """
         Send a single chunk of data in one or more data frames.
         """
-        while data:
+        position = 0
+        while position < len(data):
             max_flow = self._wait_for_outgoing_flow(request, stream_id)
-            chunk_size = min(len(data), max_flow)
-            chunk, data = data[:chunk_size], data[chunk_size:]
+            chunk = data[position : position + max_flow]
+            position += len(chunk)
             self._h2_state.send_data(stream_id, chunk)
             self._write_outgoing_data(request)
 
@@ -373,9 +374,10 @@
                     self._max_streams -= 1
 
     def _response_closed(self, stream_id: int) -> None:
-        self._max_streams_semaphore.release()
         with self._state_lock:
-            del self._events[stream_id]
+            if stream_id in self._events:
+                self._max_streams_semaphore.release()
+                del self._events[stream_id]
             if self._connection_terminated and not self._events:
                 self.close()
 
@@ -474,6 +476,9 @@
     def can_handle_request(self, origin: Origin) -> bool:
         return origin == self._origin
 
+    def is_connected(self) -> bool:
+        return not self.is_closed()
+
     def is_available(self) -> bool:
         return (
             self._state != HTTPConnectionState.CLOSED
@@ -484,11 +489,17 @@
 
     def has_expired(self) -> bool:
         now = time.monotonic()
-        return self._expire_at is not None and now > self._expire_at
+        # Read `_expire_at` once into a local: on free-threaded builds another
+        # thread may reset it to `None` between the check and the comparison.
+        expire_at = self._expire_at
+        return expire_at is not None and now > expire_at
 
     def is_idle(self) -> bool:
         return self._state == HTTPConnectionState.IDLE
 
+    def can_multiplex(self) -> bool:
+        return True
+
     def is_closed(self) -> bool:
         return self._state == HTTPConnectionState.CLOSED
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_sync/http_proxy.py 
new/httpcore2-2.12.0/httpcore2/_sync/http_proxy.py
--- old/httpcore2-2.5.0/httpcore2/_sync/http_proxy.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_sync/http_proxy.py  2020-02-02 
01:00:00.000000000 +0100
@@ -204,6 +204,9 @@
     def info(self) -> str:
         return self._connection.info()
 
+    def is_connected(self) -> bool:
+        return self._connection.is_connected()
+
     def is_available(self) -> bool:
         return self._connection.is_available()
 
@@ -330,6 +333,9 @@
     def info(self) -> str:
         return self._connection.info()
 
+    def is_connected(self) -> bool:
+        return self._connection.is_connected()
+
     def is_available(self) -> bool:
         return self._connection.is_available()
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_sync/interfaces.py 
new/httpcore2-2.12.0/httpcore2/_sync/interfaces.py
--- old/httpcore2-2.5.0/httpcore2/_sync/interfaces.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_sync/interfaces.py  2020-02-02 
01:00:00.000000000 +0100
@@ -95,6 +95,18 @@
     def can_handle_request(self, origin: Origin) -> bool:
         raise NotImplementedError()  # pragma: no cover
 
+    def is_connected(self) -> bool:
+        """
+        Return `True` if the connection is open (the underlying socket has been
+        established).  A connection in the NEW state (just created but not yet
+        connected) returns `False`.
+
+        Note: for some implementations `is_connected() != not is_closed()`.
+        The default implementation returns `not self.is_closed()`, which is
+        correct for connections that are never in the NEW (pre-TCP) state.
+        """
+        return not self.is_closed()  # pragma: no cover
+
     def is_available(self) -> bool:
         """
         Return `True` if the connection is currently able to accept an
@@ -128,6 +140,16 @@
         """
         raise NotImplementedError()  # pragma: no cover
 
+    def can_multiplex(self) -> bool:
+        """
+        Return `True` if the connection can serve multiple requests
+        concurrently, such as an established HTTP/2 connection.
+
+        The default covers HTTP/1.1-style implementations, which serve a
+        single request at a time.
+        """
+        return False
+
     def is_closed(self) -> bool:
         """
         Return `True` if the connection has been closed.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_sync/socks_proxy.py 
new/httpcore2-2.12.0/httpcore2/_sync/socks_proxy.py
--- old/httpcore2-2.5.0/httpcore2/_sync/socks_proxy.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_sync/socks_proxy.py 2020-02-02 
01:00:00.000000000 +0100
@@ -240,7 +240,7 @@
                         trace.return_value = stream
 
                     # Upgrade the stream to SSL
-                    if self._remote_origin.scheme == b"https":
+                    if self._remote_origin.scheme in (b"https", b"wss"):
                         ssl_context = default_ssl_context() if 
self._ssl_context is None else self._ssl_context
                         alpn_protocols = ["http/1.1", "h2"] if self._http2 
else ["http/1.1"]
                         ssl_context.set_alpn_protocols(alpn_protocols)
@@ -288,6 +288,9 @@
         if self._connection is not None:
             self._connection.close()
 
+    def is_connected(self) -> bool:
+        return self._connection is not None and self._connection.is_connected()
+
     def is_available(self) -> bool:
         if self._connection is None:  # pragma: no cover
             # If HTTP/2 support is enabled, and the resulting connection could
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_synchronization.py 
new/httpcore2-2.12.0/httpcore2/_synchronization.py
--- old/httpcore2-2.5.0/httpcore2/_synchronization.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_synchronization.py  2020-02-02 
01:00:00.000000000 +0100
@@ -18,13 +18,16 @@
 except ImportError:  # pragma: no cover
     anyio = None  # type: ignore
 
+try:
+    import sniffio
+except ImportError:  # pragma: no cover
+    sniffio = None  # type: ignore
+
 
 def current_async_library() -> str:
     # Determine if we're running under trio or asyncio.
     # See https://sniffio.readthedocs.io/en/latest/
-    try:
-        import sniffio
-    except ImportError:  # pragma: no cover
+    if sniffio is None:  # pragma: no cover
         environment = "asyncio"
     else:
         environment = sniffio.current_async_library()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpcore2-2.5.0/pyproject.toml 
new/httpcore2-2.12.0/pyproject.toml
--- old/httpcore2-2.5.0/pyproject.toml  2020-02-02 01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
@@ -2,7 +2,7 @@
 requires = [
     "hatchling",
     "hatch-fancy-pypi-readme",
-    "uv-dynamic-versioning>=0.8.0",
+    "uv-dynamic-versioning>=0.14.0",
 ]
 build-backend = "hatchling.build"
 
@@ -41,6 +41,7 @@
     "Programming Language :: Python :: 3.12",
     "Programming Language :: Python :: 3.13",
     "Programming Language :: Python :: 3.14",
+    "Programming Language :: Python :: 3.15",
     "Topic :: Internet :: WWW/HTTP",
 ]
 dependencies = ["truststore>=0.10", "h11>=0.16"]
@@ -48,7 +49,7 @@
 [project.optional-dependencies]
 http2 = ["h2>=3,<5"]
 socks = ["socksio==1.*"]
-trio = ["trio>=0.22.0,<1.0"]
+trio = ["trio>=0.33.0,<1.0"]
 asyncio = ["anyio>=4.5.0,<5.0"]
 
 [project.urls]

Reply via email to