Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-sse-starlette for 
openSUSE:Factory checked in at 2026-03-24 18:48:56
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-sse-starlette (Old)
 and      /work/SRC/openSUSE:Factory/.python-sse-starlette.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-sse-starlette"

Tue Mar 24 18:48:56 2026 rev:4 rq:1342123 version:3.3.3

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-sse-starlette/python-sse-starlette.changes    
    2026-03-10 18:01:17.439290675 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-sse-starlette.new.8177/python-sse-starlette.changes
      2026-03-24 18:49:50.856879014 +0100
@@ -1,0 +2,7 @@
+Mon Mar 23 22:53:53 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 3.3.3:
+  * updated documentation
+  * bump dependencies
+
+-------------------------------------------------------------------

Old:
----
  sse_starlette-3.3.2.tar.gz

New:
----
  sse_starlette-3.3.3.tar.gz

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

Other differences:
------------------
++++++ python-sse-starlette.spec ++++++
--- /var/tmp/diff_new_pack.I3oka0/_old  2026-03-24 18:49:51.408901791 +0100
+++ /var/tmp/diff_new_pack.I3oka0/_new  2026-03-24 18:49:51.412901956 +0100
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-sse-starlette
-Version:        3.3.2
+Version:        3.3.3
 Release:        0
 Summary:        SSE plugin for Starlette
 License:        BSD-3-Clause

++++++ sse_starlette-3.3.2.tar.gz -> sse_starlette-3.3.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sse-starlette-3.3.2/.github/workflows/build.yml 
new/sse-starlette-3.3.3/.github/workflows/build.yml
--- old/sse-starlette-3.3.2/.github/workflows/build.yml 2026-02-28 
12:27:42.000000000 +0100
+++ new/sse-starlette-3.3.3/.github/workflows/build.yml 2026-03-17 
21:04:53.000000000 +0100
@@ -20,7 +20,7 @@
         uses: actions/checkout@v6
 
       - name: Install uv
-        uses: astral-sh/setup-uv@v6
+        uses: astral-sh/setup-uv@v7
 
       - name: Set up Python
         uses: actions/setup-python@v6
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sse-starlette-3.3.2/ARCHITECTURE.md 
new/sse-starlette-3.3.3/ARCHITECTURE.md
--- old/sse-starlette-3.3.2/ARCHITECTURE.md     1970-01-01 01:00:00.000000000 
+0100
+++ new/sse-starlette-3.3.3/ARCHITECTURE.md     2026-03-17 21:04:53.000000000 
+0100
@@ -0,0 +1,273 @@
+# Shutdown & Cancellation Flow
+
+## Task Group Architecture
+
+When `EventSourceResponse.__call__` is invoked, it creates an `anyio` task 
group with
+four concurrent tasks, each wrapped in `cancel_on_finish`. **Whichever task 
returns first
+cancels all siblings** via `task_group.cancel_scope.cancel()`.
+
+```
+EventSourceResponse.__call__(scope, receive, send)
+   |
+   v
+anyio.create_task_group()
+   |
+   +-- cancel_on_finish(_stream_response)      # pushes SSE data to client
+   +-- cancel_on_finish(_ping)                 # keepalive pings every ~15s
+   +-- cancel_on_finish(_listen_for_exit_signal_with_grace)  # server shutdown
+   +-- cancel_on_finish(_listen_for_disconnect) # client disconnect
+   |   (+ optional: data_sender_callable)
+   |
+   v
+All tasks cancelled --> background task (if any) --> return
+```
+
+---
+
+## The `cancel_on_finish` Pattern
+
+```python
+async def cancel_on_finish(coro):
+    await coro()                        # run until coro returns
+    task_group.cancel_scope.cancel()    # then cancel ALL sibling tasks
+```
+
+This makes the task group a **race**: the first task to complete wins and 
kills the rest.
+
+---
+
+## Flow 1: Normal Generator Exhaustion
+
+```
+Generator yields all items and finishes naturally.
+
+_stream_response         _ping        _exit_signal    _disconnect
+      |                    |               |               |
+  async for data:          |               |               |
+    send(chunk)       sleep(15)       wait(event)     receive()
+    send(chunk)            |               |               |
+      ...                  |               |               |
+  [generator ends]         |               |               |
+  self.active = False      |               |               |
+  send(more_body=False)    |               |               |
+  return                   |               |               |
+      |                    |               |               |
+  cancel_on_finish ----> CANCEL          CANCEL          CANCEL
+      |
+      v
+  Task group exits cleanly
+```
+
+---
+
+## Flow 2: Client Disconnect
+
+```
+Client closes connection (browser navigates away, network drop).
+
+_stream_response         _ping        _exit_signal    _disconnect
+      |                    |               |               |
+  async for data:     sleep(15)       wait(event)     receive()
+    send(chunk)            |               |               |
+      |                    |               |          http.disconnect
+      |                    |               |          self.active=False
+      |                    |               |          return
+      |                    |               |               |
+      |               cancel_on_finish <-------- CANCEL <--+
+      |                    |               |
+   CANCEL <----------------+----- CANCEL <-+
+      |
+      v
+  Task group exits (generator receives CancelledError)
+```
+
+---
+
+## Flow 3: Server Shutdown (No Cooperative Shutdown)
+
+Default behavior when `shutdown_event` is not provided.
+
+```
+                          SIGTERM / SIGINT
+                               |
+                               v
+                    Server.handle_exit() [monkey-patched]
+                               |
+                    AppStatus.should_exit = True
+                               |
+                               v
+              _shutdown_watcher (polls every 0.5s)
+                               |
+                    detects should_exit == True
+                               |
+                    broadcasts to all registered anyio.Events
+                               |
+                               v
+
+_stream_response         _ping        _exit_signal_with_grace    _disconnect
+      |                    |                    |                      |
+  async for data:     sleep(15)       _listen_for_exit_signal()   receive()
+    send(chunk)            |                    |                      |
+      |                    |             event.wait() returns          |
+      |                    |                    |                      |
+      |                    |             shutdown_event = None         |
+      |                    |             grace_period = 0              |
+      |                    |             --> return immediately        |
+      |                    |                    |                      |
+   CANCEL <------------ CANCEL <-- cancel_on_finish           CANCEL <-+
+      |
+      v
+  Generator receives CancelledError (no chance for farewell)
+  Task group exits
+```
+
+---
+
+## Flow 4: Server Shutdown WITH Cooperative Shutdown (Issue #167)
+
+When `shutdown_event` and `shutdown_grace_period` are provided.
+
+```
+                          SIGTERM / SIGINT
+                               |
+                               v
+                    AppStatus.should_exit = True
+                               |
+                    _shutdown_watcher broadcasts
+                               |
+                               v
+
+_stream_response         _ping        _exit_signal_with_grace    _disconnect
+      |                    |                    |                      |
+  async for data:     sleep(15)       _listen_for_exit_signal()   receive()
+    send(chunk)            |                    |                      |
+      |                    |             event.wait() returns          |
+      |                    |                    |                      |
+      |                    |             shutdown_event.set()          |
+      |                    |              (user event signaled)        |
+      |                    |                    |                      |
+      |                    |             move_on_after(grace_period)   |
+      |                    |              |                            |
+      |                    |              +-- while self.active:       |
+      |                    |              |     sleep(0.1)             |
+      |                    |              |                            |
+  [generator sees event]   |              |     (polling...)           |
+  yield farewell event     |              |                            |
+  return                   |              |                            |
+  self.active = False      |              |                            |
+  send(more_body=False)    |              |                            |
+  return                   |              |                            |
+      |                    |              |                            |
+  cancel_on_finish -----> CANCEL         CANCEL                  CANCEL
+      |
+      v
+  Clean exit! Farewell event reached client.
+```
+
+### Sub-scenario: Generator ignores shutdown_event (grace expires)
+
+```
+_stream_response                    _exit_signal_with_grace
+      |                                      |
+  async for data:                   _listen_for_exit_signal() returns
+    send(chunk)                              |
+      |                             shutdown_event.set()
+      |                                      |
+  [generator ignores event,        move_on_after(grace_period)
+   keeps yielding]                   |
+      |                              +-- while self.active:
+      |                              |     sleep(0.1)
+      |                              |       ...
+      |                              |     (grace_period seconds pass)
+      |                              |
+      |                              +-- move_on_after EXPIRES
+      |                                      |
+      |                                    return
+      |                                      |
+   CANCEL <----------------------- cancel_on_finish
+      |
+      v
+  Generator receives CancelledError (force-cancelled)
+```
+
+---
+
+## Flow 5: Send Timeout
+
+```
+_stream_response         _ping        _exit_signal    _disconnect
+      |                    |               |               |
+  async for data:          |               |               |
+    move_on_after(timeout):|               |               |
+      send(chunk)          |               |               |
+        [send hangs!]      |               |               |
+           ...             |               |               |
+    [timeout expires]      |               |               |
+    cancel_called = True   |               |               |
+    aclose() iterator      |               |               |
+    raise SendTimeoutError |               |               |
+      |                    |               |               |
+  EXCEPTION propagates through cancel_on_finish into task group
+  Task group cancels all siblings
+```
+
+---
+
+## Shutdown Detection: Two-Layer Architecture
+
+```
+Layer 1: Signal Capture (process-wide)
++------------------------------------------------------------------+
+|                                                                  |
+|  SIGTERM/SIGINT                                                  |
+|       |                                                          |
+|       v                                                          |
+|  uvicorn.Server.handle_exit()  [monkey-patched at import time]   |
+|       |                                                          |
+|       v                                                          |
+|  AppStatus.should_exit = True                                    |
+|  + calls original uvicorn handler                                |
+|                                                                  |
+|  Fallback (monkey-patch fails, e.g. uvicorn 0.29+):             |
+|  _get_uvicorn_server() introspects signal.getsignal(SIGTERM)    |
+|  to find uvicorn's Server instance and check .should_exit        |
+|                                                                  |
++------------------------------------------------------------------+
+
+Layer 2: Per-Thread Broadcast (thread-local)
++------------------------------------------------------------------+
+|                                                                  |
+|  Thread A (main event loop)          Thread B (secondary loop)   |
+|  +----------------------------+     +------------------------+   |
+|  | _thread_state.shutdown_state|     | _thread_state (separate)|  |
+|  |   .events = {ev1, ev2, ev3}|     |   .events = {ev4}      |  |
+|  |   .watcher_started = True  |     |   .watcher_started=True|  |
+|  +----------------------------+     +------------------------+   |
+|           |                                   |                  |
+|    _shutdown_watcher()                 _shutdown_watcher()       |
+|    polls AppStatus.should_exit         polls AppStatus.should_exit|
+|    every 0.5s                          every 0.5s                |
+|           |                                   |                  |
+|    on True: ev1.set()                  on True: ev4.set()        |
+|             ev2.set()                                            |
+|             ev3.set()                                            |
+|                                                                  |
++------------------------------------------------------------------+
+```
+
+Each SSE connection registers its own `anyio.Event` with the thread's shutdown 
state.
+One watcher per thread broadcasts to all connections in that thread.
+
+---
+
+## Summary: Who Wins the Race?
+
+| Scenario | Task that returns first | Effect on generator |
+|---|---|---|
+| Generator exhausted | `_stream_response` | Clean exit, farewell sent |
+| Client disconnects | `_listen_for_disconnect` | CancelledError |
+| Server shutdown (no grace) | `_listen_for_exit_signal_with_grace` | 
CancelledError |
+| Server shutdown (with grace, generator cooperates) | `_stream_response` | 
Clean exit, farewell sent |
+| Server shutdown (with grace, generator ignores) | 
`_listen_for_exit_signal_with_grace` (after timeout) | CancelledError |
+| Send timeout | `_stream_response` (via exception) | SendTimeoutError |
+| Client disconnect during grace | `_listen_for_disconnect` | Grace period cut 
short |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sse-starlette-3.3.2/README.md 
new/sse-starlette-3.3.3/README.md
--- old/sse-starlette-3.3.2/README.md   2026-02-28 12:27:42.000000000 +0100
+++ new/sse-starlette-3.3.3/README.md   2026-03-17 21:04:53.000000000 +0100
@@ -50,6 +50,9 @@
 - **Thread Safety**: Context-local event management for multi-threaded 
applications
 - **Multi-Loop Support**: Works correctly with multiple asyncio event loops
 
+For a detailed look at the internal task coordination, shutdown detection, and 
cancellation
+flows, see [ARCHITECTURE.md](ARCHITECTURE.md).
+
 ## Key Components
 
 ### EventSourceResponse
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sse-starlette-3.3.2/VERSION 
new/sse-starlette-3.3.3/VERSION
--- old/sse-starlette-3.3.2/VERSION     2026-02-28 12:27:42.000000000 +0100
+++ new/sse-starlette-3.3.3/VERSION     2026-03-17 21:04:53.000000000 +0100
@@ -1 +1 @@
-3.3.2
+3.3.3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sse-starlette-3.3.2/pyproject.toml 
new/sse-starlette-3.3.3/pyproject.toml
--- old/sse-starlette-3.3.2/pyproject.toml      2026-02-28 12:27:42.000000000 
+0100
+++ new/sse-starlette-3.3.3/pyproject.toml      2026-03-17 21:04:53.000000000 
+0100
@@ -1,6 +1,6 @@
 [project]
 name = "sse-starlette"
-version = "3.3.2"
+version = "3.3.3"
 description = "SSE plugin for Starlette"
 readme = "README.md"
 license = "BSD-3-Clause"
@@ -73,7 +73,7 @@
 include = ["sse_starlette*"]
 
 [tool.bumpversion]
-current_version = "3.3.2"
+current_version = "3.3.3"
 commit = true
 tag = false
 message = "Bump version to {new_version}"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sse-starlette-3.3.2/sse_starlette/__init__.py 
new/sse-starlette-3.3.3/sse_starlette/__init__.py
--- old/sse-starlette-3.3.2/sse_starlette/__init__.py   2026-02-28 
12:27:42.000000000 +0100
+++ new/sse-starlette-3.3.3/sse_starlette/__init__.py   2026-03-17 
21:04:53.000000000 +0100
@@ -2,4 +2,4 @@
 from sse_starlette.sse import EventSourceResponse
 
 __all__ = ["EventSourceResponse", "ServerSentEvent", "JSONServerSentEvent"]
-__version__ = "3.3.2"
+__version__ = "3.3.3"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sse-starlette-3.3.2/tests/integration/test_multiple_consumers.py 
new/sse-starlette-3.3.3/tests/integration/test_multiple_consumers.py
--- old/sse-starlette-3.3.2/tests/integration/test_multiple_consumers.py        
2026-02-28 12:27:42.000000000 +0100
+++ new/sse-starlette-3.3.3/tests/integration/test_multiple_consumers.py        
2026-03-17 21:04:53.000000000 +0100
@@ -37,7 +37,7 @@
 async def consume_events(url: str, expected_lines: int = 2):
     """Simulate Client: Stream the SSE endpoint and count received lines."""
     i = 0
-    async with httpx.AsyncClient() as client:
+    async with httpx.AsyncClient(trust_env=False) as client:
         try:
             async with client.stream("GET", url) as response:
                 async for line in response.aiter_lines():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sse-starlette-3.3.2/uv.lock 
new/sse-starlette-3.3.3/uv.lock
--- old/sse-starlette-3.3.2/uv.lock     2026-02-28 12:27:42.000000000 +0100
+++ new/sse-starlette-3.3.3/uv.lock     2026-03-17 21:04:53.000000000 +0100
@@ -1105,11 +1105,11 @@
 
 [[package]]
 name = "pyasn1"
-version = "0.6.2"
+version = "0.6.3"
 source = { registry = "https://pypi.org/simple"; }
-sdist = { url = 
"https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz";,
 hash = 
"sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size 
= 146586, upload-time = "2026-01-16T18:04:18.534Z" }
+sdist = { url = 
"https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz";,
 hash = 
"sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size 
= 148685, upload-time = "2026-03-17T01:06:53.382Z" }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl";,
 hash = 
"sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size 
= 83371, upload-time = "2026-01-16T18:04:17.174Z" },
+    { url = 
"https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl";,
 hash = 
"sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size 
= 83997, upload-time = "2026-03-17T01:06:52.036Z" },
 ]
 
 [[package]]
@@ -1277,15 +1277,15 @@
 
 [[package]]
 name = "pyopenssl"
-version = "25.3.0"
+version = "26.0.0"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
     { name = "cryptography" },
     { name = "typing-extensions", marker = "python_full_version < '3.13'" },
 ]
-sdist = { url = 
"https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz";,
 hash = 
"sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size 
= 184073, upload-time = "2025-09-17T00:32:21.037Z" }
+sdist = { url = 
"https://files.pythonhosted.org/packages/8e/11/a62e1d33b373da2b2c2cd9eb508147871c80f12b1cacde3c5d314922afdd/pyopenssl-26.0.0.tar.gz";,
 hash = 
"sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc", size 
= 185534, upload-time = "2026-03-15T14:28:26.353Z" }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl";,
 hash = 
"sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size 
= 57268, upload-time = "2025-09-17T00:32:19.474Z" },
+    { url = 
"https://files.pythonhosted.org/packages/fb/7d/d4f7d908fa8415571771b30669251d57c3cf313b36a856e6d7548ae01619/pyopenssl-26.0.0-py3-none-any.whl";,
 hash = 
"sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81", size 
= 57969, upload-time = "2026-03-15T14:28:24.864Z" },
 ]
 
 [[package]]

Reply via email to