]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
:bug: Fix callback handling in sub-routers (#792)
authorJames Kaplan <jekirl@users.noreply.github.com>
Wed, 8 Jan 2020 21:22:14 +0000 (13:22 -0800)
committerSebastián Ramírez <tiangolo@gmail.com>
Wed, 8 Jan 2020 21:22:14 +0000 (22:22 +0100)
docs/src/openapi_callbacks/tutorial001.py
docs/tutorial/openapi-callbacks.md
fastapi/routing.py
tests/test_sub_callbacks.py [new file with mode: 0644]

index b2838b0a267ada044e13287b9ddf4f8cf6d041b6..717e981f82622b4e7a00c59cd7018d41fcad5331 100644 (file)
@@ -25,8 +25,7 @@ invoices_callback_router = APIRouter(default_response_class=JSONResponse)
 
 
 @invoices_callback_router.post(
-    "{$callback_url}/invoices/{$request.body.id}",
-    response_model=InvoiceEventReceived,
+    "{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived,
 )
 def invoice_notification(body: InvoiceEvent):
     pass
index e48ee7f95168d0d9d7efd8f5eea5f79ce08e1c23..311767a6b0ec1221335bd8fd26074b2ce31ffb6d 100644 (file)
@@ -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:
 
-```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!}
 ```
 
@@ -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`.
 * 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!}
 ```
 
@@ -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:
 
-```Python hl_lines="35"
+```Python hl_lines="34"
 {!./src/openapi_callbacks/tutorial001.py!}
 ```
 
index ee75374b4e0680e5033aa3c82c86ed4c37bf4102..4e08c61b7ebabb2a4d324bc0c766cf2cee874d0a 100644 (file)
@@ -554,6 +554,7 @@ class APIRouter(routing.Router):
                     response_class=route.response_class or default_response_class,
                     name=route.name,
                     route_class_override=type(route),
+                    callbacks=route.callbacks,
                 )
             elif isinstance(route, routing.Route):
                 self.add_route(
diff --git a/tests/test_sub_callbacks.py b/tests/test_sub_callbacks.py
new file mode 100644 (file)
index 0000000..5391a66
--- /dev/null
@@ -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({})