Repository: aio-libs/aiohttp-sse

Issue:
Add EventSourceResponse.last_event_id
I think it would be useful to add `last_event_id` property to the `EventSourceResponse` class from the example: https://github.com/aio-libs/aiohttp-sse/blob/aca7808960635ff7511ff46002b7f83168f6952c/examples/graceful_shutdown.py#L14-L16

Also there is a bug at that example (`sse_response` doesn't accept `response_cls` parameter):
https://github.com/aio-libs/aiohttp-sse/blob/aca7808960635ff7511ff46002b7f83168f6952c/examples/graceful_shutdown.py#L63

Discussion:

Patch:
diff --git a/aiohttp_sse/__init__.py b/aiohttp_sse/__init__.py
index 1b04105..7a18da2 100644
--- a/aiohttp_sse/__init__.py
+++ b/aiohttp_sse/__init__.py
@@ -2,6 +2,7 @@ import asyncio
 import contextlib
 import io
 import re
+from typing import Optional
 
 from aiohttp.web import HTTPMethodNotAllowed, StreamResponse
 
@@ -26,6 +27,7 @@ class EventSourceResponse(StreamResponse):
 
     DEFAULT_PING_INTERVAL = 15
     DEFAULT_SEPARATOR = "\r\n"
+    DEFAULT_LAST_EVENT_HEADER = "Last-Event-Id"
     LINE_SEP_EXPR = re.compile(r"\r\n|\r|\n")
 
     def __init__(self, *, status=200, reason=None, headers=None, sep=None):
@@ -134,6 +136,15 @@ class EventSourceResponse(StreamResponse):
         """Time interval between two ping massages"""
         return self._ping_interval
 
+    @property
+    def last_event_id(self) -> Optional[str]:
+        """Last event ID, requested by client."""
+        if self._req is None:
+            msg = "EventSource request must be prepared first"
+            raise RuntimeError(msg)
+
+        return self._req.headers.get(self.DEFAULT_LAST_EVENT_HEADER)
+
     @ping_interval.setter
     def ping_interval(self, value):
         """Setter for ping_interval property.
diff --git a/examples/graceful_shutdown.py b/examples/graceful_shutdown.py
index b884ead..065c883 100644
--- a/examples/graceful_shutdown.py
+++ b/examples/graceful_shutdown.py
@@ -14,10 +14,6 @@ worker = web.AppKey("worker", asyncio.Task[None])
 
 
 class SSEResponse(EventSourceResponse):
-    @property
-    def last_event_id(self):
-        return self._req.headers.get("Last-Event-Id")
-
     async def send_json(
         self,
         data,


Test patch:
diff --git a/tests/test_sse.py b/tests/test_sse.py
index 7805b1a..696cba5 100644
--- a/tests/test_sse.py
+++ b/tests/test_sse.py
@@ -388,3 +388,34 @@ async def test_multiline_data(loop, unused_tcp_port, session, stream_sep, line_s
     )
     assert streamed_data == expected.format(stream_sep)
     await runner.cleanup()
+
+
+@pytest.mark.asyncio
+class TestLastEventId:
+    async def test_success(self, unused_tcp_port, session):
+        async def func(request):
+            async with sse_response(request) as sse:
+                await sse.send(sse.last_event_id)
+            return sse
+
+        app = web.Application()
+        app.router.add_route("GET", "/", func)
+
+        host = "127.0.0.1"
+        runner = await make_runner(app, host, unused_tcp_port)
+        url = f"http://{host}:{unused_tcp_port}/"
+
+        last_event_id = "42"
+        headers = {EventSourceResponse.DEFAULT_LAST_EVENT_HEADER: last_event_id}
+        resp = await session.request("GET", url, headers=headers)
+        assert resp.status == 200
+
+        # check streamed data
+        streamed_data = await resp.text()
+        assert streamed_data == f"data: {last_event_id}\r\n\r\n"
+        await runner.cleanup()
+
+    async def test_get_before_prepare(self):
+        sse = EventSourceResponse()
+        with pytest.raises(RuntimeError):
+            _ = sse.last_event_id