Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-httpx2 for openSUSE:Factory 
checked in at 2026-07-06 12:27:15
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-httpx2 (Old)
 and      /work/SRC/openSUSE:Factory/.python-httpx2.new.1982 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-httpx2"

Mon Jul  6 12:27:15 2026 rev:3 rq:1362975 version:2.5.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-httpx2/python-httpx2.changes      
2026-06-18 18:38:37.872042446 +0200
+++ /work/SRC/openSUSE:Factory/.python-httpx2.new.1982/python-httpx2.changes    
2026-07-06 12:27:31.597385674 +0200
@@ -1,0 +2,6 @@
+Wed Jul  1 12:02:35 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 2.5.0:
+  * Propagate the timeout through the SOCKS5 handshake.
+
+-------------------------------------------------------------------

Old:
----
  httpx2-2.4.0.tar.gz

New:
----
  httpx2-2.5.0.tar.gz

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

Other differences:
------------------
++++++ python-httpx2.spec ++++++
--- /var/tmp/diff_new_pack.6R3Kvk/_old  2026-07-06 12:27:32.393413283 +0200
+++ /var/tmp/diff_new_pack.6R3Kvk/_new  2026-07-06 12:27:32.397413422 +0200
@@ -24,7 +24,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-httpx2
-Version:        2.4.0
+Version:        2.5.0
 Release:        0
 Summary:        The next generation HTTP client
 License:        BSD-3-Clause

++++++ httpx2-2.4.0.tar.gz -> httpx2-2.5.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpx2-2.4.0/docs/api.md new/httpx2-2.5.0/docs/api.md
--- old/httpx2-2.4.0/docs/api.md        2026-06-11 08:29:52.000000000 +0200
+++ new/httpx2-2.5.0/docs/api.md        2026-06-25 16:14:41.000000000 +0200
@@ -44,6 +44,7 @@
         - patch
         - delete
         - stream
+        - sse
         - build_request
         - send
         - close
@@ -66,6 +67,7 @@
         - patch
         - delete
         - stream
+        - sse
         - build_request
         - send
         - aclose
@@ -197,3 +199,21 @@
 * `.auth` - **tuple[str, str]**
 * `.headers` - **Headers**
 * `.ssl_context` - **SSLContext**
+
+## `EventSource`
+
+::: httpx2.EventSource
+    options:
+      members:
+        - response
+
+## `ServerSentEvent`
+
+::: httpx2.ServerSentEvent
+    options:
+      members:
+        - event
+        - data
+        - id
+        - retry
+        - json
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpx2-2.4.0/docs/exceptions.md 
new/httpx2-2.5.0/docs/exceptions.md
--- old/httpx2-2.4.0/docs/exceptions.md 2026-06-11 08:29:52.000000000 +0200
+++ new/httpx2-2.5.0/docs/exceptions.md 2026-06-25 16:14:41.000000000 +0200
@@ -24,6 +24,7 @@
                 * RemoteProtocolError
             * ProxyError
             * UnsupportedProtocol
+            * SSEError
         * DecodingError
         * TooManyRedirects
     * HTTPStatusError
@@ -75,6 +76,8 @@
 
 ::: httpx2.UnsupportedProtocol
 
+::: httpx2.SSEError
+
 ::: httpx2.DecodingError
 
 ::: httpx2.TooManyRedirects
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpx2-2.4.0/docs/sse.md new/httpx2-2.5.0/docs/sse.md
--- old/httpx2-2.4.0/docs/sse.md        1970-01-01 01:00:00.000000000 +0100
+++ new/httpx2-2.5.0/docs/sse.md        2026-06-25 16:14:41.000000000 +0200
@@ -0,0 +1,77 @@
+# Server-Sent Events
+
+[Server-sent events][mdn] (SSE) let a server push a stream of events to the 
client over a single long-lived HTTP response with the `text/event-stream` 
content type. HTTPX has native support for consuming them through 
`client.sse()`.
+
+## Consuming events
+
+`client.sse()` is a context manager that yields an `EventSource`. Iterating 
the `EventSource` decodes the stream and yields a `ServerSentEvent` for each 
event:
+
+```pycon
+>>> with httpx2.Client() as client:
+...     with client.sse("https://example.com/sse";) as source:
+...         for event in source:
+...             print(event.event, event.data)
+```
+
+It works the same way with the async client, using `async with` and `async 
for`:
+
+```pycon
+>>> async with httpx2.AsyncClient() as client:
+...     async with client.sse("https://example.com/sse";) as source:
+...         async for event in source:
+...             print(event.event, event.data)
+```
+
+`sse()` issues a `GET` request by default. Some APIs stream events in response 
to a `POST` - pass `method="POST"` along with the usual request arguments:
+
+```pycon
+>>> with client.sse("https://example.com/sse";, method="POST", json={"query": 
"..."}) as source:
+...     for event in source:
+...         print(event.data)
+```
+
+The `Accept: text/event-stream` and `Cache-Control: no-store` headers are set 
for you; any headers you pass take precedence.
+
+## The `ServerSentEvent`
+
+Each event exposes the fields defined by the SSE specification:
+
+| Attribute | Description |
+| --------- | ----------- |
+| `event`   | The event type. Defaults to `"message"`. |
+| `data`    | The event payload. Multiple `data:` lines are joined with `\n`. |
+| `id`      | The last event ID, which persists across events until changed. |
+| `retry`   | The reconnection time in milliseconds, or `None`. |
+
+When the payload is JSON, `event.json()` decodes `event.data` for you:
+
+```pycon
+>>> with client.sse("https://example.com/sse";) as source:
+...     for event in source:
+...         print(event.json())
+```
+
+## Accessing the response
+
+The underlying [`Response`](api.md#response) is available as 
`source.response`, which is useful for inspecting the status code or headers 
before iterating:
+
+```pycon
+>>> with client.sse("https://example.com/sse";) as source:
+...     source.response.raise_for_status()
+...     for event in source:
+...         print(event.data)
+```
+
+## Error handling
+
+If the response does not have a `text/event-stream` content type, iterating 
the `EventSource` raises `SSEError`:
+
+```pycon
+>>> with client.sse("https://example.com/not-an-event-stream";) as source:
+...     for event in source:  # raises httpx2.SSEError
+...         ...
+```
+
+`SSEError` is a subclass of [`TransportError`](exceptions.md), so it is also 
caught by `except httpx2.TransportError`.
+
+[mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpx2-2.4.0/mkdocs.yml new/httpx2-2.5.0/mkdocs.yml
--- old/httpx2-2.4.0/mkdocs.yml 2026-06-11 08:29:52.000000000 +0200
+++ new/httpx2-2.5.0/mkdocs.yml 2026-06-25 16:14:41.000000000 +0200
@@ -38,6 +38,7 @@
         - Async Support: 'async.md'
         - HTTP/2 Support: 'http2.md'
         - Logging: 'logging.md'
+        - Server-Sent Events: 'sse.md'
         - Requests Compatibility: 'compatibility.md'
         - Troubleshooting: 'troubleshooting.md'
     - API Reference:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpx2-2.4.0/src/httpcore2/CHANGELOG.md 
new/httpx2-2.5.0/src/httpcore2/CHANGELOG.md
--- old/httpx2-2.4.0/src/httpcore2/CHANGELOG.md 2026-06-11 08:29:52.000000000 
+0200
+++ new/httpx2-2.5.0/src/httpcore2/CHANGELOG.md 2026-06-25 16:14:41.000000000 
+0200
@@ -4,6 +4,12 @@
 
 The format is based on [Keep a 
Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## 2.5.0 (June 25th, 2026)
+
+### Fixed
+
+* Propagate the timeout through the SOCKS5 handshake. 
([#1009](https://github.com/pydantic/httpx2/pull/1009))
+
 ## 2.4.0 (June 11th, 2026)
 
 ### Fixed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/httpx2-2.4.0/src/httpcore2/httpcore2/_async/socks_proxy.py 
new/httpx2-2.5.0/src/httpcore2/httpcore2/_async/socks_proxy.py
--- old/httpx2-2.4.0/src/httpcore2/httpcore2/_async/socks_proxy.py      
2026-06-11 08:29:52.000000000 +0200
+++ new/httpx2-2.5.0/src/httpcore2/httpcore2/_async/socks_proxy.py      
2026-06-25 16:14:41.000000000 +0200
@@ -45,7 +45,11 @@
     host: bytes,
     port: int,
     auth: tuple[bytes, bytes] | None = None,
+    timeouts: dict[str, float | None] | None = None,
 ) -> None:
+    timeouts = timeouts or {}
+    write_timeout = timeouts.get("write", None)
+    read_timeout = timeouts.get("read", None)
     conn = socksio.socks5.SOCKS5Connection()
 
     # Auth method request
@@ -56,10 +60,10 @@
     )
     conn.send(socksio.socks5.SOCKS5AuthMethodsRequest([auth_method]))
     outgoing_bytes = conn.data_to_send()
-    await stream.write(outgoing_bytes)
+    await stream.write(outgoing_bytes, timeout=write_timeout)
 
     # Auth method response
-    incoming_bytes = await stream.read(max_bytes=4096)
+    incoming_bytes = await stream.read(max_bytes=4096, timeout=read_timeout)
     response = conn.receive_data(incoming_bytes)
     assert isinstance(response, socksio.socks5.SOCKS5AuthReply)
     if response.method != auth_method:
@@ -73,10 +77,10 @@
         username, password = auth
         conn.send(socksio.socks5.SOCKS5UsernamePasswordRequest(username, 
password))
         outgoing_bytes = conn.data_to_send()
-        await stream.write(outgoing_bytes)
+        await stream.write(outgoing_bytes, timeout=write_timeout)
 
         # Username/password response
-        incoming_bytes = await stream.read(max_bytes=4096)
+        incoming_bytes = await stream.read(max_bytes=4096, 
timeout=read_timeout)
         response = conn.receive_data(incoming_bytes)
         assert isinstance(response, socksio.socks5.SOCKS5UsernamePasswordReply)
         if not response.success:
@@ -85,10 +89,10 @@
     # Connect request
     
conn.send(socksio.socks5.SOCKS5CommandRequest.from_address(socksio.socks5.SOCKS5Command.CONNECT,
 (host, port)))
     outgoing_bytes = conn.data_to_send()
-    await stream.write(outgoing_bytes)
+    await stream.write(outgoing_bytes, timeout=write_timeout)
 
     # Connect response
-    incoming_bytes = await stream.read(max_bytes=4096)
+    incoming_bytes = await stream.read(max_bytes=4096, timeout=read_timeout)
     response = conn.receive_data(incoming_bytes)
     assert isinstance(response, socksio.socks5.SOCKS5Reply)
     if response.reply_code != socksio.socks5.SOCKS5ReplyCode.SUCCEEDED:
@@ -229,6 +233,7 @@
                         "host": self._remote_origin.host.decode("ascii"),
                         "port": self._remote_origin.port,
                         "auth": self._proxy_auth,
+                        "timeouts": timeouts,
                     }
                     async with Trace("setup_socks5_connection", logger, 
request, kwargs) as trace:
                         await _init_socks5_connection(**kwargs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/httpx2-2.4.0/src/httpcore2/httpcore2/_sync/socks_proxy.py 
new/httpx2-2.5.0/src/httpcore2/httpcore2/_sync/socks_proxy.py
--- old/httpx2-2.4.0/src/httpcore2/httpcore2/_sync/socks_proxy.py       
2026-06-11 08:29:52.000000000 +0200
+++ new/httpx2-2.5.0/src/httpcore2/httpcore2/_sync/socks_proxy.py       
2026-06-25 16:14:41.000000000 +0200
@@ -45,7 +45,11 @@
     host: bytes,
     port: int,
     auth: tuple[bytes, bytes] | None = None,
+    timeouts: dict[str, float | None] | None = None,
 ) -> None:
+    timeouts = timeouts or {}
+    write_timeout = timeouts.get("write", None)
+    read_timeout = timeouts.get("read", None)
     conn = socksio.socks5.SOCKS5Connection()
 
     # Auth method request
@@ -56,10 +60,10 @@
     )
     conn.send(socksio.socks5.SOCKS5AuthMethodsRequest([auth_method]))
     outgoing_bytes = conn.data_to_send()
-    stream.write(outgoing_bytes)
+    stream.write(outgoing_bytes, timeout=write_timeout)
 
     # Auth method response
-    incoming_bytes = stream.read(max_bytes=4096)
+    incoming_bytes = stream.read(max_bytes=4096, timeout=read_timeout)
     response = conn.receive_data(incoming_bytes)
     assert isinstance(response, socksio.socks5.SOCKS5AuthReply)
     if response.method != auth_method:
@@ -73,10 +77,10 @@
         username, password = auth
         conn.send(socksio.socks5.SOCKS5UsernamePasswordRequest(username, 
password))
         outgoing_bytes = conn.data_to_send()
-        stream.write(outgoing_bytes)
+        stream.write(outgoing_bytes, timeout=write_timeout)
 
         # Username/password response
-        incoming_bytes = stream.read(max_bytes=4096)
+        incoming_bytes = stream.read(max_bytes=4096, timeout=read_timeout)
         response = conn.receive_data(incoming_bytes)
         assert isinstance(response, socksio.socks5.SOCKS5UsernamePasswordReply)
         if not response.success:
@@ -85,10 +89,10 @@
     # Connect request
     
conn.send(socksio.socks5.SOCKS5CommandRequest.from_address(socksio.socks5.SOCKS5Command.CONNECT,
 (host, port)))
     outgoing_bytes = conn.data_to_send()
-    stream.write(outgoing_bytes)
+    stream.write(outgoing_bytes, timeout=write_timeout)
 
     # Connect response
-    incoming_bytes = stream.read(max_bytes=4096)
+    incoming_bytes = stream.read(max_bytes=4096, timeout=read_timeout)
     response = conn.receive_data(incoming_bytes)
     assert isinstance(response, socksio.socks5.SOCKS5Reply)
     if response.reply_code != socksio.socks5.SOCKS5ReplyCode.SUCCEEDED:
@@ -229,6 +233,7 @@
                         "host": self._remote_origin.host.decode("ascii"),
                         "port": self._remote_origin.port,
                         "auth": self._proxy_auth,
+                        "timeouts": timeouts,
                     }
                     with Trace("setup_socks5_connection", logger, request, 
kwargs) as trace:
                         _init_socks5_connection(**kwargs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpx2-2.4.0/src/httpx2/CHANGELOG.md 
new/httpx2-2.5.0/src/httpx2/CHANGELOG.md
--- old/httpx2-2.4.0/src/httpx2/CHANGELOG.md    2026-06-11 08:29:52.000000000 
+0200
+++ new/httpx2-2.5.0/src/httpx2/CHANGELOG.md    2026-06-25 16:14:41.000000000 
+0200
@@ -4,6 +4,17 @@
 
 The format is based on [Keep a 
Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## 2.5.0 (June 25th, 2026)
+
+### Added
+
+* Add native server-sent events support via `client.sse()`. 
([#1046](https://github.com/pydantic/httpx2/pull/1046))
+* Support `|` and `|=` operators for `Headers`. 
([#1047](https://github.com/pydantic/httpx2/pull/1047))
+
+### Fixed
+
+* Allow IPv6 CIDR notation in `no_proxy`. 
([#967](https://github.com/pydantic/httpx2/pull/967))
+
 ## 2.4.0 (June 11th, 2026)
 
 ### Added
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpx2-2.4.0/src/httpx2/httpx2/__init__.py 
new/httpx2-2.5.0/src/httpx2/httpx2/__init__.py
--- old/httpx2-2.4.0/src/httpx2/httpx2/__init__.py      2026-06-11 
08:29:52.000000000 +0200
+++ new/httpx2-2.5.0/src/httpx2/httpx2/__init__.py      2026-06-25 
16:14:41.000000000 +0200
@@ -6,6 +6,7 @@
 from ._content import *
 from ._exceptions import *
 from ._models import *
+from ._sse import *
 from ._status_codes import *
 from ._transports import *
 from ._types import *
@@ -35,6 +36,7 @@
     "DecodingError",
     "delete",
     "DigestAuth",
+    "EventSource",
     "FunctionAuth",
     "get",
     "head",
@@ -66,6 +68,8 @@
     "RequestNotRead",
     "Response",
     "ResponseNotRead",
+    "ServerSentEvent",
+    "SSEError",
     "stream",
     "StreamClosed",
     "StreamConsumed",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpx2-2.4.0/src/httpx2/httpx2/_client.py 
new/httpx2-2.5.0/src/httpx2/httpx2/_client.py
--- old/httpx2-2.4.0/src/httpx2/httpx2/_client.py       2026-06-11 
08:29:52.000000000 +0200
+++ new/httpx2-2.5.0/src/httpx2/httpx2/_client.py       2026-06-25 
16:14:41.000000000 +0200
@@ -28,6 +28,7 @@
     request_context,
 )
 from ._models import Cookies, Headers, Request, Response
+from ._sse import EventSource
 from ._status_codes import codes
 from ._transports.base import AsyncBaseTransport, BaseTransport
 from ._transports.default import AsyncHTTPTransport, HTTPTransport
@@ -845,6 +846,48 @@
         finally:
             response.close()
 
+    @contextmanager
+    def sse(
+        self,
+        url: URL | str,
+        *,
+        method: str = "GET",
+        content: RequestContent | None = None,
+        data: RequestData | None = None,
+        files: RequestFiles | None = None,
+        json: typing.Any | None = None,
+        params: QueryParamTypes | None = None,
+        headers: HeaderTypes | None = None,
+        cookies: CookieTypes | None = None,
+        auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
+        follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+        timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+        extensions: RequestExtensions | None = None,
+    ) -> Generator[EventSource]:
+        """
+        Connect to a server-sent events endpoint and yield an `EventSource`.
+
+        Iterating the `EventSource` yields `ServerSentEvent` instances.
+
+        **Parameters**: See `httpx2.request`.
+        """
+        with self.stream(
+            method,
+            url,
+            content=content,
+            data=data,
+            files=files,
+            json=json,
+            params=params,
+            headers={"Accept": "text/event-stream", "Cache-Control": 
"no-store"} | Headers(headers),
+            cookies=cookies,
+            auth=auth,
+            follow_redirects=follow_redirects,
+            timeout=timeout,
+            extensions=extensions,
+        ) as response:
+            yield EventSource(response)
+
     def send(
         self,
         request: Request,
@@ -1548,6 +1591,48 @@
         finally:
             await response.aclose()
 
+    @asynccontextmanager
+    async def sse(
+        self,
+        url: URL | str,
+        *,
+        method: str = "GET",
+        content: RequestContent | None = None,
+        data: RequestData | None = None,
+        files: RequestFiles | None = None,
+        json: typing.Any | None = None,
+        params: QueryParamTypes | None = None,
+        headers: HeaderTypes | None = None,
+        cookies: CookieTypes | None = None,
+        auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
+        follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+        timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+        extensions: RequestExtensions | None = None,
+    ) -> AsyncGenerator[EventSource]:
+        """
+        Connect to a server-sent events endpoint and yield an `EventSource`.
+
+        Iterating the `EventSource` yields `ServerSentEvent` instances.
+
+        **Parameters**: See `httpx2.request`.
+        """
+        async with self.stream(
+            method,
+            url,
+            content=content,
+            data=data,
+            files=files,
+            json=json,
+            params=params,
+            headers={"Accept": "text/event-stream", "Cache-Control": 
"no-store"} | Headers(headers),
+            cookies=cookies,
+            auth=auth,
+            follow_redirects=follow_redirects,
+            timeout=timeout,
+            extensions=extensions,
+        ) as response:
+            yield EventSource(response)
+
     async def send(
         self,
         request: Request,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpx2-2.4.0/src/httpx2/httpx2/_models.py 
new/httpx2-2.5.0/src/httpx2/httpx2/_models.py
--- old/httpx2-2.4.0/src/httpx2/httpx2/_models.py       2026-06-11 
08:29:52.000000000 +0200
+++ new/httpx2-2.5.0/src/httpx2/httpx2/_models.py       2026-06-25 
16:14:41.000000000 +0200
@@ -277,6 +277,24 @@
     def copy(self) -> Headers:
         return Headers(self, encoding=self.encoding)
 
+    def __or__(self, other: Mapping[str, str]) -> Headers:
+        if not isinstance(other, Mapping):
+            return NotImplemented
+        merged = self.copy()
+        merged.update(other)
+        return merged
+
+    def __ror__(self, other: Mapping[str, str]) -> Headers:
+        if not isinstance(other, Mapping):
+            return NotImplemented
+        merged = Headers(other)
+        merged.update(self)
+        return merged
+
+    def __ior__(self, other: HeaderTypes) -> Headers:
+        self.update(other)
+        return self
+
     def __getitem__(self, key: str) -> str:
         """
         Return a single header value.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpx2-2.4.0/src/httpx2/httpx2/_sse.py 
new/httpx2-2.5.0/src/httpx2/httpx2/_sse.py
--- old/httpx2-2.4.0/src/httpx2/httpx2/_sse.py  1970-01-01 01:00:00.000000000 
+0100
+++ new/httpx2-2.5.0/src/httpx2/httpx2/_sse.py  2026-06-25 16:14:41.000000000 
+0200
@@ -0,0 +1,158 @@
+"""
+Server-sent events support, derived from httpx-sse 
(https://github.com/florimondmanca/httpx-sse).
+
+Copyright (c) 2022 Florimond Manca, MIT License 
(https://github.com/florimondmanca/httpx-sse/blob/master/LICENSE).
+"""
+
+from __future__ import annotations
+
+import json as jsonlib
+from collections.abc import AsyncIterator, Iterator
+from dataclasses import dataclass
+
+from ._exceptions import TransportError
+from ._models import Response
+
+__all__ = ["EventSource", "SSEError", "ServerSentEvent"]
+
+
+class SSEError(TransportError):
+    """
+    An error that occurred while connecting to a server-sent events endpoint.
+    """
+
+
+@dataclass(frozen=True)
+class ServerSentEvent:
+    event: str = "message"
+    data: str = ""
+    id: str = ""
+    retry: int | None = None
+
+    def json(self) -> object:
+        return jsonlib.loads(self.data)
+
+
+class _SSEDecoder:
+    def __init__(self) -> None:
+        self._event = ""
+        self._data: list[str] = []
+        self._last_event_id = ""
+        self._retry: int | None = None
+        self._pending = False
+
+    def decode(self, line: str) -> ServerSentEvent | None:
+        if not line:
+            if not self._pending:
+                return None
+
+            sse = ServerSentEvent(
+                event=self._event or "message",
+                data="\n".join(self._data),
+                id=self._last_event_id,
+                retry=self._retry,
+            )
+            self._event = ""
+            self._data = []
+            self._retry = None
+            self._pending = False
+            return sse
+
+        if line.startswith(":"):
+            return None
+
+        fieldname, _, value = line.partition(":")
+        value = value[1:] if value.startswith(" ") else value
+
+        if fieldname == "event":
+            self._event = value
+            self._pending = True
+        elif fieldname == "data":
+            self._data.append(value)
+            self._pending = True
+        elif fieldname == "id":
+            if "\0" not in value:
+                self._last_event_id = value
+                self._pending = True
+        elif fieldname == "retry":
+            try:
+                self._retry = int(value)
+                self._pending = True
+            except ValueError:
+                pass
+
+        return None
+
+
+class _SSELineDecoder:
+    def __init__(self) -> None:
+        self._buffer = ""
+        self._trailing_cr = False
+
+    def decode(self, text: str) -> list[str]:
+        if self._trailing_cr:
+            text = "\r" + text
+            self._trailing_cr = False
+        if text.endswith("\r"):
+            self._trailing_cr = True
+            text = text[:-1]
+
+        text = self._buffer + text.replace("\r\n", "\n").replace("\r", "\n")
+        lines = text.split("\n")
+        self._buffer = lines.pop()
+        return lines
+
+    def flush(self) -> list[str]:
+        if self._trailing_cr:
+            self._buffer += "\n"
+            self._trailing_cr = False
+        if not self._buffer:
+            return []
+        lines = self._buffer.split("\n")
+        self._buffer = ""
+        return lines
+
+
+class EventSource:
+    def __init__(self, response: Response) -> None:
+        self._response = response
+
+    @property
+    def response(self) -> Response:
+        return self._response
+
+    def _check_content_type(self) -> None:
+        content_type, _, _ = self._response.headers.get("content-type", 
"").partition(";")
+        if content_type.strip().lower() != "text/event-stream":
+            raise SSEError(
+                f"Expected response with content type 'text/event-stream', got 
{content_type.strip()!r}.",
+                request=self._response.request,
+            )
+
+    def __iter__(self) -> Iterator[ServerSentEvent]:
+        self._check_content_type()
+        decoder = _SSEDecoder()
+        lines = _SSELineDecoder()
+        for chunk in self._response.iter_text():
+            for line in lines.decode(chunk):
+                sse = decoder.decode(line)
+                if sse is not None:
+                    yield sse
+        for line in lines.flush():
+            sse = decoder.decode(line)
+            if sse is not None:
+                yield sse
+
+    async def __aiter__(self) -> AsyncIterator[ServerSentEvent]:
+        self._check_content_type()
+        decoder = _SSEDecoder()
+        lines = _SSELineDecoder()
+        async for chunk in self._response.aiter_text():
+            for line in lines.decode(chunk):
+                sse = decoder.decode(line)
+                if sse is not None:
+                    yield sse
+        for line in lines.flush():
+            sse = decoder.decode(line)
+            if sse is not None:
+                yield sse
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpx2-2.4.0/src/httpx2/httpx2/_utils.py 
new/httpx2-2.5.0/src/httpx2/httpx2/_utils.py
--- old/httpx2-2.4.0/src/httpx2/httpx2/_utils.py        2026-06-11 
08:29:52.000000000 +0200
+++ new/httpx2-2.5.0/src/httpx2/httpx2/_utils.py        2026-06-25 
16:14:41.000000000 +0200
@@ -65,7 +65,11 @@
             elif is_ipv4_hostname(hostname):
                 mounts[f"all://{hostname}"] = None
             elif is_ipv6_hostname(hostname):
-                mounts[f"all://[{hostname}]"] = None
+                if "/" in hostname:
+                    addr, _, subnet = hostname.partition("/")
+                    mounts[f"all://[{addr}]/{subnet}"] = None
+                else:
+                    mounts[f"all://[{hostname}]"] = None
             elif hostname.lower() == "localhost":
                 mounts[f"all://{hostname}"] = None
             else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpx2-2.4.0/tests/httpx2/models/test_headers.py 
new/httpx2-2.5.0/tests/httpx2/models/test_headers.py
--- old/httpx2-2.4.0/tests/httpx2/models/test_headers.py        2026-06-11 
08:29:52.000000000 +0200
+++ new/httpx2-2.5.0/tests/httpx2/models/test_headers.py        2026-06-25 
16:14:41.000000000 +0200
@@ -65,6 +65,46 @@
     assert headers == headers_copy
 
 
+def test_headers_or() -> None:
+    left = httpx2.Headers({"a": "1", "b": "2"})
+    merged = left | {"b": "3", "c": "4"}
+    assert isinstance(merged, httpx2.Headers)
+    assert dict(merged) == {"a": "1", "b": "3", "c": "4"}
+    assert dict(left) == {"a": "1", "b": "2"}
+
+
+def test_headers_or_with_headers() -> None:
+    merged = httpx2.Headers({"a": "1"}) | httpx2.Headers({"a": "2"})
+    assert dict(merged) == {"a": "2"}
+
+
+def test_headers_ror() -> None:
+    merged = {"a": "1", "b": "2"} | httpx2.Headers({"b": "3"})
+    assert isinstance(merged, httpx2.Headers)
+    assert dict(merged) == {"a": "1", "b": "3"}
+
+
+def test_headers_ior() -> None:
+    headers = httpx2.Headers({"a": "1"})
+    original = headers
+    headers |= {"a": "2", "b": "3"}
+    assert headers is original
+    assert dict(headers) == {"a": "2", "b": "3"}
+
+
+def test_headers_ior_accepts_update_inputs() -> None:
+    headers = httpx2.Headers({"a": "1"})
+    headers |= [("a", "2"), ("b", "3")]
+    assert dict(headers) == {"a": "2", "b": "3"}
+
+
+def test_headers_or_unsupported_type() -> None:
+    with pytest.raises(TypeError):
+        httpx2.Headers({"a": "1"}) | [("b", "2")]  # type: ignore[operator]
+    with pytest.raises(TypeError):
+        [("b", "2")] | httpx2.Headers({"a": "1"})  # type: ignore[operator]
+
+
 def test_headers_insert_retains_ordering() -> None:
     headers = httpx2.Headers({"a": "a", "b": "b", "c": "c"})
     headers["b"] = "123"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpx2-2.4.0/tests/httpx2/test_sse.py 
new/httpx2-2.5.0/tests/httpx2/test_sse.py
--- old/httpx2-2.4.0/tests/httpx2/test_sse.py   1970-01-01 01:00:00.000000000 
+0100
+++ new/httpx2-2.5.0/tests/httpx2/test_sse.py   2026-06-25 16:14:41.000000000 
+0200
@@ -0,0 +1,306 @@
+from __future__ import annotations
+
+from collections.abc import AsyncIterator, Iterator
+
+import pytest
+
+import httpx2
+
+SSE_BODY = b": this is a comment\nevent: ping\ndata: hello\nid: 1\nretry: 
500\n\ndata: first\ndata: second\n\n"
+
+
+def sse_handler(request: httpx2.Request) -> httpx2.Response:
+    return httpx2.Response(200, content=SSE_BODY, headers={"Content-Type": 
"text/event-stream"})
+
+
+def test_sse_sync() -> None:
+    with httpx2.Client(transport=httpx2.MockTransport(sse_handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            events = list(source)
+
+    assert events == [
+        httpx2.ServerSentEvent(event="ping", data="hello", id="1", retry=500),
+        httpx2.ServerSentEvent(event="message", data="first\nsecond", id="1"),
+    ]
+
+
[email protected]
+async def test_sse_async() -> None:
+    async with httpx2.AsyncClient(transport=httpx2.MockTransport(sse_handler)) 
as client:
+        async with client.sse("http://testserver/sse";) as source:
+            events = [event async for event in source]
+
+    assert events == [
+        httpx2.ServerSentEvent(event="ping", data="hello", id="1", retry=500),
+        httpx2.ServerSentEvent(event="message", data="first\nsecond", id="1"),
+    ]
+
+
+def test_default_event_is_message() -> None:
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        return httpx2.Response(200, content=b"data: hi\n\n", 
headers={"Content-Type": "text/event-stream"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            (event,) = list(source)
+
+    assert event.event == "message"
+    assert event.data == "hi"
+    assert event.id == ""
+    assert event.retry is None
+
+
+def test_json() -> None:
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        return httpx2.Response(200, content=b'data: {"x": 1}\n\n', 
headers={"Content-Type": "text/event-stream"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            (event,) = list(source)
+
+    assert event.json() == {"x": 1}
+
+
+def test_invalid_retry_is_ignored() -> None:
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        body = b"retry: not-a-number\ndata: hi\n\n"
+        return httpx2.Response(200, content=body, headers={"Content-Type": 
"text/event-stream"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            (event,) = list(source)
+
+    assert event.retry is None
+
+
+def test_id_with_null_is_ignored() -> None:
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        body = b"id: a\0b\ndata: hi\n\n"
+        return httpx2.Response(200, content=body, headers={"Content-Type": 
"text/event-stream"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            (event,) = list(source)
+
+    assert event.id == ""
+
+
+def test_field_without_value() -> None:
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        return httpx2.Response(200, content=b"data\n\n", 
headers={"Content-Type": "text/event-stream"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            (event,) = list(source)
+
+    assert event.data == ""
+
+
+def test_last_event_id_persists() -> None:
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        body = b"id: 1\ndata: a\n\ndata: b\n\n"
+        return httpx2.Response(200, content=body, headers={"Content-Type": 
"text/event-stream"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            events = list(source)
+
+    assert [event.id for event in events] == ["1", "1"]
+
+
+def test_blank_lines_after_id_do_not_dispatch() -> None:
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        body = b"id: 1\ndata: a\n\n\n\ndata: b\n\n"
+        return httpx2.Response(200, content=body, headers={"Content-Type": 
"text/event-stream"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            events = list(source)
+
+    assert [event.data for event in events] == ["a", "b"]
+
+
+def test_id_only_block_is_dispatched() -> None:
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        return httpx2.Response(200, content=b"id: 1\n\n", 
headers={"Content-Type": "text/event-stream"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            (event,) = list(source)
+
+    assert event.id == "1"
+    assert event.data == ""
+
+
[email protected](
+    "content_type",
+    ["text/event-stream", "text/event-stream; charset=utf-8", 
"Text/Event-Stream", "TEXT/EVENT-STREAM"],
+)
+def test_content_type_accepted(content_type: str) -> None:
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        return httpx2.Response(200, content=b"data: hi\n\n", 
headers={"Content-Type": content_type})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            (event,) = list(source)
+
+    assert event.data == "hi"
+
+
+def test_content_type_mismatch_raises() -> None:
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        return httpx2.Response(200, content=b"data: hi\n\n", 
headers={"Content-Type": "application/json"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            with pytest.raises(httpx2.SSEError, match="text/event-stream"):
+                list(source)
+
+
[email protected]
+async def test_content_type_mismatch_raises_async() -> None:
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        return httpx2.Response(200, content=b"data: hi\n\n", 
headers={"Content-Type": "application/json"})
+
+    async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as 
client:
+        async with client.sse("http://testserver/sse";) as source:
+            with pytest.raises(httpx2.SSEError, match="text/event-stream"):
+                [event async for event in source]
+
+
+def test_sets_sse_headers() -> None:
+    captured: dict[str, str] = {}
+
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        captured.update(request.headers)
+        return httpx2.Response(200, content=b"data: hi\n\n", 
headers={"Content-Type": "text/event-stream"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            list(source)
+
+    assert captured["accept"] == "text/event-stream"
+    assert captured["cache-control"] == "no-store"
+
+
+def test_user_headers_take_precedence() -> None:
+    captured: dict[str, str] = {}
+
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        captured.update(request.headers)
+        return httpx2.Response(200, content=b"data: hi\n\n", 
headers={"Content-Type": "text/event-stream"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";, headers={"Accept": 
"text/event-stream, application/json"}) as source:
+            list(source)
+
+    assert captured["accept"] == "text/event-stream, application/json"
+
+
+def test_response_is_accessible() -> None:
+    with httpx2.Client(transport=httpx2.MockTransport(sse_handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            assert isinstance(source.response, httpx2.Response)
+            assert source.response.status_code == 200
+
+
+def test_post_method() -> None:
+    captured: list[str] = []
+
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        captured.append(request.method)
+        return httpx2.Response(200, content=b"data: hi\n\n", 
headers={"Content-Type": "text/event-stream"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";, method="POST", json={"q": 1}) 
as source:
+            list(source)
+
+    assert captured == ["POST"]
+
+
+def test_chunk_boundaries_and_crlf_sync() -> None:
+    def chunks() -> Iterator[bytes]:
+        yield b"data: he"
+        yield b"llo\r\n\r"
+        yield b"\ndata: wor"
+        yield b"ld\r\n\r\n"
+
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        return httpx2.Response(200, content=chunks(), headers={"Content-Type": 
"text/event-stream"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            events = list(source)
+
+    assert [event.data for event in events] == ["hello", "world"]
+
+
[email protected]
+async def test_chunk_boundaries_and_crlf_async() -> None:
+    async def chunks() -> AsyncIterator[bytes]:
+        yield b"data: he"
+        yield b"llo\r\n\r"
+        yield b"\ndata: wor"
+        yield b"ld\r\n\r\n"
+
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        return httpx2.Response(200, content=chunks(), headers={"Content-Type": 
"text/event-stream"})
+
+    async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as 
client:
+        async with client.sse("http://testserver/sse";) as source:
+            events = [event async for event in source]
+
+    assert [event.data for event in events] == ["hello", "world"]
+
+
+def test_event_without_trailing_blank_line() -> None:
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        return httpx2.Response(200, content=b"data: hi", 
headers={"Content-Type": "text/event-stream"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            events = list(source)
+
+    assert events == []
+
+
+def test_leading_blank_line_is_ignored() -> None:
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        return httpx2.Response(200, content=b"\ndata: hi\n\n", 
headers={"Content-Type": "text/event-stream"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            (event,) = list(source)
+
+    assert event.data == "hi"
+
+
+def test_event_dispatched_at_eof_on_trailing_cr_sync() -> None:
+    def chunks() -> Iterator[bytes]:
+        yield b"data: hi\n"
+        yield b"\r"
+
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        return httpx2.Response(200, content=chunks(), headers={"Content-Type": 
"text/event-stream"})
+
+    with httpx2.Client(transport=httpx2.MockTransport(handler)) as client:
+        with client.sse("http://testserver/sse";) as source:
+            (event,) = list(source)
+
+    assert event.data == "hi"
+
+
[email protected]
+async def test_event_dispatched_at_eof_on_trailing_cr_async() -> None:
+    async def chunks() -> AsyncIterator[bytes]:
+        yield b"data: hi\n"
+        yield b"\r"
+
+    def handler(request: httpx2.Request) -> httpx2.Response:
+        return httpx2.Response(200, content=chunks(), headers={"Content-Type": 
"text/event-stream"})
+
+    async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as 
client:
+        async with client.sse("http://testserver/sse";) as source:
+            events = [event async for event in source]
+
+    assert [event.data for event in events] == ["hi"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httpx2-2.4.0/tests/httpx2/test_utils.py 
new/httpx2-2.5.0/tests/httpx2/test_utils.py
--- old/httpx2-2.4.0/tests/httpx2/test_utils.py 2026-06-11 08:29:52.000000000 
+0200
+++ new/httpx2-2.5.0/tests/httpx2/test_utils.py 2026-06-25 16:14:41.000000000 
+0200
@@ -106,6 +106,7 @@
         ({"no_proxy": "127.0.0.1"}, {"all://127.0.0.1": None}),
         ({"no_proxy": "192.168.0.0/16"}, {"all://192.168.0.0/16": None}),
         ({"no_proxy": "::1"}, {"all://[::1]": None}),
+        ({"no_proxy": "fe11::/16"}, {"all://[fe11::]/16": None}),
         ({"no_proxy": "localhost"}, {"all://localhost": None}),
         ({"no_proxy": "github.com"}, {"all://*github.com": None}),
         ({"no_proxy": ".github.com"}, {"all://*.github.com": None}),

Reply via email to