From c23c072572ac4e0a2f504b5855155f507c3e42dc Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Thu, 27 Nov 2025 09:07:06 +0100 Subject: [PATCH] Add variants for `openapi-callbacks/tutorial001` --- docs/en/docs/advanced/openapi-callbacks.md | 8 +-- .../openapi_callbacks/tutorial001_py310.py | 51 +++++++++++++++++++ .../test_tutorial001.py | 35 ++++++++++--- 3 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 docs_src/openapi_callbacks/tutorial001_py310.py diff --git a/docs/en/docs/advanced/openapi-callbacks.md b/docs/en/docs/advanced/openapi-callbacks.md index 059d893c2..5bd7c2cfd 100644 --- a/docs/en/docs/advanced/openapi-callbacks.md +++ b/docs/en/docs/advanced/openapi-callbacks.md @@ -31,7 +31,7 @@ It will have a *path operation* that will receive an `Invoice` body, and a query This part is pretty normal, most of the code is probably already familiar to you: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[9:13,36:53] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *} /// tip @@ -90,7 +90,7 @@ Temporarily adopting this point of view (of the *external developer*) can help y First create a new `APIRouter` that will contain one or more callbacks. -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[3,25] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *} ### Create the callback *path operation* { #create-the-callback-path-operation } @@ -101,7 +101,7 @@ It should look just like a normal FastAPI *path operation*: * It should probably have a declaration of the body it should receive, e.g. `body: InvoiceEvent`. * And it could also have a declaration of the response it should return, e.g. `response_model=InvoiceEventReceived`. -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[16:18,21:22,28:32] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *} There are 2 main differences from a normal *path operation*: @@ -169,7 +169,7 @@ At this point you have the *callback path operation(s)* needed (the one(s) that Now use the parameter `callbacks` in *your API's path operation decorator* to pass the attribute `.routes` (that's actually just a `list` of routes/*path operations*) from that callback router: -{* ../../docs_src/openapi_callbacks/tutorial001.py hl[35] *} +{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *} /// tip diff --git a/docs_src/openapi_callbacks/tutorial001_py310.py b/docs_src/openapi_callbacks/tutorial001_py310.py new file mode 100644 index 000000000..3efe0ee25 --- /dev/null +++ b/docs_src/openapi_callbacks/tutorial001_py310.py @@ -0,0 +1,51 @@ +from fastapi import APIRouter, FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Invoice(BaseModel): + id: str + title: str | None = None + customer: str + total: float + + +class InvoiceEvent(BaseModel): + description: str + paid: bool + + +class InvoiceEventReceived(BaseModel): + ok: bool + + +invoices_callback_router = APIRouter() + + +@invoices_callback_router.post( + "{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived +) +def invoice_notification(body: InvoiceEvent): + pass + + +@app.post("/invoices/", callbacks=invoices_callback_router.routes) +def create_invoice(invoice: Invoice, callback_url: HttpUrl | None = None): + """ + Create an invoice. + + This will (let's imagine) let the API user (some external developer) create an + invoice. + + And this path operation will: + + * Send the invoice to the client. + * Collect the money from the client. + * Send a notification back to the API user (the external developer), as a callback. + * At this point is that the API will somehow send a POST request to the + external API with the notification of the invoice event + (e.g. "payment successful"). + """ + # Send the invoice, collect the money, send the notification (the callback) + return {"msg": "Invoice received"} diff --git a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py index 73af420ae..2df2b9889 100644 --- a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py @@ -1,12 +1,33 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.openapi_callbacks.tutorial001 import app, invoice_notification - -client = TestClient(app) +from tests.utils import needs_py310 -def test_get(): +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001"), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_mod(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.openapi_callbacks.{request.param}") + return mod + + +@pytest.fixture(name="client") +def get_client(mod: ModuleType): + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_get(client: TestClient): response = client.post( "/invoices/", json={"id": "fooinvoice", "customer": "John", "total": 5.3} ) @@ -14,12 +35,12 @@ def test_get(): assert response.json() == {"msg": "Invoice received"} -def test_dummy_callback(): +def test_dummy_callback(mod: ModuleType): # Just for coverage - invoice_notification({}) + mod.invoice_notification({}) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == {