|
|
|
@ -1,10 +1,50 @@ |
|
|
|
import json |
|
|
|
from collections.abc import AsyncIterable |
|
|
|
from typing import Any, cast |
|
|
|
|
|
|
|
import pytest |
|
|
|
from fastapi import Depends, FastAPI, Response |
|
|
|
from fastapi.responses import EventSourceResponse, StreamingResponse |
|
|
|
from fastapi.testclient import TestClient |
|
|
|
from inline_snapshot import snapshot |
|
|
|
|
|
|
|
SSE_RESPONSE = { |
|
|
|
"description": "Successful Response", |
|
|
|
"content": { |
|
|
|
"text/event-stream": { |
|
|
|
"itemSchema": { |
|
|
|
"type": "object", |
|
|
|
"properties": { |
|
|
|
"data": { |
|
|
|
"type": "string", |
|
|
|
"contentMediaType": "application/json", |
|
|
|
"contentSchema": { |
|
|
|
"type": "object", |
|
|
|
"additionalProperties": {"type": "string"}, |
|
|
|
"title": "SSE stream item", |
|
|
|
}, |
|
|
|
}, |
|
|
|
"event": {"type": "string"}, |
|
|
|
"id": {"type": "string"}, |
|
|
|
"retry": {"type": "integer", "minimum": 0}, |
|
|
|
}, |
|
|
|
"required": ["data"], |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
JSONL_RESPONSE = { |
|
|
|
"description": "Successful Response", |
|
|
|
"content": { |
|
|
|
"application/jsonl": { |
|
|
|
"itemSchema": { |
|
|
|
"type": "object", |
|
|
|
"additionalProperties": {"type": "string"}, |
|
|
|
"title": "JSONL stream item", |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
app = FastAPI() |
|
|
|
|
|
|
|
@ -13,53 +53,80 @@ def set_accepted(response: Response) -> None: |
|
|
|
response.status_code = 202 |
|
|
|
|
|
|
|
|
|
|
|
@app.post("/sse-created", response_class=EventSourceResponse, status_code=201) |
|
|
|
async def sse_created() -> AsyncIterable[dict[str, str]]: |
|
|
|
@app.post("/sse", response_class=EventSourceResponse, status_code=201) |
|
|
|
async def sse() -> AsyncIterable[dict[str, str]]: |
|
|
|
yield {"message": "created"} |
|
|
|
|
|
|
|
|
|
|
|
@app.post("/jsonl-created", status_code=201) |
|
|
|
async def jsonl_created() -> AsyncIterable[dict[str, str]]: |
|
|
|
@app.post("/jsonl", status_code=201) |
|
|
|
async def jsonl() -> AsyncIterable[dict[str, str]]: |
|
|
|
yield {"message": "created"} |
|
|
|
|
|
|
|
|
|
|
|
@app.get("/sse-dependency", response_class=EventSourceResponse) |
|
|
|
@app.post("/raw", response_class=StreamingResponse, status_code=201) |
|
|
|
async def raw() -> AsyncIterable[str]: |
|
|
|
yield "accepted" |
|
|
|
|
|
|
|
|
|
|
|
@app.post( |
|
|
|
"/sse-dependency", |
|
|
|
response_class=EventSourceResponse, |
|
|
|
responses={202: SSE_RESPONSE}, |
|
|
|
) |
|
|
|
async def sse_dependency( |
|
|
|
accepted: None = Depends(set_accepted), |
|
|
|
) -> AsyncIterable[dict[str, str]]: |
|
|
|
yield {"message": "accepted"} |
|
|
|
|
|
|
|
|
|
|
|
@app.get("/jsonl-dependency") |
|
|
|
@app.post("/jsonl-dependency", responses={202: JSONL_RESPONSE}) |
|
|
|
async def jsonl_dependency( |
|
|
|
accepted: None = Depends(set_accepted), |
|
|
|
) -> AsyncIterable[dict[str, str]]: |
|
|
|
yield {"message": "accepted"} |
|
|
|
|
|
|
|
|
|
|
|
@app.get("/raw-dependency", response_class=StreamingResponse) |
|
|
|
@app.post( |
|
|
|
"/raw-dependency", |
|
|
|
response_class=StreamingResponse, |
|
|
|
responses={202: {"description": "Accepted"}}, |
|
|
|
) |
|
|
|
async def raw_dependency( |
|
|
|
accepted: None = Depends(set_accepted), |
|
|
|
) -> AsyncIterable[str]: |
|
|
|
yield "accepted" |
|
|
|
|
|
|
|
|
|
|
|
@app.post("/sse-created-override", response_class=EventSourceResponse, status_code=201) |
|
|
|
async def sse_created_override( |
|
|
|
@app.post( |
|
|
|
"/sse-dependency-override", |
|
|
|
response_class=EventSourceResponse, |
|
|
|
status_code=201, |
|
|
|
responses={202: SSE_RESPONSE}, |
|
|
|
) |
|
|
|
async def sse_dependency_override( |
|
|
|
accepted: None = Depends(set_accepted), |
|
|
|
) -> AsyncIterable[dict[str, str]]: |
|
|
|
yield {"message": "overridden"} |
|
|
|
|
|
|
|
|
|
|
|
@app.post("/jsonl-created-override", status_code=201) |
|
|
|
async def jsonl_created_override( |
|
|
|
@app.post( |
|
|
|
"/jsonl-dependency-override", |
|
|
|
status_code=201, |
|
|
|
responses={202: JSONL_RESPONSE}, |
|
|
|
) |
|
|
|
async def jsonl_dependency_override( |
|
|
|
accepted: None = Depends(set_accepted), |
|
|
|
) -> AsyncIterable[dict[str, str]]: |
|
|
|
yield {"message": "overridden"} |
|
|
|
|
|
|
|
|
|
|
|
@app.post("/raw-created-override", response_class=StreamingResponse, status_code=201) |
|
|
|
async def raw_created_override( |
|
|
|
@app.post( |
|
|
|
"/raw-dependency-override", |
|
|
|
response_class=StreamingResponse, |
|
|
|
status_code=201, |
|
|
|
responses={202: {"description": "Accepted"}}, |
|
|
|
) |
|
|
|
async def raw_dependency_override( |
|
|
|
accepted: None = Depends(set_accepted), |
|
|
|
) -> AsyncIterable[str]: |
|
|
|
yield "overridden" |
|
|
|
@ -68,91 +135,318 @@ async def raw_created_override( |
|
|
|
client = TestClient(app) |
|
|
|
|
|
|
|
|
|
|
|
def get_sse_data(response_text: str) -> list[dict[str, str]]: |
|
|
|
data_lines = [ |
|
|
|
line.removeprefix("data: ") |
|
|
|
for line in response_text.splitlines() |
|
|
|
if line.startswith("data: ") |
|
|
|
] |
|
|
|
return [json.loads(line) for line in data_lines] |
|
|
|
|
|
|
|
|
|
|
|
def get_jsonl_data(response_text: str) -> list[dict[str, str]]: |
|
|
|
return [json.loads(line) for line in response_text.splitlines()] |
|
|
|
|
|
|
|
|
|
|
|
def test_sse_stream_honors_declared_status_code() -> None: |
|
|
|
response = client.post("/sse-created") |
|
|
|
|
|
|
|
assert response.status_code == 201 |
|
|
|
assert response.headers["content-type"] == "text/event-stream; charset=utf-8" |
|
|
|
assert get_sse_data(response.text) == [{"message": "created"}] |
|
|
|
|
|
|
|
|
|
|
|
def test_jsonl_stream_honors_declared_status_code() -> None: |
|
|
|
response = client.post("/jsonl-created") |
|
|
|
|
|
|
|
assert response.status_code == 201 |
|
|
|
assert response.headers["content-type"] == "application/jsonl" |
|
|
|
assert get_jsonl_data(response.text) == [{"message": "created"}] |
|
|
|
|
|
|
|
|
|
|
|
def test_sse_stream_honors_dependency_status_code() -> None: |
|
|
|
response = client.get("/sse-dependency") |
|
|
|
|
|
|
|
assert response.status_code == 202 |
|
|
|
assert get_sse_data(response.text) == [{"message": "accepted"}] |
|
|
|
|
|
|
|
|
|
|
|
def test_jsonl_stream_honors_dependency_status_code() -> None: |
|
|
|
response = client.get("/jsonl-dependency") |
|
|
|
|
|
|
|
assert response.status_code == 202 |
|
|
|
assert get_jsonl_data(response.text) == [{"message": "accepted"}] |
|
|
|
|
|
|
|
|
|
|
|
def test_raw_stream_still_honors_dependency_status_code() -> None: |
|
|
|
response = client.get("/raw-dependency") |
|
|
|
|
|
|
|
assert response.status_code == 202 |
|
|
|
assert response.text == "accepted" |
|
|
|
|
|
|
|
|
|
|
|
def test_sse_stream_dependency_overrides_declared_status_code() -> None: |
|
|
|
response = client.post("/sse-created-override") |
|
|
|
|
|
|
|
assert response.status_code == 202 |
|
|
|
assert get_sse_data(response.text) == [{"message": "overridden"}] |
|
|
|
|
|
|
|
|
|
|
|
def test_jsonl_stream_dependency_overrides_declared_status_code() -> None: |
|
|
|
response = client.post("/jsonl-created-override") |
|
|
|
|
|
|
|
assert response.status_code == 202 |
|
|
|
assert get_jsonl_data(response.text) == [{"message": "overridden"}] |
|
|
|
|
|
|
|
|
|
|
|
def test_raw_stream_dependency_overrides_declared_status_code() -> None: |
|
|
|
response = client.post("/raw-created-override") |
|
|
|
|
|
|
|
assert response.status_code == 202 |
|
|
|
assert response.text == "overridden" |
|
|
|
|
|
|
|
|
|
|
|
def test_stream_status_codes_match_openapi() -> None: |
|
|
|
schema = client.get("/openapi.json").json() |
|
|
|
|
|
|
|
assert response_status_codes(schema, "/sse-created", "post") == ["201"] |
|
|
|
assert response_status_codes(schema, "/jsonl-created", "post") == ["201"] |
|
|
|
|
|
|
|
assert response_status_codes(schema, "/sse-created-override", "post") == ["201"] |
|
|
|
assert response_status_codes(schema, "/jsonl-created-override", "post") == ["201"] |
|
|
|
assert response_status_codes(schema, "/raw-created-override", "post") == ["201"] |
|
|
|
|
|
|
|
|
|
|
|
def response_status_codes(schema: dict[str, Any], path: str, method: str) -> list[str]: |
|
|
|
paths = cast(dict[str, Any], schema["paths"]) |
|
|
|
route = cast(dict[str, Any], paths[path]) |
|
|
|
operation = cast(dict[str, Any], route[method]) |
|
|
|
responses = cast(dict[str, Any], operation["responses"]) |
|
|
|
return sorted(responses) |
|
|
|
@pytest.mark.parametrize( |
|
|
|
"path,expected_status_code", |
|
|
|
[ |
|
|
|
("/sse", 201), |
|
|
|
("/jsonl", 201), |
|
|
|
("/raw", 201), |
|
|
|
("/sse-dependency", 202), |
|
|
|
("/jsonl-dependency", 202), |
|
|
|
("/raw-dependency", 202), |
|
|
|
("/sse-dependency-override", 202), |
|
|
|
("/jsonl-dependency-override", 202), |
|
|
|
("/raw-dependency-override", 202), |
|
|
|
], |
|
|
|
) |
|
|
|
def test_status_code(path: str, expected_status_code: int) -> None: |
|
|
|
response = client.post(path) |
|
|
|
assert response.status_code == expected_status_code |
|
|
|
|
|
|
|
|
|
|
|
def test_openapi() -> None: |
|
|
|
openapi = app.openapi() |
|
|
|
|
|
|
|
assert openapi == snapshot( |
|
|
|
{ |
|
|
|
"openapi": "3.1.0", |
|
|
|
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|
|
|
"paths": { |
|
|
|
"/sse": { |
|
|
|
"post": { |
|
|
|
"summary": "Sse", |
|
|
|
"operationId": "sse_sse_post", |
|
|
|
"responses": { |
|
|
|
"201": { |
|
|
|
"description": "Successful Response", |
|
|
|
"content": { |
|
|
|
"text/event-stream": { |
|
|
|
"itemSchema": { |
|
|
|
"type": "object", |
|
|
|
"properties": { |
|
|
|
"data": { |
|
|
|
"type": "string", |
|
|
|
"contentMediaType": "application/json", |
|
|
|
"contentSchema": { |
|
|
|
"type": "object", |
|
|
|
"additionalProperties": { |
|
|
|
"type": "string" |
|
|
|
}, |
|
|
|
"title": "Streamitem Sse Sse Post", |
|
|
|
}, |
|
|
|
}, |
|
|
|
"event": {"type": "string"}, |
|
|
|
"id": {"type": "string"}, |
|
|
|
"retry": { |
|
|
|
"type": "integer", |
|
|
|
"minimum": 0, |
|
|
|
}, |
|
|
|
}, |
|
|
|
"required": ["data"], |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
} |
|
|
|
}, |
|
|
|
} |
|
|
|
}, |
|
|
|
"/jsonl": { |
|
|
|
"post": { |
|
|
|
"summary": "Jsonl", |
|
|
|
"operationId": "jsonl_jsonl_post", |
|
|
|
"responses": { |
|
|
|
"201": { |
|
|
|
"description": "Successful Response", |
|
|
|
"content": { |
|
|
|
"application/jsonl": { |
|
|
|
"itemSchema": { |
|
|
|
"type": "object", |
|
|
|
"additionalProperties": {"type": "string"}, |
|
|
|
"title": "Streamitem Jsonl Jsonl Post", |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
} |
|
|
|
}, |
|
|
|
} |
|
|
|
}, |
|
|
|
"/raw": { |
|
|
|
"post": { |
|
|
|
"summary": "Raw", |
|
|
|
"operationId": "raw_raw_post", |
|
|
|
"responses": {"201": {"description": "Successful Response"}}, |
|
|
|
} |
|
|
|
}, |
|
|
|
"/sse-dependency": { |
|
|
|
"post": { |
|
|
|
"summary": "Sse Dependency", |
|
|
|
"operationId": "sse_dependency_sse_dependency_post", |
|
|
|
"responses": { |
|
|
|
"200": { |
|
|
|
"description": "Successful Response", |
|
|
|
"content": { |
|
|
|
"text/event-stream": { |
|
|
|
"itemSchema": { |
|
|
|
"type": "object", |
|
|
|
"properties": { |
|
|
|
"data": { |
|
|
|
"type": "string", |
|
|
|
"contentMediaType": "application/json", |
|
|
|
"contentSchema": { |
|
|
|
"type": "object", |
|
|
|
"additionalProperties": { |
|
|
|
"type": "string" |
|
|
|
}, |
|
|
|
"title": "Streamitem Sse Dependency Sse Dependency Post", |
|
|
|
}, |
|
|
|
}, |
|
|
|
"event": {"type": "string"}, |
|
|
|
"id": {"type": "string"}, |
|
|
|
"retry": { |
|
|
|
"type": "integer", |
|
|
|
"minimum": 0, |
|
|
|
}, |
|
|
|
}, |
|
|
|
"required": ["data"], |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
"202": { |
|
|
|
"description": "Successful Response", |
|
|
|
"content": { |
|
|
|
"text/event-stream": { |
|
|
|
"itemSchema": { |
|
|
|
"type": "object", |
|
|
|
"properties": { |
|
|
|
"data": { |
|
|
|
"type": "string", |
|
|
|
"contentMediaType": "application/json", |
|
|
|
"contentSchema": { |
|
|
|
"type": "object", |
|
|
|
"additionalProperties": { |
|
|
|
"type": "string" |
|
|
|
}, |
|
|
|
"title": "SSE stream item", |
|
|
|
}, |
|
|
|
}, |
|
|
|
"event": {"type": "string"}, |
|
|
|
"id": {"type": "string"}, |
|
|
|
"retry": { |
|
|
|
"type": "integer", |
|
|
|
"minimum": 0, |
|
|
|
}, |
|
|
|
}, |
|
|
|
"required": ["data"], |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
}, |
|
|
|
"/jsonl-dependency": { |
|
|
|
"post": { |
|
|
|
"summary": "Jsonl Dependency", |
|
|
|
"operationId": "jsonl_dependency_jsonl_dependency_post", |
|
|
|
"responses": { |
|
|
|
"200": { |
|
|
|
"description": "Successful Response", |
|
|
|
"content": { |
|
|
|
"application/jsonl": { |
|
|
|
"itemSchema": { |
|
|
|
"type": "object", |
|
|
|
"additionalProperties": {"type": "string"}, |
|
|
|
"title": "Streamitem Jsonl Dependency Jsonl Dependency Post", |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
"202": { |
|
|
|
"description": "Successful Response", |
|
|
|
"content": { |
|
|
|
"application/jsonl": { |
|
|
|
"itemSchema": { |
|
|
|
"type": "object", |
|
|
|
"additionalProperties": {"type": "string"}, |
|
|
|
"title": "JSONL stream item", |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
}, |
|
|
|
"/raw-dependency": { |
|
|
|
"post": { |
|
|
|
"summary": "Raw Dependency", |
|
|
|
"operationId": "raw_dependency_raw_dependency_post", |
|
|
|
"responses": { |
|
|
|
"200": {"description": "Successful Response"}, |
|
|
|
"202": {"description": "Accepted"}, |
|
|
|
}, |
|
|
|
} |
|
|
|
}, |
|
|
|
"/sse-dependency-override": { |
|
|
|
"post": { |
|
|
|
"summary": "Sse Dependency Override", |
|
|
|
"operationId": "sse_dependency_override_sse_dependency_override_post", |
|
|
|
"responses": { |
|
|
|
"201": { |
|
|
|
"description": "Successful Response", |
|
|
|
"content": { |
|
|
|
"text/event-stream": { |
|
|
|
"itemSchema": { |
|
|
|
"type": "object", |
|
|
|
"properties": { |
|
|
|
"data": { |
|
|
|
"type": "string", |
|
|
|
"contentMediaType": "application/json", |
|
|
|
"contentSchema": { |
|
|
|
"type": "object", |
|
|
|
"additionalProperties": { |
|
|
|
"type": "string" |
|
|
|
}, |
|
|
|
"title": "Streamitem Sse Dependency Override Sse Dependency Override Post", |
|
|
|
}, |
|
|
|
}, |
|
|
|
"event": {"type": "string"}, |
|
|
|
"id": {"type": "string"}, |
|
|
|
"retry": { |
|
|
|
"type": "integer", |
|
|
|
"minimum": 0, |
|
|
|
}, |
|
|
|
}, |
|
|
|
"required": ["data"], |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
"202": { |
|
|
|
"description": "Successful Response", |
|
|
|
"content": { |
|
|
|
"text/event-stream": { |
|
|
|
"itemSchema": { |
|
|
|
"type": "object", |
|
|
|
"properties": { |
|
|
|
"data": { |
|
|
|
"type": "string", |
|
|
|
"contentMediaType": "application/json", |
|
|
|
"contentSchema": { |
|
|
|
"type": "object", |
|
|
|
"additionalProperties": { |
|
|
|
"type": "string" |
|
|
|
}, |
|
|
|
"title": "SSE stream item", |
|
|
|
}, |
|
|
|
}, |
|
|
|
"event": {"type": "string"}, |
|
|
|
"id": {"type": "string"}, |
|
|
|
"retry": { |
|
|
|
"type": "integer", |
|
|
|
"minimum": 0, |
|
|
|
}, |
|
|
|
}, |
|
|
|
"required": ["data"], |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
}, |
|
|
|
"/jsonl-dependency-override": { |
|
|
|
"post": { |
|
|
|
"summary": "Jsonl Dependency Override", |
|
|
|
"operationId": "jsonl_dependency_override_jsonl_dependency_override_post", |
|
|
|
"responses": { |
|
|
|
"201": { |
|
|
|
"description": "Successful Response", |
|
|
|
"content": { |
|
|
|
"application/jsonl": { |
|
|
|
"itemSchema": { |
|
|
|
"type": "object", |
|
|
|
"additionalProperties": {"type": "string"}, |
|
|
|
"title": "Streamitem Jsonl Dependency Override Jsonl Dependency Override Post", |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
"202": { |
|
|
|
"description": "Successful Response", |
|
|
|
"content": { |
|
|
|
"application/jsonl": { |
|
|
|
"itemSchema": { |
|
|
|
"type": "object", |
|
|
|
"additionalProperties": {"type": "string"}, |
|
|
|
"title": "JSONL stream item", |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
}, |
|
|
|
"/raw-dependency-override": { |
|
|
|
"post": { |
|
|
|
"summary": "Raw Dependency Override", |
|
|
|
"operationId": "raw_dependency_override_raw_dependency_override_post", |
|
|
|
"responses": { |
|
|
|
"201": {"description": "Successful Response"}, |
|
|
|
"202": {"description": "Accepted"}, |
|
|
|
}, |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
) |
|
|
|
|