Skip to content

Commit

Permalink
Update SSE documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
RobertoPrevato committed Jan 16, 2024
1 parent b5a077b commit 6a83058
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 26 deletions.
12 changes: 11 additions & 1 deletion docs/responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,19 @@ Use the `Response.remove_cookie` method to remove a cookie from the response
object before it's sent to the client. Note that this method does not generate
a `Set-Cookie` header.

## Response streaming

BlackSheep supports response streaming using `StreamedContent` objects bound
to a generator yielding bytes, like described in the following paragraph.
If the content length is not known in advance, chunked encoding is used by
default.
If the content length is known in advance, it can be set in the instance of
`StreamedContent` (in this case, it is responsibility of the user to ensure
the generator will return the correct amount of bytes).

## Chunked encoding

The following example shows how chunked encoding can be used in responses,
The following example shows how response streaming can be used in responses,
using a `StreamedContent` object bound to a generator yielding bytes.

```python
Expand Down
103 changes: 78 additions & 25 deletions docs/server-sent-events.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
# Server-Sent Events

[**Server-Sent Events**](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) (SSE) enable creating a persistent, one-way connection between a client and a server.
When using SSE, a client can receive automatic updates from a server via an HTTP
connection.
[**Server-Sent Events**](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
(SSE) enable creating a persistent, one-way connection between a client and a
server. When using SSE, a client can receive automatic updates from a server
via an HTTP connection.

BlackSheep implements built-in support for server-side events since version
`2.0.6`, offering features to simplify their use. This page describes how to
use the built-in features for server-sent events.

!!! tip
Older versions of the web framework can also be configured to use SSE,
because they already supported response streaming. But this requires
writing functions that generate the right bytes sequences.
because they already supported response streaming, but this requires
writing functions that generate the right bytes.

## Defining a server-sent events route

The following example describes the base elements to configure a route for
server-sent events:
The following example describes how to configure a route for server-sent events,
defining a request handler as asynchronous generator:

```python
import asyncio
Expand All @@ -30,21 +31,32 @@ app = Application()

# A request handler defined as async generator yielding ServerSentEvent...
@get("/events")
def events_handler() -> AsyncIterable[ServerSentEvent]:
async def events_handler() -> AsyncIterable[ServerSentEvent]:
for i in range(3):
yield ServerSentEvent({"message": f"Hello World {i}"})
await asyncio.sleep(1)
```

1. Import `ServerSentEvent` from `blacksheep.server.sse`.
2. Import `AsyncIterable` from `collections.abc`, or from `typing` if support for Python 3.8 is desired.
3. Define an asynchronous generator with a return type annotation of `AsyncIterable[ServerSentEvent]`.
4. Register a request handler that returns a `ServerEventsResponse` bound to
the asynchronous generator. This kind of response returns a specialized type
of `StreamedContent` that streams events information to the client.

In this case the return type annotation is mandatory because the request
handler is automatically normalized by BlackSheep.
2. Import `AsyncIterable` from `collections.abc`, or from `typing` if support
for Python 3.8 is desired.
3. Define a request handler as asynchronous generator with a return type
annotation of `AsyncIterable[ServerSentEvent]` (the function must be `async`
and must include at least one `yield` statement like above).

In this case the return type annotation on the request handler is mandatory
because the request handler is automatically normalized by BlackSheep.

BlackSheep supports request handlers defined as asynchronous generators since
version `2.0.6` especially for this use case, to support less verbose code
when using server-sent events. Previous versions already supported response
streaming, but required returning a `Response` bound to a `StreamedContent` and
an asynchronous generator yielding bytes. Using `async generators` with custom
classes require configuring the type of `Response` that is used to convert
those classes into bytes, like described in [the responses page](/responses/#chunked-encoding).

The following example shows how to define a server-sent event route controlling
the `Response` object.

## Defining a server-sent events route controlling the Response object

Expand All @@ -56,7 +68,7 @@ import asyncio
from collections.abc import AsyncIterable

from blacksheep import Application, get
from blacksheep.server.sse import ServerSentEvent, ServerEventsResponse
from blacksheep.server.sse import ServerSentEvent, ServerSentEventsResponse

app = Application()

Expand All @@ -71,11 +83,55 @@ async def events_provider() -> AsyncIterable[ServerSentEvent]:
# A request handler returning a streaming response bound to the generator...
@get("/events")
def events_handler():
return ServerEventsResponse(events_provider)
return ServerSentEventsResponse(events_provider)
```

In this case the return type annotation is optional.

!!! tip
If you need to access the request object or other injected objects inside
the generator, use `functools.partial` for the function argument of the
`ServerSentEventsResponse`.

## Using controllers

Server-sent events routes are also supported in controllers.

```python
import asyncio
from collections.abc import AsyncIterable

from blacksheep import Application, Request
from blacksheep.server.process import is_stopping
from blacksheep.server.sse import ServerSentEvent
from blacksheep.server.controllers import Controller, get

app = Application()


class Home(Controller):
@get("/events")
async def events_handler(self, request: Request) -> AsyncIterable[ServerSentEvent]:
i = 0

while True:
if await request.is_disconnected():
print("The request is disconnected!")
break

if is_stopping():
print("The application is stopping!")
break

i += 1
yield ServerSentEvent({"message": f"Hello World {i}"})

try:
await asyncio.sleep(1)
except asyncio.exceptions.CancelledError:
break
```

## The ServerSentEvent class

| Property | Description |
Expand All @@ -100,10 +156,10 @@ To check if a request is disconnected, use the `request.is_disconnected()`
method where appropriate.

To check if the server process is shutting down, use the `is_stopping` function
from `blacksheep.server.application`.
from `blacksheep.server.process`.

```python
from blacksheep.server.application import is_stopping
from blacksheep.server.process import is_stopping
```

The following example demonstrates a basic usage of both features:
Expand All @@ -113,11 +169,10 @@ import asyncio
from collections.abc import AsyncIterable

from blacksheep import Application, Request, get
from blacksheep.server.application import is_stopping
from blacksheep.server.process import is_stopping
from blacksheep.server.sse import ServerSentEvent

app = Application(show_error_details=True)
app.serve_files("static")
app = Application()


@get("/events")
Expand All @@ -142,8 +197,6 @@ async def events_handler(request: Request) -> AsyncIterable[ServerSentEvent]:
break
```

Note how `functools.partial` is used in the example above.

## Example in GitHub

Refer to the [server-sent events example](https://github.com/Neoteroi/BlackSheep-Examples/tree/main/server-sent-events) for an example that handles application shutdown and client
Expand Down

0 comments on commit 6a83058

Please sign in to comment.