mirror of https://github.com/tiangolo/fastapi.git
🐛 Fix callback handling in sub-routers (#792)
This commit is contained in:
parent
7b31e52766
commit
cb1410426e
|
|
@ -25,8 +25,7 @@ invoices_callback_router = APIRouter(default_response_class=JSONResponse)
|
||||||
|
|
||||||
|
|
||||||
@invoices_callback_router.post(
|
@invoices_callback_router.post(
|
||||||
"{$callback_url}/invoices/{$request.body.id}",
|
"{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived,
|
||||||
response_model=InvoiceEventReceived,
|
|
||||||
)
|
)
|
||||||
def invoice_notification(body: InvoiceEvent):
|
def invoice_notification(body: InvoiceEvent):
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,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:
|
This part is pretty normal, most of the code is probably already familiar to you:
|
||||||
|
|
||||||
```Python hl_lines="8 9 10 11 12 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54"
|
```Python hl_lines="8 9 10 11 12 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53"
|
||||||
{!./src/openapi_callbacks/tutorial001.py!}
|
{!./src/openapi_callbacks/tutorial001.py!}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -103,7 +103,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`.
|
* 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`.
|
* And it could also have a declaration of the response it should return, e.g. `response_model=InvoiceEventReceived`.
|
||||||
|
|
||||||
```Python hl_lines="15 16 17 20 21 27 28 29 30 31 32"
|
```Python hl_lines="15 16 17 20 21 27 28 29 30 31"
|
||||||
{!./src/openapi_callbacks/tutorial001.py!}
|
{!./src/openapi_callbacks/tutorial001.py!}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -170,7 +170,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:
|
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:
|
||||||
|
|
||||||
```Python hl_lines="35"
|
```Python hl_lines="34"
|
||||||
{!./src/openapi_callbacks/tutorial001.py!}
|
{!./src/openapi_callbacks/tutorial001.py!}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -554,6 +554,7 @@ class APIRouter(routing.Router):
|
||||||
response_class=route.response_class or default_response_class,
|
response_class=route.response_class or default_response_class,
|
||||||
name=route.name,
|
name=route.name,
|
||||||
route_class_override=type(route),
|
route_class_override=type(route),
|
||||||
|
callbacks=route.callbacks,
|
||||||
)
|
)
|
||||||
elif isinstance(route, routing.Route):
|
elif isinstance(route, routing.Route):
|
||||||
self.add_route(
|
self.add_route(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
from fastapi import APIRouter, FastAPI
|
||||||
|
from pydantic import BaseModel, HttpUrl
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
class Invoice(BaseModel):
|
||||||
|
id: str
|
||||||
|
title: str = None
|
||||||
|
customer: str
|
||||||
|
total: float
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceEvent(BaseModel):
|
||||||
|
description: str
|
||||||
|
paid: bool
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceEventReceived(BaseModel):
|
||||||
|
ok: bool
|
||||||
|
|
||||||
|
|
||||||
|
invoices_callback_router = APIRouter(default_response_class=JSONResponse)
|
||||||
|
|
||||||
|
|
||||||
|
@invoices_callback_router.post(
|
||||||
|
"{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived,
|
||||||
|
)
|
||||||
|
def invoice_notification(body: InvoiceEvent):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
subrouter = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@subrouter.post("/invoices/", callbacks=invoices_callback_router.routes)
|
||||||
|
def create_invoice(invoice: Invoice, callback_url: HttpUrl = 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"}
|
||||||
|
|
||||||
|
|
||||||
|
app.include_router(subrouter)
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
openapi_schema = {
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||||
|
"paths": {
|
||||||
|
"/invoices/": {
|
||||||
|
"post": {
|
||||||
|
"summary": "Create Invoice",
|
||||||
|
"description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").',
|
||||||
|
"operationId": "create_invoice_invoices__post",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": False,
|
||||||
|
"schema": {
|
||||||
|
"title": "Callback Url",
|
||||||
|
"maxLength": 2083,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
},
|
||||||
|
"name": "callback_url",
|
||||||
|
"in": "query",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {"$ref": "#/components/schemas/Invoice"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"callbacks": {
|
||||||
|
"invoice_notification": {
|
||||||
|
"{$callback_url}/invoices/{$request.body.id}": {
|
||||||
|
"post": {
|
||||||
|
"summary": "Invoice Notification",
|
||||||
|
"operationId": "invoice_notification__callback_url__invoices___request_body_id__post",
|
||||||
|
"requestBody": {
|
||||||
|
"required": True,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/InvoiceEvent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/InvoiceEventReceived"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"HTTPValidationError": {
|
||||||
|
"title": "HTTPValidationError",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"detail": {
|
||||||
|
"title": "Detail",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Invoice": {
|
||||||
|
"title": "Invoice",
|
||||||
|
"required": ["id", "customer", "total"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"title": "Id", "type": "string"},
|
||||||
|
"title": {"title": "Title", "type": "string"},
|
||||||
|
"customer": {"title": "Customer", "type": "string"},
|
||||||
|
"total": {"title": "Total", "type": "number"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"InvoiceEvent": {
|
||||||
|
"title": "InvoiceEvent",
|
||||||
|
"required": ["description", "paid"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"description": {"title": "Description", "type": "string"},
|
||||||
|
"paid": {"title": "Paid", "type": "boolean"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"InvoiceEventReceived": {
|
||||||
|
"title": "InvoiceEventReceived",
|
||||||
|
"required": ["ok"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"ok": {"title": "Ok", "type": "boolean"}},
|
||||||
|
},
|
||||||
|
"ValidationError": {
|
||||||
|
"title": "ValidationError",
|
||||||
|
"required": ["loc", "msg", "type"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"loc": {
|
||||||
|
"title": "Location",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
},
|
||||||
|
"msg": {"title": "Message", "type": "string"},
|
||||||
|
"type": {"title": "Error Type", "type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_openapi():
|
||||||
|
with client:
|
||||||
|
response = client.get("/openapi.json")
|
||||||
|
|
||||||
|
assert response.json() == openapi_schema
|
||||||
|
|
||||||
|
|
||||||
|
def test_get():
|
||||||
|
response = client.post(
|
||||||
|
"/invoices/", json={"id": "fooinvoice", "customer": "John", "total": 5.3}
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"msg": "Invoice received"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_dummy_callback():
|
||||||
|
# Just for coverage
|
||||||
|
invoice_notification({})
|
||||||
Loading…
Reference in New Issue