committed by
GitHub
23 changed files with 1681 additions and 37 deletions
@ -0,0 +1,120 @@ |
|||
# Server-Sent Events (SSE) { #server-sent-events-sse } |
|||
|
|||
You can stream data to the client using **Server-Sent Events** (SSE). |
|||
|
|||
This is similar to [Stream JSON Lines](stream-json-lines.md){.internal-link target=_blank}, but uses the `text/event-stream` format, which is supported natively by browsers with the <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource" class="external-link" target="_blank">`EventSource` API</a>. |
|||
|
|||
/// info |
|||
|
|||
Added in FastAPI 0.135.0. |
|||
|
|||
/// |
|||
|
|||
## What are Server-Sent Events? { #what-are-server-sent-events } |
|||
|
|||
SSE is a standard for streaming data from the server to the client over HTTP. |
|||
|
|||
Each event is a small text block with "fields" like `data`, `event`, `id`, and `retry`, separated by blank lines. |
|||
|
|||
It looks like this: |
|||
|
|||
``` |
|||
data: {"name": "Portal Gun", "price": 999.99} |
|||
|
|||
data: {"name": "Plumbus", "price": 32.99} |
|||
|
|||
``` |
|||
|
|||
SSE is commonly used for AI chat streaming, live notifications, logs and observability, and other cases where the server pushes updates to the client. |
|||
|
|||
/// tip |
|||
|
|||
If you want to stream binary data, for example video or audio, check the advanced guide: [Stream Data](../advanced/stream-data.md){.internal-link target=_blank}. |
|||
|
|||
/// |
|||
|
|||
## Stream SSE with FastAPI { #stream-sse-with-fastapi } |
|||
|
|||
To stream SSE with FastAPI, use `yield` in your *path operation function* and set `response_class=EventSourceResponse`. |
|||
|
|||
Import `EventSourceResponse` from `fastapi.sse`: |
|||
|
|||
{* ../../docs_src/server_sent_events/tutorial001_py310.py ln[1:25] hl[4,22] *} |
|||
|
|||
Each yielded item is encoded as JSON and sent in the `data:` field of an SSE event. |
|||
|
|||
If you declare the return type as `AsyncIterable[Item]`, FastAPI will use it to **validate**, **document**, and **serialize** the data using Pydantic. |
|||
|
|||
{* ../../docs_src/server_sent_events/tutorial001_py310.py ln[1:25] hl[10:12,23] *} |
|||
|
|||
/// tip |
|||
|
|||
As Pydantic will serialize it in the **Rust** side, you will get much higher **performance** than if you don't declare a return type. |
|||
|
|||
/// |
|||
|
|||
### Non-async *path operation functions* { #non-async-path-operation-functions } |
|||
|
|||
You can also use regular `def` functions (without `async`), and use `yield` the same way. |
|||
|
|||
FastAPI will make sure it's run correctly so that it doesn't block the event loop. |
|||
|
|||
As in this case the function is not async, the right return type would be `Iterable[Item]`: |
|||
|
|||
{* ../../docs_src/server_sent_events/tutorial001_py310.py ln[28:31] hl[29] *} |
|||
|
|||
### No Return Type { #no-return-type } |
|||
|
|||
You can also omit the return type. FastAPI will use the [`jsonable_encoder`](./encoder.md){.internal-link target=_blank} to convert the data and send it. |
|||
|
|||
{* ../../docs_src/server_sent_events/tutorial001_py310.py ln[34:37] hl[35] *} |
|||
|
|||
## `ServerSentEvent` { #serversentevent } |
|||
|
|||
If you need to set SSE fields like `event`, `id`, `retry`, or `comment`, you can yield `ServerSentEvent` objects instead of plain data. |
|||
|
|||
Import `ServerSentEvent` from `fastapi.sse`: |
|||
|
|||
{* ../../docs_src/server_sent_events/tutorial002_py310.py hl[4,26] *} |
|||
|
|||
The `data` field is always encoded as JSON. You can pass any value that can be serialized as JSON, including Pydantic models. |
|||
|
|||
## Raw Data { #raw-data } |
|||
|
|||
If you need to send data **without** JSON encoding, use `raw_data` instead of `data`. |
|||
|
|||
This is useful for sending pre-formatted text, log lines, or special <dfn title="A value used to indicate a special condition or state">"sentinel"</dfn> values like `[DONE]`. |
|||
|
|||
{* ../../docs_src/server_sent_events/tutorial003_py310.py hl[17] *} |
|||
|
|||
/// note |
|||
|
|||
`data` and `raw_data` are mutually exclusive. You can only set one of them on each `ServerSentEvent`. |
|||
|
|||
/// |
|||
|
|||
## Resuming with `Last-Event-ID` { #resuming-with-last-event-id } |
|||
|
|||
When a browser reconnects after a connection drop, it sends the last received `id` in the `Last-Event-ID` header. |
|||
|
|||
You can read it as a header parameter and use it to resume the stream from where the client left off: |
|||
|
|||
{* ../../docs_src/server_sent_events/tutorial004_py310.py hl[25,27,31] *} |
|||
|
|||
## SSE with POST { #sse-with-post } |
|||
|
|||
SSE works with **any HTTP method**, not just `GET`. |
|||
|
|||
This is useful for protocols like <a href="https://modelcontextprotocol.io" class="external-link" target="_blank">MCP</a> that stream SSE over `POST`: |
|||
|
|||
{* ../../docs_src/server_sent_events/tutorial005_py310.py hl[14] *} |
|||
|
|||
## Technical Details { #technical-details } |
|||
|
|||
FastAPI implements some SSE best practices out of the box. |
|||
|
|||
* Send a **"keep alive" `ping` comment** every 15 seconds when there hasn't been any message, to prevent some proxies from closing the connection, as suggested in the <a href="https://html.spec.whatwg.org/multipage/server-sent-events.html#authoring-notes" class="external-link" target="_blank">HTML specification: Server-Sent Events</a>. |
|||
* Set the `Cache-Control: no-cache` header to **prevent caching** of the stream. |
|||
* Set a special header `X-Accel-Buffering: no` to **prevent buffering** in some proxies like Nginx. |
|||
|
|||
You don't have to do anything about it, it works out of the box. 🤓 |
|||
@ -0,0 +1,43 @@ |
|||
from collections.abc import AsyncIterable, Iterable |
|||
|
|||
from fastapi import FastAPI |
|||
from fastapi.sse import EventSourceResponse |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class Item(BaseModel): |
|||
name: str |
|||
description: str | None |
|||
|
|||
|
|||
items = [ |
|||
Item(name="Plumbus", description="A multi-purpose household device."), |
|||
Item(name="Portal Gun", description="A portal opening device."), |
|||
Item(name="Meeseeks Box", description="A box that summons a Meeseeks."), |
|||
] |
|||
|
|||
|
|||
@app.get("/items/stream", response_class=EventSourceResponse) |
|||
async def sse_items() -> AsyncIterable[Item]: |
|||
for item in items: |
|||
yield item |
|||
|
|||
|
|||
@app.get("/items/stream-no-async", response_class=EventSourceResponse) |
|||
def sse_items_no_async() -> Iterable[Item]: |
|||
for item in items: |
|||
yield item |
|||
|
|||
|
|||
@app.get("/items/stream-no-annotation", response_class=EventSourceResponse) |
|||
async def sse_items_no_annotation(): |
|||
for item in items: |
|||
yield item |
|||
|
|||
|
|||
@app.get("/items/stream-no-async-no-annotation", response_class=EventSourceResponse) |
|||
def sse_items_no_async_no_annotation(): |
|||
for item in items: |
|||
yield item |
|||
@ -0,0 +1,26 @@ |
|||
from collections.abc import AsyncIterable |
|||
|
|||
from fastapi import FastAPI |
|||
from fastapi.sse import EventSourceResponse, ServerSentEvent |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class Item(BaseModel): |
|||
name: str |
|||
price: float |
|||
|
|||
|
|||
items = [ |
|||
Item(name="Plumbus", price=32.99), |
|||
Item(name="Portal Gun", price=999.99), |
|||
Item(name="Meeseeks Box", price=49.99), |
|||
] |
|||
|
|||
|
|||
@app.get("/items/stream", response_class=EventSourceResponse) |
|||
async def stream_items() -> AsyncIterable[ServerSentEvent]: |
|||
yield ServerSentEvent(comment="stream of item updates") |
|||
for i, item in enumerate(items): |
|||
yield ServerSentEvent(data=item, event="item_update", id=str(i + 1), retry=5000) |
|||
@ -0,0 +1,17 @@ |
|||
from collections.abc import AsyncIterable |
|||
|
|||
from fastapi import FastAPI |
|||
from fastapi.sse import EventSourceResponse, ServerSentEvent |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.get("/logs/stream", response_class=EventSourceResponse) |
|||
async def stream_logs() -> AsyncIterable[ServerSentEvent]: |
|||
logs = [ |
|||
"2025-01-01 INFO Application started", |
|||
"2025-01-01 DEBUG Connected to database", |
|||
"2025-01-01 WARN High memory usage detected", |
|||
] |
|||
for log_line in logs: |
|||
yield ServerSentEvent(raw_data=log_line) |
|||
@ -0,0 +1,31 @@ |
|||
from collections.abc import AsyncIterable |
|||
from typing import Annotated |
|||
|
|||
from fastapi import FastAPI, Header |
|||
from fastapi.sse import EventSourceResponse, ServerSentEvent |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class Item(BaseModel): |
|||
name: str |
|||
price: float |
|||
|
|||
|
|||
items = [ |
|||
Item(name="Plumbus", price=32.99), |
|||
Item(name="Portal Gun", price=999.99), |
|||
Item(name="Meeseeks Box", price=49.99), |
|||
] |
|||
|
|||
|
|||
@app.get("/items/stream", response_class=EventSourceResponse) |
|||
async def stream_items( |
|||
last_event_id: Annotated[int | None, Header()] = None, |
|||
) -> AsyncIterable[ServerSentEvent]: |
|||
start = last_event_id + 1 if last_event_id is not None else 0 |
|||
for i, item in enumerate(items): |
|||
if i < start: |
|||
continue |
|||
yield ServerSentEvent(data=item, id=str(i)) |
|||
@ -0,0 +1,19 @@ |
|||
from collections.abc import AsyncIterable |
|||
|
|||
from fastapi import FastAPI |
|||
from fastapi.sse import EventSourceResponse, ServerSentEvent |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class Prompt(BaseModel): |
|||
text: str |
|||
|
|||
|
|||
@app.post("/chat/stream", response_class=EventSourceResponse) |
|||
async def stream_chat(prompt: Prompt) -> AsyncIterable[ServerSentEvent]: |
|||
words = prompt.text.split() |
|||
for word in words: |
|||
yield ServerSentEvent(data=word, event="token") |
|||
yield ServerSentEvent(raw_data="[DONE]", event="done") |
|||
@ -0,0 +1,222 @@ |
|||
from typing import Annotated, Any |
|||
|
|||
from annotated_doc import Doc |
|||
from pydantic import AfterValidator, BaseModel, Field, model_validator |
|||
from starlette.responses import StreamingResponse |
|||
|
|||
# Canonical SSE event schema matching the OpenAPI 3.2 spec |
|||
# (Section 4.14.4 "Special Considerations for Server-Sent Events") |
|||
_SSE_EVENT_SCHEMA: dict[str, Any] = { |
|||
"type": "object", |
|||
"properties": { |
|||
"data": {"type": "string"}, |
|||
"event": {"type": "string"}, |
|||
"id": {"type": "string"}, |
|||
"retry": {"type": "integer", "minimum": 0}, |
|||
}, |
|||
} |
|||
|
|||
|
|||
class EventSourceResponse(StreamingResponse): |
|||
"""Streaming response with `text/event-stream` media type. |
|||
|
|||
Use as `response_class=EventSourceResponse` on a *path operation* that uses `yield` |
|||
to enable Server Sent Events (SSE) responses. |
|||
|
|||
Works with **any HTTP method** (`GET`, `POST`, etc.), which makes it compatible |
|||
with protocols like MCP that stream SSE over `POST`. |
|||
|
|||
The actual encoding logic lives in the FastAPI routing layer. This class |
|||
serves mainly as a marker and sets the correct `Content-Type`. |
|||
""" |
|||
|
|||
media_type = "text/event-stream" |
|||
|
|||
|
|||
def _check_id_no_null(v: str | None) -> str | None: |
|||
if v is not None and "\0" in v: |
|||
raise ValueError("SSE 'id' must not contain null characters") |
|||
return v |
|||
|
|||
|
|||
class ServerSentEvent(BaseModel): |
|||
"""Represents a single Server-Sent Event. |
|||
|
|||
When `yield`ed from a *path operation function* that uses |
|||
`response_class=EventSourceResponse`, each `ServerSentEvent` is encoded |
|||
into the [SSE wire format](https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream) |
|||
(`text/event-stream`). |
|||
|
|||
If you yield a plain object (dict, Pydantic model, etc.) instead, it is |
|||
automatically JSON-encoded and sent as the `data:` field. |
|||
|
|||
All `data` values **including plain strings** are JSON-serialized. |
|||
|
|||
For example, `data="hello"` produces `data: "hello"` on the wire (with |
|||
quotes). |
|||
""" |
|||
|
|||
data: Annotated[ |
|||
Any, |
|||
Doc( |
|||
""" |
|||
The event payload. |
|||
|
|||
Can be any JSON-serializable value: a Pydantic model, dict, list, |
|||
string, number, etc. It is **always** serialized to JSON: strings |
|||
are quoted (`"hello"` becomes `data: "hello"` on the wire). |
|||
|
|||
Mutually exclusive with `raw_data`. |
|||
""" |
|||
), |
|||
] = None |
|||
raw_data: Annotated[ |
|||
str | None, |
|||
Doc( |
|||
""" |
|||
Raw string to send as the `data:` field **without** JSON encoding. |
|||
|
|||
Use this when you need to send pre-formatted text, HTML fragments, |
|||
CSV lines, or any non-JSON payload. The string is placed directly |
|||
into the `data:` field as-is. |
|||
|
|||
Mutually exclusive with `data`. |
|||
""" |
|||
), |
|||
] = None |
|||
event: Annotated[ |
|||
str | None, |
|||
Doc( |
|||
""" |
|||
Optional event type name. |
|||
|
|||
Maps to `addEventListener(event, ...)` on the browser. When omitted, |
|||
the browser dispatches on the generic `message` event. |
|||
""" |
|||
), |
|||
] = None |
|||
id: Annotated[ |
|||
str | None, |
|||
AfterValidator(_check_id_no_null), |
|||
Doc( |
|||
""" |
|||
Optional event ID. |
|||
|
|||
The browser sends this value back as the `Last-Event-ID` header on |
|||
automatic reconnection. **Must not contain null (`\\0`) characters.** |
|||
""" |
|||
), |
|||
] = None |
|||
retry: Annotated[ |
|||
int | None, |
|||
Field(ge=0), |
|||
Doc( |
|||
""" |
|||
Optional reconnection time in **milliseconds**. |
|||
|
|||
Tells the browser how long to wait before reconnecting after the |
|||
connection is lost. Must be a non-negative integer. |
|||
""" |
|||
), |
|||
] = None |
|||
comment: Annotated[ |
|||
str | None, |
|||
Doc( |
|||
""" |
|||
Optional comment line(s). |
|||
|
|||
Comment lines start with `:` in the SSE wire format and are ignored by |
|||
`EventSource` clients. Useful for keep-alive pings to prevent |
|||
proxy/load-balancer timeouts. |
|||
""" |
|||
), |
|||
] = None |
|||
|
|||
@model_validator(mode="after") |
|||
def _check_data_exclusive(self) -> "ServerSentEvent": |
|||
if self.data is not None and self.raw_data is not None: |
|||
raise ValueError( |
|||
"Cannot set both 'data' and 'raw_data' on the same " |
|||
"ServerSentEvent. Use 'data' for JSON-serialized payloads " |
|||
"or 'raw_data' for pre-formatted strings." |
|||
) |
|||
return self |
|||
|
|||
|
|||
def format_sse_event( |
|||
*, |
|||
data_str: Annotated[ |
|||
str | None, |
|||
Doc( |
|||
""" |
|||
Pre-serialized data string to use as the `data:` field. |
|||
""" |
|||
), |
|||
] = None, |
|||
event: Annotated[ |
|||
str | None, |
|||
Doc( |
|||
""" |
|||
Optional event type name (`event:` field). |
|||
""" |
|||
), |
|||
] = None, |
|||
id: Annotated[ |
|||
str | None, |
|||
Doc( |
|||
""" |
|||
Optional event ID (`id:` field). |
|||
""" |
|||
), |
|||
] = None, |
|||
retry: Annotated[ |
|||
int | None, |
|||
Doc( |
|||
""" |
|||
Optional reconnection time in milliseconds (`retry:` field). |
|||
""" |
|||
), |
|||
] = None, |
|||
comment: Annotated[ |
|||
str | None, |
|||
Doc( |
|||
""" |
|||
Optional comment line(s) (`:` prefix). |
|||
""" |
|||
), |
|||
] = None, |
|||
) -> bytes: |
|||
"""Build SSE wire-format bytes from **pre-serialized** data. |
|||
|
|||
The result always ends with `\n\n` (the event terminator). |
|||
""" |
|||
lines: list[str] = [] |
|||
|
|||
if comment is not None: |
|||
for line in comment.splitlines(): |
|||
lines.append(f": {line}") |
|||
|
|||
if event is not None: |
|||
lines.append(f"event: {event}") |
|||
|
|||
if data_str is not None: |
|||
for line in data_str.splitlines(): |
|||
lines.append(f"data: {line}") |
|||
|
|||
if id is not None: |
|||
lines.append(f"id: {id}") |
|||
|
|||
if retry is not None: |
|||
lines.append(f"retry: {retry}") |
|||
|
|||
lines.append("") |
|||
lines.append("") |
|||
return "\n".join(lines).encode("utf-8") |
|||
|
|||
|
|||
# Keep-alive comment, per the SSE spec recommendation |
|||
KEEPALIVE_COMMENT = b": ping\n\n" |
|||
|
|||
# Seconds between keep-alive pings when a generator is idle. |
|||
# Private but importable so tests can monkeypatch it. |
|||
_PING_INTERVAL: float = 15.0 |
|||
@ -0,0 +1,318 @@ |
|||
import asyncio |
|||
import time |
|||
from collections.abc import AsyncIterable, Iterable |
|||
|
|||
import fastapi.routing |
|||
import pytest |
|||
from fastapi import APIRouter, FastAPI |
|||
from fastapi.responses import EventSourceResponse |
|||
from fastapi.sse import ServerSentEvent |
|||
from fastapi.testclient import TestClient |
|||
from pydantic import BaseModel |
|||
|
|||
|
|||
class Item(BaseModel): |
|||
name: str |
|||
description: str | None = None |
|||
|
|||
|
|||
items = [ |
|||
Item(name="Plumbus", description="A multi-purpose household device."), |
|||
Item(name="Portal Gun", description="A portal opening device."), |
|||
Item(name="Meeseeks Box", description="A box that summons a Meeseeks."), |
|||
] |
|||
|
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.get("/items/stream", response_class=EventSourceResponse) |
|||
async def sse_items() -> AsyncIterable[Item]: |
|||
for item in items: |
|||
yield item |
|||
|
|||
|
|||
@app.get("/items/stream-sync", response_class=EventSourceResponse) |
|||
def sse_items_sync() -> Iterable[Item]: |
|||
yield from items |
|||
|
|||
|
|||
@app.get("/items/stream-no-annotation", response_class=EventSourceResponse) |
|||
async def sse_items_no_annotation(): |
|||
for item in items: |
|||
yield item |
|||
|
|||
|
|||
@app.get("/items/stream-sync-no-annotation", response_class=EventSourceResponse) |
|||
def sse_items_sync_no_annotation(): |
|||
yield from items |
|||
|
|||
|
|||
@app.get("/items/stream-dict", response_class=EventSourceResponse) |
|||
async def sse_items_dict(): |
|||
for item in items: |
|||
yield {"name": item.name, "description": item.description} |
|||
|
|||
|
|||
@app.get("/items/stream-sse-event", response_class=EventSourceResponse) |
|||
async def sse_items_event(): |
|||
yield ServerSentEvent(data="hello", event="greeting", id="1") |
|||
yield ServerSentEvent(data={"key": "value"}, event="json-data", id="2") |
|||
yield ServerSentEvent(comment="just a comment") |
|||
yield ServerSentEvent(data="retry-test", retry=5000) |
|||
|
|||
|
|||
@app.get("/items/stream-mixed", response_class=EventSourceResponse) |
|||
async def sse_items_mixed() -> AsyncIterable[Item]: |
|||
yield items[0] |
|||
yield ServerSentEvent(data="custom-event", event="special") |
|||
yield items[1] |
|||
|
|||
|
|||
@app.get("/items/stream-string", response_class=EventSourceResponse) |
|||
async def sse_items_string(): |
|||
yield ServerSentEvent(data="plain text data") |
|||
|
|||
|
|||
@app.post("/items/stream-post", response_class=EventSourceResponse) |
|||
async def sse_items_post() -> AsyncIterable[Item]: |
|||
for item in items: |
|||
yield item |
|||
|
|||
|
|||
@app.get("/items/stream-raw", response_class=EventSourceResponse) |
|||
async def sse_items_raw(): |
|||
yield ServerSentEvent(raw_data="plain text without quotes") |
|||
yield ServerSentEvent(raw_data="<div>html fragment</div>", event="html") |
|||
yield ServerSentEvent(raw_data="cpu,87.3,1709145600", event="csv") |
|||
|
|||
|
|||
router = APIRouter() |
|||
|
|||
|
|||
@router.get("/events", response_class=EventSourceResponse) |
|||
async def stream_events(): |
|||
yield {"msg": "hello"} |
|||
yield {"msg": "world"} |
|||
|
|||
|
|||
app.include_router(router, prefix="/api") |
|||
|
|||
|
|||
@pytest.fixture(name="client") |
|||
def client_fixture(): |
|||
with TestClient(app) as c: |
|||
yield c |
|||
|
|||
|
|||
def test_async_generator_with_model(client: TestClient): |
|||
response = client.get("/items/stream") |
|||
assert response.status_code == 200 |
|||
assert response.headers["content-type"] == "text/event-stream; charset=utf-8" |
|||
assert response.headers["cache-control"] == "no-cache" |
|||
assert response.headers["x-accel-buffering"] == "no" |
|||
|
|||
lines = response.text.strip().split("\n") |
|||
data_lines = [line for line in lines if line.startswith("data: ")] |
|||
assert len(data_lines) == 3 |
|||
assert '"name":"Plumbus"' in data_lines[0] or '"name": "Plumbus"' in data_lines[0] |
|||
assert ( |
|||
'"name":"Portal Gun"' in data_lines[1] |
|||
or '"name": "Portal Gun"' in data_lines[1] |
|||
) |
|||
assert ( |
|||
'"name":"Meeseeks Box"' in data_lines[2] |
|||
or '"name": "Meeseeks Box"' in data_lines[2] |
|||
) |
|||
|
|||
|
|||
def test_sync_generator_with_model(client: TestClient): |
|||
response = client.get("/items/stream-sync") |
|||
assert response.status_code == 200 |
|||
assert response.headers["content-type"] == "text/event-stream; charset=utf-8" |
|||
|
|||
data_lines = [ |
|||
line for line in response.text.strip().split("\n") if line.startswith("data: ") |
|||
] |
|||
assert len(data_lines) == 3 |
|||
|
|||
|
|||
def test_async_generator_no_annotation(client: TestClient): |
|||
response = client.get("/items/stream-no-annotation") |
|||
assert response.status_code == 200 |
|||
assert response.headers["content-type"] == "text/event-stream; charset=utf-8" |
|||
|
|||
data_lines = [ |
|||
line for line in response.text.strip().split("\n") if line.startswith("data: ") |
|||
] |
|||
assert len(data_lines) == 3 |
|||
|
|||
|
|||
def test_sync_generator_no_annotation(client: TestClient): |
|||
response = client.get("/items/stream-sync-no-annotation") |
|||
assert response.status_code == 200 |
|||
assert response.headers["content-type"] == "text/event-stream; charset=utf-8" |
|||
|
|||
data_lines = [ |
|||
line for line in response.text.strip().split("\n") if line.startswith("data: ") |
|||
] |
|||
assert len(data_lines) == 3 |
|||
|
|||
|
|||
def test_dict_items(client: TestClient): |
|||
response = client.get("/items/stream-dict") |
|||
assert response.status_code == 200 |
|||
data_lines = [ |
|||
line for line in response.text.strip().split("\n") if line.startswith("data: ") |
|||
] |
|||
assert len(data_lines) == 3 |
|||
assert '"name"' in data_lines[0] |
|||
|
|||
|
|||
def test_post_method_sse(client: TestClient): |
|||
"""SSE should work with POST (needed for MCP compatibility).""" |
|||
response = client.post("/items/stream-post") |
|||
assert response.status_code == 200 |
|||
assert response.headers["content-type"] == "text/event-stream; charset=utf-8" |
|||
data_lines = [ |
|||
line for line in response.text.strip().split("\n") if line.startswith("data: ") |
|||
] |
|||
assert len(data_lines) == 3 |
|||
|
|||
|
|||
def test_sse_events_with_fields(client: TestClient): |
|||
response = client.get("/items/stream-sse-event") |
|||
assert response.status_code == 200 |
|||
text = response.text |
|||
|
|||
assert "event: greeting\n" in text |
|||
assert 'data: "hello"\n' in text |
|||
assert "id: 1\n" in text |
|||
|
|||
assert "event: json-data\n" in text |
|||
assert "id: 2\n" in text |
|||
assert 'data: {"key": "value"}\n' in text |
|||
|
|||
assert ": just a comment\n" in text |
|||
|
|||
assert "retry: 5000\n" in text |
|||
assert 'data: "retry-test"\n' in text |
|||
|
|||
|
|||
def test_mixed_plain_and_sse_events(client: TestClient): |
|||
response = client.get("/items/stream-mixed") |
|||
assert response.status_code == 200 |
|||
text = response.text |
|||
|
|||
assert "event: special\n" in text |
|||
assert 'data: "custom-event"\n' in text |
|||
assert '"name"' in text |
|||
|
|||
|
|||
def test_string_data_json_encoded(client: TestClient): |
|||
"""Strings are always JSON-encoded (quoted).""" |
|||
response = client.get("/items/stream-string") |
|||
assert response.status_code == 200 |
|||
assert 'data: "plain text data"\n' in response.text |
|||
|
|||
|
|||
def test_server_sent_event_null_id_rejected(): |
|||
with pytest.raises(ValueError, match="null"): |
|||
ServerSentEvent(data="test", id="has\0null") |
|||
|
|||
|
|||
def test_server_sent_event_negative_retry_rejected(): |
|||
with pytest.raises(ValueError): |
|||
ServerSentEvent(data="test", retry=-1) |
|||
|
|||
|
|||
def test_server_sent_event_float_retry_rejected(): |
|||
with pytest.raises(ValueError): |
|||
ServerSentEvent(data="test", retry=1.5) # type: ignore[arg-type] |
|||
|
|||
|
|||
def test_raw_data_sent_without_json_encoding(client: TestClient): |
|||
"""raw_data is sent as-is, not JSON-encoded.""" |
|||
response = client.get("/items/stream-raw") |
|||
assert response.status_code == 200 |
|||
text = response.text |
|||
|
|||
# raw_data should appear without JSON quotes |
|||
assert "data: plain text without quotes\n" in text |
|||
# Not JSON-quoted |
|||
assert 'data: "plain text without quotes"' not in text |
|||
|
|||
assert "event: html\n" in text |
|||
assert "data: <div>html fragment</div>\n" in text |
|||
|
|||
assert "event: csv\n" in text |
|||
assert "data: cpu,87.3,1709145600\n" in text |
|||
|
|||
|
|||
def test_data_and_raw_data_mutually_exclusive(): |
|||
"""Cannot set both data and raw_data.""" |
|||
with pytest.raises(ValueError, match="Cannot set both"): |
|||
ServerSentEvent(data="json", raw_data="raw") |
|||
|
|||
|
|||
def test_sse_on_router_included_in_app(client: TestClient): |
|||
response = client.get("/api/events") |
|||
assert response.status_code == 200 |
|||
assert response.headers["content-type"] == "text/event-stream; charset=utf-8" |
|||
data_lines = [ |
|||
line for line in response.text.strip().split("\n") if line.startswith("data: ") |
|||
] |
|||
assert len(data_lines) == 2 |
|||
|
|||
|
|||
# Keepalive ping tests |
|||
|
|||
|
|||
keepalive_app = FastAPI() |
|||
|
|||
|
|||
@keepalive_app.get("/slow-async", response_class=EventSourceResponse) |
|||
async def slow_async_stream(): |
|||
yield {"n": 1} |
|||
# Sleep longer than the (monkeypatched) ping interval so a keepalive |
|||
# comment is emitted before the next item. |
|||
await asyncio.sleep(0.3) |
|||
yield {"n": 2} |
|||
|
|||
|
|||
@keepalive_app.get("/slow-sync", response_class=EventSourceResponse) |
|||
def slow_sync_stream(): |
|||
yield {"n": 1} |
|||
time.sleep(0.3) |
|||
yield {"n": 2} |
|||
|
|||
|
|||
def test_keepalive_ping_async(monkeypatch: pytest.MonkeyPatch): |
|||
monkeypatch.setattr(fastapi.routing, "_PING_INTERVAL", 0.05) |
|||
with TestClient(keepalive_app) as c: |
|||
response = c.get("/slow-async") |
|||
assert response.status_code == 200 |
|||
text = response.text |
|||
# The keepalive comment ": ping" should appear between the two data events |
|||
assert ": ping\n" in text |
|||
data_lines = [line for line in text.split("\n") if line.startswith("data: ")] |
|||
assert len(data_lines) == 2 |
|||
|
|||
|
|||
def test_keepalive_ping_sync(monkeypatch: pytest.MonkeyPatch): |
|||
monkeypatch.setattr(fastapi.routing, "_PING_INTERVAL", 0.05) |
|||
with TestClient(keepalive_app) as c: |
|||
response = c.get("/slow-sync") |
|||
assert response.status_code == 200 |
|||
text = response.text |
|||
assert ": ping\n" in text |
|||
data_lines = [line for line in text.split("\n") if line.startswith("data: ")] |
|||
assert len(data_lines) == 2 |
|||
|
|||
|
|||
def test_no_keepalive_when_fast(client: TestClient): |
|||
"""No keepalive comment when items arrive quickly.""" |
|||
response = client.get("/items/stream") |
|||
assert response.status_code == 200 |
|||
# KEEPALIVE_COMMENT is ": ping\n\n". |
|||
assert ": ping\n" not in response.text |
|||
@ -0,0 +1,191 @@ |
|||
import importlib |
|||
|
|||
import pytest |
|||
from fastapi.testclient import TestClient |
|||
from inline_snapshot import snapshot |
|||
|
|||
|
|||
@pytest.fixture( |
|||
name="client", |
|||
params=[ |
|||
pytest.param("tutorial001_py310"), |
|||
], |
|||
) |
|||
def get_client(request: pytest.FixtureRequest): |
|||
mod = importlib.import_module(f"docs_src.server_sent_events.{request.param}") |
|||
|
|||
client = TestClient(mod.app) |
|||
return client |
|||
|
|||
|
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/items/stream", |
|||
"/items/stream-no-async", |
|||
"/items/stream-no-annotation", |
|||
"/items/stream-no-async-no-annotation", |
|||
], |
|||
) |
|||
def test_stream_items(client: TestClient, path: str): |
|||
response = client.get(path) |
|||
assert response.status_code == 200, response.text |
|||
assert response.headers["content-type"] == "text/event-stream; charset=utf-8" |
|||
data_lines = [ |
|||
line for line in response.text.strip().split("\n") if line.startswith("data: ") |
|||
] |
|||
assert len(data_lines) == 3 |
|||
|
|||
|
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == snapshot( |
|||
{ |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/items/stream": { |
|||
"get": { |
|||
"summary": "Sse Items", |
|||
"operationId": "sse_items_items_stream_get", |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"text/event-stream": { |
|||
"itemSchema": { |
|||
"type": "object", |
|||
"properties": { |
|||
"data": { |
|||
"type": "string", |
|||
"contentMediaType": "application/json", |
|||
"contentSchema": { |
|||
"$ref": "#/components/schemas/Item" |
|||
}, |
|||
}, |
|||
"event": {"type": "string"}, |
|||
"id": {"type": "string"}, |
|||
"retry": { |
|||
"type": "integer", |
|||
"minimum": 0, |
|||
}, |
|||
}, |
|||
"required": ["data"], |
|||
} |
|||
} |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
}, |
|||
"/items/stream-no-async": { |
|||
"get": { |
|||
"summary": "Sse Items No Async", |
|||
"operationId": "sse_items_no_async_items_stream_no_async_get", |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"text/event-stream": { |
|||
"itemSchema": { |
|||
"type": "object", |
|||
"properties": { |
|||
"data": { |
|||
"type": "string", |
|||
"contentMediaType": "application/json", |
|||
"contentSchema": { |
|||
"$ref": "#/components/schemas/Item" |
|||
}, |
|||
}, |
|||
"event": {"type": "string"}, |
|||
"id": {"type": "string"}, |
|||
"retry": { |
|||
"type": "integer", |
|||
"minimum": 0, |
|||
}, |
|||
}, |
|||
"required": ["data"], |
|||
} |
|||
} |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
}, |
|||
"/items/stream-no-annotation": { |
|||
"get": { |
|||
"summary": "Sse Items No Annotation", |
|||
"operationId": "sse_items_no_annotation_items_stream_no_annotation_get", |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"text/event-stream": { |
|||
"itemSchema": { |
|||
"type": "object", |
|||
"properties": { |
|||
"data": {"type": "string"}, |
|||
"event": {"type": "string"}, |
|||
"id": {"type": "string"}, |
|||
"retry": { |
|||
"type": "integer", |
|||
"minimum": 0, |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
}, |
|||
"/items/stream-no-async-no-annotation": { |
|||
"get": { |
|||
"summary": "Sse Items No Async No Annotation", |
|||
"operationId": "sse_items_no_async_no_annotation_items_stream_no_async_no_annotation_get", |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"text/event-stream": { |
|||
"itemSchema": { |
|||
"type": "object", |
|||
"properties": { |
|||
"data": {"type": "string"}, |
|||
"event": {"type": "string"}, |
|||
"id": {"type": "string"}, |
|||
"retry": { |
|||
"type": "integer", |
|||
"minimum": 0, |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
}, |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"Item": { |
|||
"properties": { |
|||
"name": {"type": "string", "title": "Name"}, |
|||
"description": { |
|||
"anyOf": [ |
|||
{"type": "string"}, |
|||
{"type": "null"}, |
|||
], |
|||
"title": "Description", |
|||
}, |
|||
}, |
|||
"type": "object", |
|||
"required": ["name", "description"], |
|||
"title": "Item", |
|||
} |
|||
} |
|||
}, |
|||
} |
|||
) |
|||
@ -0,0 +1,83 @@ |
|||
import importlib |
|||
|
|||
import pytest |
|||
from fastapi.testclient import TestClient |
|||
from inline_snapshot import snapshot |
|||
|
|||
|
|||
@pytest.fixture( |
|||
name="client", |
|||
params=[ |
|||
pytest.param("tutorial002_py310"), |
|||
], |
|||
) |
|||
def get_client(request: pytest.FixtureRequest): |
|||
mod = importlib.import_module(f"docs_src.server_sent_events.{request.param}") |
|||
client = TestClient(mod.app) |
|||
return client |
|||
|
|||
|
|||
def test_stream_items(client: TestClient): |
|||
response = client.get("/items/stream") |
|||
assert response.status_code == 200, response.text |
|||
assert response.headers["content-type"] == "text/event-stream; charset=utf-8" |
|||
|
|||
lines = response.text.strip().split("\n") |
|||
|
|||
# First event is a comment-only event |
|||
assert lines[0] == ": stream of item updates" |
|||
|
|||
# Remaining lines contain event:, data:, id:, retry: fields |
|||
event_lines = [line for line in lines if line.startswith("event: ")] |
|||
assert len(event_lines) == 3 |
|||
assert all(line == "event: item_update" for line in event_lines) |
|||
|
|||
data_lines = [line for line in lines if line.startswith("data: ")] |
|||
assert len(data_lines) == 3 |
|||
|
|||
id_lines = [line for line in lines if line.startswith("id: ")] |
|||
assert id_lines == ["id: 1", "id: 2", "id: 3"] |
|||
|
|||
retry_lines = [line for line in lines if line.startswith("retry: ")] |
|||
assert len(retry_lines) == 3 |
|||
assert all(line == "retry: 5000" for line in retry_lines) |
|||
|
|||
|
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == snapshot( |
|||
{ |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/items/stream": { |
|||
"get": { |
|||
"summary": "Stream Items", |
|||
"operationId": "stream_items_items_stream_get", |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"text/event-stream": { |
|||
"itemSchema": { |
|||
"type": "object", |
|||
"properties": { |
|||
"data": {"type": "string"}, |
|||
"event": {"type": "string"}, |
|||
"id": {"type": "string"}, |
|||
"retry": { |
|||
"type": "integer", |
|||
"minimum": 0, |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
} |
|||
) |
|||
@ -0,0 +1,73 @@ |
|||
import importlib |
|||
|
|||
import pytest |
|||
from fastapi.testclient import TestClient |
|||
from inline_snapshot import snapshot |
|||
|
|||
|
|||
@pytest.fixture( |
|||
name="client", |
|||
params=[ |
|||
pytest.param("tutorial003_py310"), |
|||
], |
|||
) |
|||
def get_client(request: pytest.FixtureRequest): |
|||
mod = importlib.import_module(f"docs_src.server_sent_events.{request.param}") |
|||
client = TestClient(mod.app) |
|||
return client |
|||
|
|||
|
|||
def test_stream_logs(client: TestClient): |
|||
response = client.get("/logs/stream") |
|||
assert response.status_code == 200, response.text |
|||
assert response.headers["content-type"] == "text/event-stream; charset=utf-8" |
|||
|
|||
data_lines = [ |
|||
line for line in response.text.strip().split("\n") if line.startswith("data: ") |
|||
] |
|||
assert len(data_lines) == 3 |
|||
|
|||
# raw_data is sent without JSON encoding (no quotes around the string) |
|||
assert data_lines[0] == "data: 2025-01-01 INFO Application started" |
|||
assert data_lines[1] == "data: 2025-01-01 DEBUG Connected to database" |
|||
assert data_lines[2] == "data: 2025-01-01 WARN High memory usage detected" |
|||
|
|||
|
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == snapshot( |
|||
{ |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/logs/stream": { |
|||
"get": { |
|||
"summary": "Stream Logs", |
|||
"operationId": "stream_logs_logs_stream_get", |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"text/event-stream": { |
|||
"itemSchema": { |
|||
"type": "object", |
|||
"properties": { |
|||
"data": {"type": "string"}, |
|||
"event": {"type": "string"}, |
|||
"id": {"type": "string"}, |
|||
"retry": { |
|||
"type": "integer", |
|||
"minimum": 0, |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
} |
|||
) |
|||
@ -0,0 +1,164 @@ |
|||
import importlib |
|||
|
|||
import pytest |
|||
from fastapi.testclient import TestClient |
|||
from inline_snapshot import snapshot |
|||
|
|||
|
|||
@pytest.fixture( |
|||
name="client", |
|||
params=[ |
|||
pytest.param("tutorial004_py310"), |
|||
], |
|||
) |
|||
def get_client(request: pytest.FixtureRequest): |
|||
mod = importlib.import_module(f"docs_src.server_sent_events.{request.param}") |
|||
client = TestClient(mod.app) |
|||
return client |
|||
|
|||
|
|||
def test_stream_all_items(client: TestClient): |
|||
response = client.get("/items/stream") |
|||
assert response.status_code == 200, response.text |
|||
|
|||
data_lines = [ |
|||
line for line in response.text.strip().split("\n") if line.startswith("data: ") |
|||
] |
|||
assert len(data_lines) == 3 |
|||
|
|||
id_lines = [ |
|||
line for line in response.text.strip().split("\n") if line.startswith("id: ") |
|||
] |
|||
assert id_lines == ["id: 0", "id: 1", "id: 2"] |
|||
|
|||
|
|||
def test_resume_from_last_event_id(client: TestClient): |
|||
response = client.get( |
|||
"/items/stream", |
|||
headers={"last-event-id": "0"}, |
|||
) |
|||
assert response.status_code == 200, response.text |
|||
|
|||
data_lines = [ |
|||
line for line in response.text.strip().split("\n") if line.startswith("data: ") |
|||
] |
|||
assert len(data_lines) == 2 |
|||
|
|||
id_lines = [ |
|||
line for line in response.text.strip().split("\n") if line.startswith("id: ") |
|||
] |
|||
assert id_lines == ["id: 1", "id: 2"] |
|||
|
|||
|
|||
def test_resume_from_last_item(client: TestClient): |
|||
response = client.get( |
|||
"/items/stream", |
|||
headers={"last-event-id": "1"}, |
|||
) |
|||
assert response.status_code == 200, response.text |
|||
|
|||
data_lines = [ |
|||
line for line in response.text.strip().split("\n") if line.startswith("data: ") |
|||
] |
|||
assert len(data_lines) == 1 |
|||
|
|||
id_lines = [ |
|||
line for line in response.text.strip().split("\n") if line.startswith("id: ") |
|||
] |
|||
assert id_lines == ["id: 2"] |
|||
|
|||
|
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == snapshot( |
|||
{ |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/items/stream": { |
|||
"get": { |
|||
"summary": "Stream Items", |
|||
"operationId": "stream_items_items_stream_get", |
|||
"parameters": [ |
|||
{ |
|||
"name": "last-event-id", |
|||
"in": "header", |
|||
"required": False, |
|||
"schema": { |
|||
"anyOf": [{"type": "integer"}, {"type": "null"}], |
|||
"title": "Last-Event-Id", |
|||
}, |
|||
} |
|||
], |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"text/event-stream": { |
|||
"itemSchema": { |
|||
"type": "object", |
|||
"properties": { |
|||
"data": {"type": "string"}, |
|||
"event": {"type": "string"}, |
|||
"id": {"type": "string"}, |
|||
"retry": { |
|||
"type": "integer", |
|||
"minimum": 0, |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"HTTPValidationError": { |
|||
"properties": { |
|||
"detail": { |
|||
"items": { |
|||
"$ref": "#/components/schemas/ValidationError" |
|||
}, |
|||
"type": "array", |
|||
"title": "Detail", |
|||
} |
|||
}, |
|||
"type": "object", |
|||
"title": "HTTPValidationError", |
|||
}, |
|||
"ValidationError": { |
|||
"properties": { |
|||
"loc": { |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
"type": "array", |
|||
"title": "Location", |
|||
}, |
|||
"msg": {"type": "string", "title": "Message"}, |
|||
"type": {"type": "string", "title": "Error Type"}, |
|||
"input": {"title": "Input"}, |
|||
"ctx": {"type": "object", "title": "Context"}, |
|||
}, |
|||
"type": "object", |
|||
"required": ["loc", "msg", "type"], |
|||
"title": "ValidationError", |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
) |
|||
@ -0,0 +1,141 @@ |
|||
import importlib |
|||
|
|||
import pytest |
|||
from fastapi.testclient import TestClient |
|||
from inline_snapshot import snapshot |
|||
|
|||
|
|||
@pytest.fixture( |
|||
name="client", |
|||
params=[ |
|||
pytest.param("tutorial005_py310"), |
|||
], |
|||
) |
|||
def get_client(request: pytest.FixtureRequest): |
|||
mod = importlib.import_module(f"docs_src.server_sent_events.{request.param}") |
|||
client = TestClient(mod.app) |
|||
return client |
|||
|
|||
|
|||
def test_stream_chat(client: TestClient): |
|||
response = client.post( |
|||
"/chat/stream", |
|||
json={"text": "hello world"}, |
|||
) |
|||
assert response.status_code == 200, response.text |
|||
assert response.headers["content-type"] == "text/event-stream; charset=utf-8" |
|||
|
|||
lines = response.text.strip().split("\n") |
|||
|
|||
event_lines = [line for line in lines if line.startswith("event: ")] |
|||
assert event_lines == [ |
|||
"event: token", |
|||
"event: token", |
|||
"event: done", |
|||
] |
|||
|
|||
data_lines = [line for line in lines if line.startswith("data: ")] |
|||
assert data_lines == [ |
|||
'data: "hello"', |
|||
'data: "world"', |
|||
"data: [DONE]", |
|||
] |
|||
|
|||
|
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == snapshot( |
|||
{ |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/chat/stream": { |
|||
"post": { |
|||
"summary": "Stream Chat", |
|||
"operationId": "stream_chat_chat_stream_post", |
|||
"requestBody": { |
|||
"content": { |
|||
"application/json": { |
|||
"schema": {"$ref": "#/components/schemas/Prompt"} |
|||
} |
|||
}, |
|||
"required": True, |
|||
}, |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"text/event-stream": { |
|||
"itemSchema": { |
|||
"type": "object", |
|||
"properties": { |
|||
"data": {"type": "string"}, |
|||
"event": {"type": "string"}, |
|||
"id": {"type": "string"}, |
|||
"retry": { |
|||
"type": "integer", |
|||
"minimum": 0, |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"HTTPValidationError": { |
|||
"properties": { |
|||
"detail": { |
|||
"items": { |
|||
"$ref": "#/components/schemas/ValidationError" |
|||
}, |
|||
"type": "array", |
|||
"title": "Detail", |
|||
} |
|||
}, |
|||
"type": "object", |
|||
"title": "HTTPValidationError", |
|||
}, |
|||
"Prompt": { |
|||
"properties": {"text": {"type": "string", "title": "Text"}}, |
|||
"type": "object", |
|||
"required": ["text"], |
|||
"title": "Prompt", |
|||
}, |
|||
"ValidationError": { |
|||
"properties": { |
|||
"loc": { |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
"type": "array", |
|||
"title": "Location", |
|||
}, |
|||
"msg": {"type": "string", "title": "Message"}, |
|||
"type": {"type": "string", "title": "Error Type"}, |
|||
"input": {"title": "Input"}, |
|||
"ctx": {"type": "object", "title": "Context"}, |
|||
}, |
|||
"type": "object", |
|||
"required": ["loc", "msg", "type"], |
|||
"title": "ValidationError", |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
) |
|||
Loading…
Reference in new issue