Streaming#
What is a stream?#
Streaming is a bit different from regular REST APIs. REST APIs always return the whole data in their response.
Streaming establishes a persistent connection,
accepts headers with the content type
of application/jsonl (stands for “JSON Lines”) or text/event-stream
and then streams each individual event when it is ready one by one.
---
config:
theme: forest
---
sequenceDiagram
participant App
participant Client
Client->>App: Make the initial request
App->>Client: Establish connection and send headers
App->>App: Produce Event 1
App->>Client: Send Event 1
Client->>Client: Process Event 1
App->>App: Produce Event 2
App->>Client: Send Event 2
Client->>Client: Process Event 2
Note over App: Continue producing events...
Note over Client: Continue consuming events...
Client->>App: Closes the connection
App->>App: Cleans everything up
How streaming works#
When to use streaming? When you have a single-directional stream of events.
For example:
LLM responses
Logs
Telemetry
Financial services
Sporting events
Live locations
Do not use streaming for regular responses with regular data. Only use it when needed.
Just a regular controller#
We base our API around dmr.streaming.controller.StreamingController
type which is a slightly modified subclass
of a regular Controller.
Streaming controllers support all the same features:
Different HTTP async or sync methods
Error handling (including special error handling for events)
Optional Response validation (including events validation)
etc
We utilize collections.abc.AsyncIterator
protocol to model async event sources.
Existing streaming formats#
We support:
JsonLines is a simple format, where events are streamed line by line, and all lines are valid JSON objects.
Read more: Json Lines
1import dataclasses
2from collections.abc import AsyncIterator
3
4from dmr.plugins.msgspec import MsgspecSerializer
5from dmr.streaming.jsonl import JsonLinesController
6
7
8@dataclasses.dataclass(frozen=True, slots=True)
9class _User:
10 email: str
11
12
13class UserEventsController(JsonLinesController[MsgspecSerializer]):
14 async def get(self) -> AsyncIterator[_User]:
15 return self.produce_user_events()
16
17 async def produce_user_events(self) -> AsyncIterator[_User]:
18 # You can send any complex data that can be serialized
19 # by the controller's serializer:
20 yield _User(email='first@example.com')
21 yield _User(email='second@example.com')
22
Run result
$ curl http://127.0.0.1:8000/api/user/events/ -X GET
{"email":"first@example.com"}
{"email":"second@example.com"}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"type": "array"
},
"msg": {
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"_User": {
"properties": {
"email": {
"type": "string"
}
},
"required": [
"email"
],
"title": "_User",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usereventscontroller/": {
"get": {
"operationId": "getUserEventsControllerApiUsereventscontroller",
"responses": {
"200": {
"content": {
"application/jsonl": {
"itemSchema": {
"$ref": "#/components/schemas/_User"
}
}
},
"description": "OK",
"headers": {
"Cache-Control": {
"required": true,
"schema": {
"type": "string"
}
},
"Connection": {
"required": true,
"schema": {
"type": "string"
}
},
"X-Accel-Buffering": {
"required": true,
"schema": {
"type": "string"
}
}
}
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Server Sent Events is very similar: you can put any data in data:
field, but it has more semantics and is more customizable.
Read more: Server Sent Events
1import dataclasses
2from collections.abc import AsyncIterator
3
4from dmr.plugins.msgspec import MsgspecSerializer
5from dmr.streaming.sse import SSEController, SSEvent
6
7
8@dataclasses.dataclass(frozen=True, slots=True)
9class _User:
10 email: str
11
12
13class UserEventsController(SSEController[MsgspecSerializer]):
14 async def get(self) -> AsyncIterator[SSEvent[_User]]:
15 return self.produce_user_events()
16
17 async def produce_user_events(self) -> AsyncIterator[SSEvent[_User]]:
18 # You can send any complex data that can be serialized
19 # by the controller's serializer,
20 # all SSEvent fields can be customized:
21 yield SSEvent(
22 _User(email='first@example.com'),
23 event='user',
24 )
25 yield SSEvent(
26 _User(email='second@example.com'),
27 event='user',
28 )
29
Run result
$ curl http://127.0.0.1:8000/api/user/events/ -X GET
event: user
data: {"email":"first@example.com"}
event: user
data: {"email":"second@example.com"}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"type": "array"
},
"msg": {
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"SSEvent__User_": {
"description": "Server sent event payload.",
"properties": {
"comment": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"data": {
"$ref": "#/components/schemas/_User"
},
"event": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"id": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"retry": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null
}
},
"required": [
"data"
],
"title": "SSEvent[_User]",
"type": "object"
},
"_User": {
"properties": {
"email": {
"type": "string"
}
},
"required": [
"email"
],
"title": "_User",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usereventscontroller/": {
"get": {
"operationId": "getUserEventsControllerApiUsereventscontroller",
"responses": {
"200": {
"content": {
"text/event-stream": {
"itemSchema": {
"$ref": "#/components/schemas/SSEvent__User_"
}
}
},
"description": "OK",
"headers": {
"Cache-Control": {
"required": true,
"schema": {
"type": "string"
}
},
"Connection": {
"required": true,
"schema": {
"type": "string"
}
},
"X-Accel-Buffering": {
"required": true,
"schema": {
"type": "string"
}
}
}
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
What happens in these examples?
We create our controller as usual, except we are using
dmr.streaming.controller.StreamingControllersubtypes instead of a regularControllerclassWe return
typing.AsyncIteratorinstance instead of a single data item
Choose the one that fits your needs, or create your own format!
Important
All streaming modes require Django to run in ASGI mode in production.
OpenAPI#
We provide full OpenAPI schema support for streaming endpoints.
We also support
itemSchema
schemas for individual events, when OpenAPI 3.2.0 version is used.
Right now, the default version is 3.1.0, so you have
to change the version number manually:
1from dmr.openapi import OpenAPIConfig
2
3config = OpenAPIConfig(
4 title='My awesome API with streaming',
5 version='0.1.0',
6 openapi_version='3.2.0', # NOTE: required to get `itemSchema` support
7)
If you are using streaming, using 3.2.0 is a recommended way.
In 3.1.0 we will produce regular schema definition,
which is not the best way to describe events.
Handling errors#
Streaming controllers have two layers of error handling:
Default error handling works the same way for request / response phase, see Error handling for more information
Per-event error handling, which starts when the streaming connection is established and the events are produced. Such errors are handled in
handle_event_error()method
The second layer of event error handling is unique for streaming controllers.
It works for several cases:
When event validation fails (mostly useful in development)
When there’s an error in the event producer
Let’s see how it can be customized.
1from collections.abc import AsyncIterator
2from typing import Any
3
4from typing_extensions import override
5
6from dmr.plugins.pydantic import PydanticSerializer
7from dmr.streaming.jsonl import Json, JsonLinesController
8
9
10class NumberEventsController(JsonLinesController[PydanticSerializer]):
11 async def get(self) -> AsyncIterator[Json]:
12 return self._valid_events()
13
14 @override
15 async def handle_event_error(self, exc: Exception) -> Any:
16 if isinstance(exc, ZeroDivisionError):
17 return {'error': 'zero divizion'}
18 return await super().handle_event_error(exc)
19
20 async def _valid_events(self) -> AsyncIterator[Json]:
21 yield 1
22 # Error here:
23 yield 1 / 0 # noqa: WPS344 # pyrefly: ignore[division-by-zero]
24 # Won't be sent:
25 yield 2
26
Run result
$ curl http://127.0.0.1:8000/api/numbereventscontroller/ -X GET
1
{"error":"zero divizion"}
1from collections.abc import AsyncIterator
2from typing import Any
3
4from typing_extensions import override
5
6from dmr.plugins.pydantic import PydanticSerializer
7from dmr.streaming.sse import SSEController, SSEvent
8
9
10class NumberEventsController(SSEController[PydanticSerializer]):
11 async def get(self) -> AsyncIterator[SSEvent[float]]:
12 return self._valid_events()
13
14 @override
15 async def handle_event_error(self, exc: Exception) -> Any:
16 if isinstance(exc, ZeroDivisionError):
17 return SSEvent(b'zero divizion', event='error', serialize=False)
18 return await super().handle_event_error(exc)
19
20 async def _valid_events(self) -> AsyncIterator[SSEvent[float]]:
21 yield SSEvent(1)
22 # Error here:
23 yield SSEvent(1 / 0) # noqa: WPS344
24 # Won't be sent:
25 yield SSEvent(2)
26
Run result
$ curl http://127.0.0.1:8000/api/numbereventscontroller/ -X GET
data: 1
event: error
data: zero divizion
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"SSEvent": {
"description": "Server sent event payload.",
"properties": {
"comment": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Comment"
},
"data": {
"title": "Data",
"type": "number"
},
"event": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Event"
},
"id": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Id"
},
"retry": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"title": "Retry"
}
},
"required": [
"data"
],
"title": "SSEvent",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/numbereventscontroller/": {
"get": {
"operationId": "getNumberEventsControllerApiNumbereventscontroller",
"responses": {
"200": {
"content": {
"text/event-stream": {
"itemSchema": {
"$ref": "#/components/schemas/SSEvent"
}
}
},
"description": "OK",
"headers": {
"Cache-Control": {
"required": true,
"schema": {
"type": "string"
}
},
"Connection": {
"required": true,
"schema": {
"type": "string"
}
},
"X-Accel-Buffering": {
"required": true,
"schema": {
"type": "string"
}
}
}
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Warning
Please, note that new events won’t be produced if the error happens
in the async generator itself. If you want to handle errors there as well,
use try/except right inside the event producing async generator.
Handling disconnects#
If you need to immediately close the response stream, you can raise
StreamingCloseError
or asyncio.CancelledError
inside the events producing async iterator.
Async clients can disconnect at any time. We always handle this error gracefully.
See also
Response disconnect docs: https://docs.djangoproject.com/en/stable/ref/request-response/#request-response-streaming-disconnect
Validation#
All our regular response validation rules are applied to the SSE controllers as well. We strictly validate that all headers / cookies / etc are listed in the endpoint’s metadata.
We follow the regular rules for response validation and it can be disabled
by setting validate_responses=False on the needed level.
We also validate events structure, when streaming them to end users. It is recommended to be turned on in development and turned off in production.
Rules:
If endpoint specifies
validate_eventsin itsStreamingextras, we use itIf endpoint does not specify this flag, but controller does, we use it
If controller does not specify this flag, but settings does, we use it
If no explicit
validate_eventsboolean value is specified, we fallback tovalidate_responsesvalue
To set validate_events per endpoint,
pass Streaming as extras=
to the streaming versions of the decorators:
modify and validate.
Regular modify and validate
do not accept streaming extras.
If you don’t set validate_events for your endpoints,
you are free to use any of these two decorators.
They work identically, only typing is different.
Both validate and modify
support this extra:
1from collections.abc import AsyncIterator
2from typing import Any
3
4from dmr.plugins.msgspec import MsgspecSerializer
5from dmr.streaming import Streaming, modify
6from dmr.streaming.sse import SSEController, SSEvent
7
8
9class UserEventsController(SSEController[MsgspecSerializer]):
10 @modify(extras=Streaming(validate_events=False))
11 async def get(self) -> AsyncIterator[SSEvent[int]]:
12 return self.produce_user_events()
13
14 async def produce_user_events(self) -> AsyncIterator[SSEvent[Any]]:
15 yield SSEvent('not-an-int')
16
Run result
$ curl http://127.0.0.1:8000/api/user/events/ -X GET
data: "not-an-int"
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"type": "array"
},
"msg": {
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"SSEvent_int_": {
"description": "Server sent event payload.",
"properties": {
"comment": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"data": {
"type": "integer"
},
"event": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"id": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"retry": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null
}
},
"required": [
"data"
],
"title": "SSEvent[int]",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usereventscontroller/": {
"get": {
"operationId": "getUserEventsControllerApiUsereventscontroller",
"responses": {
"200": {
"content": {
"text/event-stream": {
"itemSchema": {
"$ref": "#/components/schemas/SSEvent_int_"
}
}
},
"description": "OK",
"headers": {
"Cache-Control": {
"required": true,
"schema": {
"type": "string"
}
},
"Connection": {
"required": true,
"schema": {
"type": "string"
}
},
"X-Accel-Buffering": {
"required": true,
"schema": {
"type": "string"
}
}
}
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
1from collections.abc import AsyncIterator
2from typing import Any
3
4from dmr.plugins.msgspec import MsgspecSerializer
5from dmr.streaming import Streaming
6from dmr.streaming.sse import SSEController, SSEvent
7
8
9class UserEventsController(SSEController[MsgspecSerializer]):
10 extras = Streaming(validate_events=False)
11
12 async def get(self) -> AsyncIterator[SSEvent[int]]:
13 return self.produce_user_events()
14
15 async def produce_user_events(self) -> AsyncIterator[SSEvent[Any]]:
16 yield SSEvent('not-an-int')
17
Run result
$ curl http://127.0.0.1:8000/api/user/events/ -X GET
data: "not-an-int"
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"type": "array"
},
"msg": {
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"SSEvent_int_": {
"description": "Server sent event payload.",
"properties": {
"comment": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"data": {
"type": "integer"
},
"event": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"id": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"retry": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null
}
},
"required": [
"data"
],
"title": "SSEvent[int]",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usereventscontroller/": {
"get": {
"operationId": "getUserEventsControllerApiUsereventscontroller",
"responses": {
"200": {
"content": {
"text/event-stream": {
"itemSchema": {
"$ref": "#/components/schemas/SSEvent_int_"
}
}
},
"description": "OK",
"headers": {
"Cache-Control": {
"required": true,
"schema": {
"type": "string"
}
},
"Connection": {
"required": true,
"schema": {
"type": "string"
}
},
"X-Accel-Buffering": {
"required": true,
"schema": {
"type": "string"
}
}
}
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Use validate_events setting
to globally disable events validation.
>>> from dmr.settings import Settings
>>> DMR_SETTINGS = {Settings.validate_events: False}
How do we know the model for events to be validated against?
It might be specified as the
return_typein theResponseSpecof@validatefor the given status codeIt might be specified as the type argument to generic
collections.abc.AsyncIteratorreturn type in@modifystyled endpoint.
We fallback to typing.Any if we can’t infer the event model.
Further reading#
See also
Best practices for working with async iterators and generators in Python: https://docs.python.org/3.15/library/asyncio-dev.html#asynchronous-generators-best-practices
Streaming individual lines of json.
Streaming Server Sent Events.
Advanced topics#
Negotiation for different formats#
If you need different event formats for different users / usecases, then you can use the regular Content negotiation process.
Send different Accept headers to get different event streams back:
1from collections.abc import AsyncGenerator, AsyncIterator
2from contextlib import aclosing
3from typing import Annotated
4
5from typing_extensions import override
6
7from dmr.negotiation import ContentType, conditional_type
8from dmr.plugins.pydantic import PydanticSerializer
9from dmr.settings import default_renderer
10from dmr.streaming import StreamingController
11from dmr.streaming.jsonl import Json
12from dmr.streaming.jsonl.renderer import JsonLinesRenderer
13from dmr.streaming.jsonl.validation import JsonLinesStreamingValidator
14from dmr.streaming.renderer import StreamingRenderer
15from dmr.streaming.sse import SSEvent
16from dmr.streaming.sse.renderer import SSERenderer
17from dmr.streaming.sse.validation import SSEStreamingValidator
18
19
20class EventStreaming(StreamingController[PydanticSerializer]):
21 @classmethod
22 @override
23 def streaming_renderers(
24 cls,
25 serializer: type[PydanticSerializer],
26 ) -> list[StreamingRenderer]:
27 return [
28 SSERenderer(
29 serializer,
30 default_renderer,
31 SSEStreamingValidator,
32 ),
33 JsonLinesRenderer(
34 serializer,
35 default_renderer,
36 JsonLinesStreamingValidator,
37 ),
38 ]
39
40 async def get(
41 self,
42 ) -> AsyncIterator[
43 Annotated[
44 SSEvent[Json] | Json,
45 conditional_type({
46 ContentType.jsonl: Json,
47 ContentType.event_stream: SSEvent[Json],
48 }),
49 ],
50 ]:
51 return self._valid_events()
52
53 async def _valid_events(self) -> AsyncIterator[SSEvent[Json] | Json]:
54 async with aclosing(self._source()) as source:
55 async for event in source:
56 if self.request.accepts(ContentType.event_stream):
57 yield SSEvent(event)
58 else:
59 yield event
60
61 async def _source(self) -> AsyncGenerator[Json]:
62 yield {'a': 1}
63 yield ['b', 2]
64
Run result
$ curl http://127.0.0.1:8000/api/user/events/ -X GET -H 'Accept: application/jsonl, application/json'
{"a":1}
["b",2]
$ curl http://127.0.0.1:8000/api/user/events/ -X GET -H 'Accept: text/event-stream, application/json'
data: {"a":1}
data: ["b",2]
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"SSEvent": {
"description": "Server sent event payload.",
"properties": {
"comment": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Comment"
},
"data": {
"title": "Data"
},
"event": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Event"
},
"id": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Id"
},
"retry": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"title": "Retry"
}
},
"required": [
"data"
],
"title": "SSEvent",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/eventstreaming/": {
"get": {
"operationId": "getEventStreamingApiEventstreaming",
"responses": {
"200": {
"content": {
"application/jsonl": {
"itemSchema": {}
},
"text/event-stream": {
"itemSchema": {
"$ref": "#/components/schemas/SSEvent"
}
}
},
"description": "OK",
"headers": {
"Cache-Control": {
"required": true,
"schema": {
"type": "string"
}
},
"Connection": {
"required": true,
"schema": {
"type": "string"
}
},
"X-Accel-Buffering": {
"required": true,
"schema": {
"type": "string"
}
}
}
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
This is a really advanced feature that is not required in 99% of cases.
Here we define our own StreamingController
subclass and override a special method to instantiate streaming renderers
streaming_renderers().
Next we define an API endpoint for GET method.
We use conditional_type() function to specify which
type will be returned in which case for the OpenAPI metadata.
And _source() method which will provide events for both of the formats.
The last thing we do is we check what Accept header
we are working with and provide an appropriate format for each case.
API Reference#
Controllers#
- class dmr.streaming.controller.StreamingController(**kwargs)[source]#
Bases:
Controller[_SerializerT_co]Base class for all streaming controllers.
It can be used directly, but the most use-cases will be fine with just using the specific streaming protocol.
- endpoint_cls#
alias of
_StreamingEndpoint
- extras: ClassVar[Streaming] = Streaming(validate_events=EMPTY, ping_seconds=EMPTY)#
Streaming settings for all endpoints of this controller.
Override it to change controller-level defaults, for example:
extras = Streaming(validate_events=False)orextras = Streaming(ping_seconds=30). Per-endpoint values are passed asextras=tomodifyandvalidate. UseStreaming.of(self)to read the resolved values.
- async handle_event_error(exc: Exception) Any[source]#
Error handler for the events.
Is called when the
StreamingResponseis iterated in the ASGI handler.
- ping_event() Any | None[source]#
Return a ping event to be generated if this streaming needs it.
By default pings are disabled for
StreamingControllertypes. Pings must be explicitly enabled in subclasses.If
ping_secondsis set inStreamingextras, this method will be called.
- abstractmethod classmethod streaming_renderers(serializer: type[_SerializerT_co]) Iterable[StreamingRenderer][source]#
Returns the iterable of streaming renderers for this controller.
- streaming_response_cls#
Streaming response type to customize.
alias of
StreamingResponse
- to_stream(streaming_content: AsyncIterator[Any], *, status_code: HTTPStatus = HTTPStatus.OK, headers: Mapping[str, str] | None = None, cookies: Mapping[str, NewCookie] | None = None, regular_renderer: Renderer | None = None, streaming_renderer: StreamingRenderer | None = None, streaming_validator: StreamingValidator | None = None) StreamingResponse[source]#
Convert streaming content to a streaming response.
- Parameters:
streaming_content – Async iterator which will produce the streaming events.
status_code – Response status code. Defaults to
200.headers – Response extra headers.
cookies – Response cookies.
regular_renderer – Renderer instance to render event payloads.
streaming_renderer – StreamingRenderer instance to render the event stream.
streaming_validator – StreamingValidator instance to validate events one by one if
validate_eventsis set toTrue.
- Returns:
StreamingResponseinstance with all the required properties set.
Endpoint#
- dmr.streaming.modify = ModifyEndpoint(extras_cls=<class 'dmr.streaming.endpoint.Streaming'>)#
Decorator to modify endpoints that return raw model data.
Apply it to change some API parts:
>>> from http import HTTPStatus >>> from dmr import Controller, modify >>> from dmr.plugins.pydantic import PydanticFastSerializer >>> class TaskController(Controller[PydanticFastSerializer]): ... @modify(status_code=HTTPStatus.ACCEPTED) ... def post(self) -> list[int]: ... return [1, 2] # id of tasks you have started
- Parameters:
status_code – Shows status_code in the documentation. When status_code is passed, always use it by default. When not provided, we use smart inference based on the HTTP method name for default returned response.
headers – Shows headers in the documentation. When headers are passed we will add them for the default response.
cookies – Shows cookies in the documentation. When cookies are passed we will add them for the default response.
validate_responses – Do we have to run runtime validation of responses for this endpoint? Customizable via global setting, per controller, and per endpoint. Here we only store the per endpoint information.
exclude_validate_responses – Set of status codes that we don’t validate, even when
validate_responsesis enabled. Useful for errors like500that can be raised from anywhere and that you might not want to describe. Overrides controller and settings values. Set it toNoneto validate all status codes back.semantic_schema – Should we generate any semantic schema for this endpoint?
semantic_responses – Should semantic responses be collected from different providers for this endpoint.
exclude_semantic_responses – Set of semantic responses status codes that user wants to disable. Overrides controller and settings values. Set it to
Noneto enable all semantic responses back.semantic_auth – Should semantic auth be collected from different providers for this endpoint. Overrides controller and settings values.
exclude_semantic_auth – Set of semantic security requirements names that should not be collected. Overrides controller and settings values.
extra_responses – Sequence of extra responses that this endpoint can return. Overrides controller and settings values. Set it to
Noneto only use the default response.no_validate_http_spec – Set of http spec validation checks that we disable for this endpoint. Overrides controller and settings values. Set it to
Noneto enable all checks back.error_handler – Callback function to be called when this endpoint faces an exception.
parsers – Sequence of types to be used for this endpoint to parse incoming request’s body. All types must be subtypes of
Parser. Overrides controller and settings values.renderers – Sequence of types to be used for this endpoint to render response’s body. All types must be subtypes of
Renderer. Overrides controller and settings values.validate_negotiation – Should we validate that returned response’s
Content-Typeheader matches the one that we inferred in the negotiation process?auth – Sequence of auth instances to be used for this endpoint. Sync endpoints must use instances of
dmr.security.SyncAuth. Async endpoints must use instances ofdmr.security.AsyncAuth. Overrides controller and settings values. Set it toNoneto disable auth for this endpoint.throttling – Sequence of throttle instances to be used for this endpoint. Sync endpoints must use instances of
dmr.throttling.SyncThrottle. Async endpoints must use instances ofdmr.throttling.AsyncThrottle. Overrides controller and settings values. Set it toNoneto disable throttling of this endpoint.summary – A short summary of what the operation does. Defaults to the first paragraph of the endpoint’s docstring. Set it to
Noneto have no summary at all.description – A verbose explanation of the operation behavior. Defaults to everything that goes after the first paragraph of the endpoint’s docstring. Set it to
Noneto have no description at all.tags – A sequence of tags for API documentation control. Used to group operations in OpenAPI documentation. Overrides controller-level and router-level tags. Set it to
Noneto have no tags at all.operation_id – Unique string used to identify the operation.
deprecated – Declares this operation to be deprecated. Overrides controller-level and router-level values. Set it to
Falseto explicitly mark this operation as not deprecated, even if its controller or router is.security – A sequence of security requirement objects for this operation. Overrides the controller value and
securityofdmr.openapi.OpenAPIConfig. Set it toNoneto disablesecurityof this endpoint. It is merged with the requirements generated fromauth, but it never affects the runtime auth. Useful to document external security mechanisms, for example, the ones enforced by an HTTP proxy. Used security schemes must be declared assecurity_schemesincomponentsofdmr.openapi.OpenAPIConfig. See Customizing security.external_docs – Additional external documentation for this operation. Overrides the controller value. Set it to
Noneto have no external docs at all.callbacks – A map of possible out-of band callbacks related to the parent operation. The key is a unique identifier for the Callback Object. Each value in the map is a Callback Object that describes a request that may be initiated by the API provider and the expected responses. Overrides the controller value. Set it to
Noneto have no callbacks at all.servers – An alternative servers sequence to service this operation. Overrides the controller value. Set it to
Noneto have no servers at all.x_extensions – Specification extensions of this operation, keys must start with
x-. Set only on this operation: it is never inherited from or merged withx_extensions, which describes the path item, not its operations.links – Possible links to other OpenAPI operations.
response_description – Description for the generated response object.
ignore_from_spec – If set to
True, this endpoint would not be added to the final OpenAPI spec.extras – Extra settings for custom controllers. See Providing extras for @modify and @validate to learn more.
- Returns:
The same function with
__dmr_payload__payload instance.
Warning
Do not disable
validate_responsesunless this is performance critical for you!Changed in version 0.15.0:
modifyused to be a function, now it is an instance withlazymethod for lazy reusable endpoints.Changed in version 0.16.0: Removed validate_events parameter, added extras parameter instead. This class is now generic and public.
- dmr.streaming.validate = ValidateEndpoint(extras_cls=<class 'dmr.streaming.endpoint.Streaming'>)#
Decorator to validate responses from endpoints that return
HttpResponse.Apply it to validate important API parts:
>>> from http import HTTPStatus >>> from django.http import HttpResponse >>> from dmr import Controller, validate, ResponseSpec >>> from dmr.plugins.pydantic import PydanticFastSerializer >>> class TaskController(Controller[PydanticFastSerializer]): ... @validate( ... ResponseSpec( ... return_type=list[int], ... status_code=HTTPStatus.OK, ... ), ... ) ... def post(self) -> HttpResponse: ... return HttpResponse(b'[1, 2]', status=HTTPStatus.OK)
Response validation can be disabled for extra speed by sending validate_responses falsy parameter or by setting this configuration in your
settings.pyfile:settings.py#>>> DMR_SETTINGS = {'validate_responses': False}- Parameters:
response – The main response that this endpoint is allowed to return.
responses – A collection of other responses that are allowed to be returned from this endpoint.
validate_responses – Do we have to run runtime validation of responses for this endpoint? Customizable via global setting, per controller, and per endpoint. Here we only store the per endpoint information.
exclude_validate_responses – Set of status codes that we don’t validate, even when
validate_responsesis enabled. Useful for errors like500that can be raised from anywhere and that you might not want to describe. Overrides controller and settings values. Set it toNoneto validate all status codes back.semantic_schema – Should we generate any semantic schema for this endpoint?
semantic_responses – Should semantic responses be collected from different providers for this endpoint. Overrides controller and settings values.
exclude_semantic_responses – Set of semantic responses status codes that user wants to disable. Overrides controller and settings values. Set it to
Noneto enable all semantic responses back.semantic_auth – Should semantic auth be collected from different providers for this endpoint. Overrides controller and settings values.
exclude_semantic_auth – Set of semantic security requirements names that should not be collected. Overrides controller and settings values.
no_validate_http_spec – Set of http spec validation checks that we disable for this endpoint. Overrides controller and settings values. Set it to
Noneto enable all checks back.error_handler – Callback function to be called when this endpoint faces an exception.
parsers – Sequence of types to be used for this endpoint to parse incoming request’s body. All types must be subtypes of
Parser. Overrides controller and settings values.renderers – Sequence of types to be used for this endpoint to render response’s body. All types must be subtypes of
Renderer. Overrides controller and settings values.validate_negotiation – Should we validate that returned response’s
Content-Typeheader matches the one that we inferred in the negotiation process?auth – Sequence of auth instances to be used for this endpoint. Sync endpoints must use instances of
dmr.security.SyncAuth. Async endpoints must use instances ofdmr.security.AsyncAuth. Overrides controller and settings values. Set it toNoneto disable auth for this endpoint.throttling – Sequence of throttle instances to be used for this endpoint. Sync endpoints must use instances of
dmr.throttling.SyncThrottle. Async endpoints must use instances ofdmr.throttling.AsyncThrottle. Overrides controller and settings values. Set it toNoneto disable throttling of this endpoint.summary – A short summary of what the operation does. Defaults to the first paragraph of the endpoint’s docstring. Set it to
Noneto have no summary at all.description – A verbose explanation of the operation behavior. Defaults to everything that goes after the first paragraph of the endpoint’s docstring. Set it to
Noneto have no description at all.tags – A sequence of tags for API documentation control. Used to group operations in OpenAPI documentation. Overrides controller-level and router-level tags. Set it to
Noneto have no tags at all.operation_id – Unique string used to identify the operation.
deprecated – Declares this operation to be deprecated. Overrides controller-level and router-level values. Set it to
Falseto explicitly mark this operation as not deprecated, even if its controller or router is.security – A sequence of security requirement objects for this operation. Overrides the controller value and
securityofdmr.openapi.OpenAPIConfig. Set it toNoneto disablesecurityof this endpoint. It is merged with the requirements generated fromauth, but it never affects the runtime auth. Useful to document external security mechanisms, for example, the ones enforced by an HTTP proxy. Used security schemes must be declared assecurity_schemesincomponentsofdmr.openapi.OpenAPIConfig. See Customizing security.external_docs – Additional external documentation for this operation. Overrides the controller value. Set it to
Noneto have no external docs at all.callbacks – A map of possible out-of band callbacks related to the parent operation. The key is a unique identifier for the Callback Object. Each value in the map is a Callback Object that describes a request that may be initiated by the API provider and the expected responses. Overrides the controller value. Set it to
Noneto have no callbacks at all.servers – An alternative servers sequence to service this operation. Overrides the controller value. Set it to
Noneto have no servers at all.x_extensions – Specification extensions of this operation, keys must start with
x-. Set only on this operation: it is never inherited from or merged withx_extensions, which describes the path item, not its operations.ignore_from_spec – If set to
True, this endpoint would not be added to the final OpenAPI spec.extras – Extra settings for custom controllers. See Providing extras for @modify and @validate to learn more.
- Returns:
The same function with
__dmr_payload__payload instance.
Warning
Do not disable
validate_responsesunless this is performance critical for you!Changed in version 0.15.0:
validateused to be a function, now it is an instance withlazymethod for lazy reusable endpoints.Changed in version 0.16.0: Removed validate_events parameter, added extras parameter instead. This class is now generic and public.
- final class dmr.streaming.Streaming(*, validate_events: bool | EMPTY = EMPTY, ping_seconds: float | EMPTY | None = EMPTY)[source]#
Extra settings for endpoints of streaming controllers.
Pass it as
extras=tomodifyorvalidate. It can only be used with streaming controllers.Set it as
extrason a controller to provide controller-level defaults, for example:extras = Streaming(validate_events=False). A controller that assigns its ownextrasreplaces all values of its base controllers, the same way any other attribute does.- validate_events#
Should this endpoint validate events? If not set, defaults to the controller value, then to
validate_events, then to thevalidate_responsesvalue.
- ping_seconds#
Optional ping keep alive event support. Some servers might close long living connections with no activity. Specify number of seconds to wait for the next event before sending a ping event. The payload of the ping event is defined in
ping_event().Nonedisables pings. If not set, defaults to the controller value, then toNone.SSEControllerenables pings every 15 seconds by default.
Added in version 0.16.0.
- classmethod build(from_endpoint: Self | EMPTY, from_controller: Self, controller_cls: type[Controller[BaseSerializer]], builder: EndpointMetadataBuilder) StreamingExtras[source]#
Resolve streaming settings from all configuration layers.
- final class dmr.streaming.endpoint.StreamingExtras(*, validate_events: bool, ping_seconds: float | None)[source]#
Resolved streaming settings of an endpoint.
It is stored in
extrasfor all endpoints of streaming controllers.- ping_seconds#
Seconds to wait for the next event before sending a ping event,
Nonedisables pings.- Type:
float | None
Added in version 0.16.0.
Responses#
- class dmr.streaming.stream.StreamingResponse(streaming_content: AsyncIterator[Any], controller: StreamingController[BaseSerializer], *, regular_renderer: Renderer, streaming_renderer: StreamingRenderer, streaming_validator: StreamingValidator, headers: Mapping[str, str] | None = None, status_code: HTTPStatus = HTTPStatus.OK)[source]#
Bases:
HttpResponseBaseOur own response subclass to mark that we explicitly return SSE.
Converts events to
byteswith the help of a passed serializer and renderer types.We don’t inherit from
django.http.StreamingHttpResponsehere, because it has a strict API for streaming that we can’t use or customize.- __aiter__() AsyncIterator[bytes][source]#
ASGI handler protocol for streaming responses.
When
streamingisTrue, ASGI handler will async iterate over the response object.When doing so, we will be inside the ASGI handler already. No DMR error handling will work.
- __iter__() Iterator[bytes][source]#
In development it is useful to have sync interface for streaming.
This is a part of the WSGI handler protocol.
Danger
Do not use this in production! We even added a special error to catch this. In production you must use ASGI servers like
uvicornwith streaming.This implementation has a lot of limitations. Be careful even in development.
Renderers#
- class dmr.streaming.renderer.StreamingRenderer(serializer: type[BaseSerializer], regular_renderer: Renderer, streaming_validator_cls: type[StreamingValidator])[source]#
Bases:
RendererBase class for all streaming responses.
It is different from the regular
Rendererin several ways:We need to initialize this renderer with a subrenderer, which will render the individual events itself
Serializer is needed to serialize events
Validator is needed to optionally validate events
- streaming: ClassVar[Literal[True]] = True#
Whether or not this renderer is used for streaming responses.
- property validation_parser: _NoOpParser#
Returns a parser that can parse what this renderer rendered.
Why? Because when
validate_responsesisTrue, we parse the response body once again to see if it fits the schema.That’s why all renderers must know how to unparse its results.
Validation#
- class dmr.streaming.validation.StreamingValidator(event_model: TypeForm[Any], serializer: type[BaseSerializer], *, validate_events: bool)[source]#
Injects itself into the stream of SSE to validate the events.
This is very different from any other validator. Why?
Because we send just one response. No events can be produced at all for a long period of time. Some events can be correct, while other can be wrong
We can’t close the connection when finding wrong events, it will be a big problem for our users and it would be hard to debug
But, we can modify events to be
errorevents instead!When validation is active and the event is either not the model we expect or has the wrong payload type - we send the error event
- classmethod from_controller(controller: StreamingController[BaseSerializer], status_code: HTTPStatus) Self[source]#
Construct validator from a controller instance.
Inferences event type model from the endpoint metadata. Also knows whether or not the events validation is turned on or not.
- class dmr.streaming.validation.StreamingResponseValidator(metadata: EndpointMetadata, serializer: type[BaseSerializer])[source]#
Streaming response validator.
Added in version 0.16.0.