mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-26 07:40:57 -05:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7c69586ae | ||
|
|
4e09feda9e | ||
|
|
73260971b5 | ||
|
|
b36bfff56e | ||
|
|
83d04df8a6 | ||
|
|
7bc78c5fd3 | ||
|
|
ae8fa3aacd | ||
|
|
08bc120771 | ||
|
|
a39efb029f | ||
|
|
58ca98285f | ||
|
|
3f5f81bbdc | ||
|
|
90236c8135 | ||
|
|
c200bc2240 | ||
|
|
e9861cd918 | ||
|
|
202fa11d50 | ||
|
|
4b6e09296c | ||
|
|
9bd0d6fa96 | ||
|
|
35510a5ea7 | ||
|
|
c1788a25c7 | ||
|
|
19c77e35bd |
56
.github/ISSUE_TEMPLATE/bug_report.md
vendored
56
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -7,29 +7,48 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
### Describe the bug
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Create a file with '...'
|
||||
2. Add a path operation function with '....'
|
||||
3. Open the browser and call it with a payload of '....'
|
||||
4. See error
|
||||
Write here a clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
### To Reproduce
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
Steps to reproduce the behavior with a minimum self-contained file.
|
||||
|
||||
**Environment:**
|
||||
- OS: [e.g. Linux / Windows / macOS]
|
||||
- FastAPI Version [e.g. 0.3.0], get it with:
|
||||
Replace each part with your own scenario:
|
||||
|
||||
1. Create a file with:
|
||||
|
||||
```Python
|
||||
import fastapi
|
||||
print(fastapi.__version__)
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
return {"Hello": "World"}
|
||||
```
|
||||
|
||||
3. Open the browser and call the endpoint `/`.
|
||||
4. It returns a JSON with `{"Hello": "World"}`.
|
||||
5. But I expected it to return `{"Hello": "Sara"}`.
|
||||
|
||||
### Expected behavior
|
||||
|
||||
Add a clear and concise description of what you expected to happen.
|
||||
|
||||
### Screenshots
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
### Environment
|
||||
|
||||
- OS: [e.g. Linux / Windows / macOS]
|
||||
- FastAPI Version [e.g. 0.3.0], get it with:
|
||||
|
||||
```bash
|
||||
python -c "import fastapi; print(fastapi.__version__)"
|
||||
```
|
||||
|
||||
- Python version, get it with:
|
||||
@@ -38,5 +57,6 @@ print(fastapi.__version__)
|
||||
python --version
|
||||
```
|
||||
|
||||
**Additional context**
|
||||
### Additional context
|
||||
|
||||
Add any other context about the problem here.
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -7,14 +7,20 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I want to be able to [...] but I can't because [...]
|
||||
### Is your feature request related to a problem
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
Is your feature request related to a problem?
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
Add a clear and concise description of what the problem is. Ex. I want to be able to [...] but I can't because [...]
|
||||
|
||||
### The solution you would like
|
||||
|
||||
Add a clear and concise description of what you want to happen.
|
||||
|
||||
### Describe alternatives you've considered
|
||||
|
||||
Add a clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
### Additional context
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/question.md
vendored
11
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -7,11 +7,18 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Description**
|
||||
### First check
|
||||
|
||||
* [ ] I used the GitHub search to find a similar issue and didn't find it.
|
||||
* [ ] I searched the FastAPI documentation, with the integrated search.
|
||||
* [ ] I already searched in Google "How to X in FastAPI" and didn't find any information.
|
||||
|
||||
### Description
|
||||
|
||||
How can I [...]?
|
||||
|
||||
Is it possible to [...]?
|
||||
|
||||
**Additional context**
|
||||
### Additional context
|
||||
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
@@ -7,12 +7,11 @@ cache: pip
|
||||
python:
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
- "3.8-dev"
|
||||
- "3.8"
|
||||
- "nightly"
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- python: "3.8-dev"
|
||||
- python: "nightly"
|
||||
|
||||
install:
|
||||
|
||||
@@ -63,6 +63,8 @@ Here's an incomplete list of some of them.
|
||||
|
||||
* <a href="https://habr.com/ru/post/454440/" target="_blank">Мелкая питонячая радость #2: Starlette - Солидная примочка – FastAPI</a> by <a href="https://habr.com/ru/users/57uff3r/" target="_blank">Andrey Korchak</a>.
|
||||
|
||||
* <a href="https://habr.com/ru/post/478620/" target="_blank">Почему Вы должны попробовать FastAPI?</a> by <a href="https://github.com/prostomarkeloff" target="_blank">prostomarkeloff</a>.
|
||||
|
||||
## Podcasts
|
||||
|
||||
* <a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">FastAPI on PythonBytes</a> by <a href="https://pythonbytes.fm/" target="_blank">Python Bytes FM</a>.
|
||||
|
||||
BIN
docs/img/github-social-preview.png
Normal file
BIN
docs/img/github-social-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
98
docs/img/github-social-preview.svg
Normal file
98
docs/img/github-social-preview.svg
Normal file
@@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
id="svg8"
|
||||
version="1.1"
|
||||
viewBox="0 0 338.66665 169.33332"
|
||||
height="169.33333mm"
|
||||
width="338.66666mm"
|
||||
sodipodi:docname="github-social-preview.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||
inkscape:export-filename="/home/user/code/fastapi/docs/img/github-social-preview.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1025"
|
||||
id="namedview9"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.52249777"
|
||||
inkscape:cx="565.37328"
|
||||
inkscape:cy="403.61034"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg8" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<rect
|
||||
style="opacity:0.98000004;fill:#ffffff;fill-opacity:1;stroke-width:0.26458332"
|
||||
id="rect853"
|
||||
width="338.66666"
|
||||
height="169.33333"
|
||||
x="-1.0833333e-05"
|
||||
y="0.71613133"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96" />
|
||||
<g
|
||||
transform="matrix(0.73259569,0,0,0.73259569,64.842852,-4.5763945)"
|
||||
id="layer1">
|
||||
<path
|
||||
style="opacity:0.98000004;fill:#009688;fill-opacity:1;stroke-width:3.20526505"
|
||||
id="path817"
|
||||
d="m 1.4365174,55.50154 c -17.6610514,0 -31.9886064,14.327532 -31.9886064,31.988554 0,17.661036 14.327555,31.988586 31.9886064,31.988586 17.6609756,0 31.9885196,-14.32755 31.9885196,-31.988586 0,-17.661022 -14.327544,-31.988554 -31.9885196,-31.988554 z m -1.66678692,57.63069 V 93.067264 H -11.384533 L 4.6417437,61.847974 V 81.912929 H 15.379405 Z"
|
||||
inkscape:connector-curvature="0" />
|
||||
<text
|
||||
id="text979"
|
||||
y="114.91215"
|
||||
x="52.115433"
|
||||
style="font-style:normal;font-weight:normal;font-size:79.71511078px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#009688;fill-opacity:1;stroke:none;stroke-width:1.99287772"
|
||||
xml:space="preserve"><tspan
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772"
|
||||
y="114.91215"
|
||||
x="52.115433"
|
||||
id="tspan977">FastAPI</tspan></text>
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="169.60979"
|
||||
y="119.20409"
|
||||
id="text851"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan849"
|
||||
x="169.60979"
|
||||
y="119.20409"
|
||||
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:'Roboto Italic';text-align:center;text-anchor:middle;stroke-width:0.26458332">High performance, easy to learn,</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="169.60979"
|
||||
y="132.53661"
|
||||
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:'Roboto Italic';text-align:center;text-anchor:middle;stroke-width:0.26458332"
|
||||
id="tspan855">fast to code, ready for production</tspan></text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
BIN
docs/img/tutorial/openapi-callbacks/image01.png
Normal file
BIN
docs/img/tutorial/openapi-callbacks/image01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
@@ -1,5 +1,25 @@
|
||||
## Latest changes
|
||||
|
||||
## 0.45.0
|
||||
|
||||
* Add support for OpenAPI Callbacks:
|
||||
* New docs: [OpenAPI Callbacks](https://fastapi.tiangolo.com/tutorial/openapi-callbacks/).
|
||||
* Refactor generation of `operationId`s to be valid Python names (also valid variables in most languages).
|
||||
* Add `default_response_class` parameter to `APIRouter`.
|
||||
* Original PR [#722](https://github.com/tiangolo/fastapi/pull/722) by [@booooh](https://github.com/booooh).
|
||||
* Refactor logging to use the same logger everywhere, update log strings and levels. PR [#781](https://github.com/tiangolo/fastapi/pull/781).
|
||||
* Add article to [External Links](https://fastapi.tiangolo.com/external-links/): [Почему Вы должны попробовать FastAPI?](https://habr.com/ru/post/478620/). PR [#766](https://github.com/tiangolo/fastapi/pull/766) by [@prostomarkeloff](https://github.com/prostomarkeloff).
|
||||
* Remove gender bias in docs for handling errors. PR [#780](https://github.com/tiangolo/fastapi/pull/780). Original idea in PR [#761](https://github.com/tiangolo/fastapi/pull/761) by [@classywhetten](https://github.com/classywhetten).
|
||||
* Rename docs and references to `body-schema` to `body-fields` to keep in line with Pydantic. PR [#746](https://github.com/tiangolo/fastapi/pull/746) by [@prostomarkeloff](https://github.com/prostomarkeloff).
|
||||
|
||||
## 0.44.1
|
||||
|
||||
* Add GitHub social preview images to git. PR [#752](https://github.com/tiangolo/fastapi/pull/752).
|
||||
* Update PyPI "trove classifiers". PR [#751](https://github.com/tiangolo/fastapi/pull/751).
|
||||
* Add full support for Python 3.8. Enable Python 3.8 in full in Travis. PR [749](https://github.com/tiangolo/fastapi/pull/749).
|
||||
* Update "new issue" templates. PR [#749](https://github.com/tiangolo/fastapi/pull/749).
|
||||
* Fix serialization of errors for exotic Pydantic types. PR [#748](https://github.com/tiangolo/fastapi/pull/748) by [@dmontagu](https://github.com/dmontagu).
|
||||
|
||||
## 0.44.0
|
||||
|
||||
* Add GitHub action [Issue Manager](https://github.com/tiangolo/issue-manager). PR [#742](https://github.com/tiangolo/fastapi/pull/742).
|
||||
|
||||
53
docs/src/openapi_callbacks/tutorial001.py
Normal file
53
docs/src/openapi_callbacks/tutorial001.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
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
|
||||
|
||||
|
||||
@app.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"}
|
||||
@@ -5,7 +5,7 @@ The same way you can declare additional validation and metadata in path operatio
|
||||
First, you have to import it:
|
||||
|
||||
```Python hl_lines="2"
|
||||
{!./src/body_schema/tutorial001.py!}
|
||||
{!./src/body_fields/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! warning
|
||||
@@ -17,7 +17,7 @@ First, you have to import it:
|
||||
You can then use `Field` with model attributes:
|
||||
|
||||
```Python hl_lines="9 10"
|
||||
{!./src/body_schema/tutorial001.py!}
|
||||
{!./src/body_fields/tutorial001.py!}
|
||||
```
|
||||
|
||||
`Field` works the same way as `Query`, `Path` and `Body`, it has all the same parameters, etc.
|
||||
@@ -34,7 +34,7 @@ You can then use `Field` with model attributes:
|
||||
!!! tip
|
||||
Notice how each model's attribute with a type, default value and `Field` has the same structure as a path operation function's parameter, with `Field` instead of `Path`, `Query` and `Body`.
|
||||
|
||||
## Schema extras
|
||||
## JSON Schema extras
|
||||
|
||||
In `Field`, `Path`, `Query`, `Body` and others you'll see later, you can declare extra parameters apart from those described before.
|
||||
|
||||
@@ -48,12 +48,12 @@ If you know JSON Schema and want to add extra information apart from what we hav
|
||||
For example, you can use that functionality to pass a <a href="http://json-schema.org/latest/json-schema-validation.html#rfc.section.8.5" target="_blank">JSON Schema example</a> field to a body request JSON Schema:
|
||||
|
||||
```Python hl_lines="20 21 22 23 24 25"
|
||||
{!./src/body_schema/tutorial002.py!}
|
||||
{!./src/body_fields/tutorial002.py!}
|
||||
```
|
||||
|
||||
And it would look in the `/docs` like this:
|
||||
|
||||
<img src="/img/tutorial/body-schema/image01.png">
|
||||
<img src="/img/tutorial/body-fields/image01.png">
|
||||
|
||||
## Recap
|
||||
|
||||
@@ -4,9 +4,9 @@ This client could be a browser with a frontend, the code from someone else, an I
|
||||
|
||||
You could need to tell that client that:
|
||||
|
||||
* He doesn't have enough privileges for that operation.
|
||||
* He doesn't have access to that resource.
|
||||
* The item he was trying to access doesn't exist.
|
||||
* The client doesn't have enough privileges for that operation.
|
||||
* The client doesn't have access to that resource.
|
||||
* The item the client was trying to access doesn't exist.
|
||||
* etc.
|
||||
|
||||
In these cases, you would normally return an **HTTP status code** in the range of **400** (from 400 to 499).
|
||||
|
||||
186
docs/tutorial/openapi-callbacks.md
Normal file
186
docs/tutorial/openapi-callbacks.md
Normal file
@@ -0,0 +1,186 @@
|
||||
You could create an API with a *path operation* that could trigger a request to an *external API* created by someone else (probably the same developer that would be *using* your API).
|
||||
|
||||
The process that happens when your API app calls the *external API* is named a "callback". Because the software that the external developer wrote sends a request to your API and then your API *calls back*, sending a request to an *external API* (that was probably created by the same developer).
|
||||
|
||||
In this case, you could want to document how that external API *should* look like. What *path operation* it should have, what body it should expect, what response it should return, etc.
|
||||
|
||||
## An app with callbacks
|
||||
|
||||
Let's see all this with an example.
|
||||
|
||||
Imagine you develop an app that allows creating invoices.
|
||||
|
||||
These invoices will have an `id`, `title` (optional), `customer`, and `total`.
|
||||
|
||||
The user of your API (an external developer) will create an invoice in your API with a POST request.
|
||||
|
||||
Then your API will (let's imagine):
|
||||
|
||||
* Send the invoice to some customer of the external developer.
|
||||
* Collect the money.
|
||||
* Send a notification back to the API user (the external developer).
|
||||
* This will be done by sending a POST request (from *your API*) to some *external API* provided by that external developer (this is the "callback").
|
||||
|
||||
## The normal **FastAPI** app
|
||||
|
||||
Let's first see how the normal API app would look like before adding the callback.
|
||||
|
||||
It will have a *path operation* that will receive an `Invoice` body, and a query parameter `callback_url` that will contain the URL for the callback.
|
||||
|
||||
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"
|
||||
{!./src/openapi_callbacks/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
The `callback_url` query parameter uses a Pydantic <a href="https://pydantic-docs.helpmanual.io/usage/types/#urls" target="_blank">URL</a> type.
|
||||
|
||||
The only new thing is the `callbacks=messages_callback_router.routes` as an argument to the *path operation decorator*. We'll see what that is next.
|
||||
|
||||
## Documenting the callback
|
||||
|
||||
The actual callback code will depend heavily on your own API app.
|
||||
|
||||
And it will probably vary a lot from one app to the next.
|
||||
|
||||
It could be just one or two lines of code, like:
|
||||
|
||||
```Python
|
||||
callback_url = "https://example.com/api/v1/invoices/events/"
|
||||
requests.post(callback_url, json={"description": "Invoice paid", "paid": True})
|
||||
```
|
||||
|
||||
But possibly the most important part of the callback is making sure that your API user (the external developer) implements the *external API* correctly, according to the data that *your API* is going to send in the request body of the callback, etc.
|
||||
|
||||
So, what we will do next is add the code to document how that *external API* should look like to receive the callback from *your API*.
|
||||
|
||||
That documentation will show up in the Swagger UI at `/docs` in your API, and it will let external developers know how to build the *external API*.
|
||||
|
||||
This example doesn't implement the callback itself (that could be just a line of code), only the documentation part.
|
||||
|
||||
!!! tip
|
||||
The actual callback is just an HTTP request.
|
||||
|
||||
When implementing the callback yourself, you could use something like <a href="https://www.encode.io/httpx/" target="_blank">HTTPX</a> or <a href="https://requests.readthedocs.io/" target="_blank">Requests</a>.
|
||||
|
||||
## Write the callback documentation code
|
||||
|
||||
This code won't be executed in your app, we only need it to *document* how that *external API* should look like.
|
||||
|
||||
But, you already know how to easily create automatic documentation for an API with **FastAPI**.
|
||||
|
||||
So we are going to use that same knowledge to document how the *external API* should look like... by creating the *path operation(s)* that the external API should implement (the ones your API will call).
|
||||
|
||||
!!! tip
|
||||
When writing the code to document a callback, it might be useful to imagine that you are that *external developer*. And that you are currently implementing the *external API*, not *your API*.
|
||||
|
||||
Temporarily adopting this point of view (of the *external developer*) can help you feel like it's more obvious where to put the parameters, the Pydantic model for the body, for the response, etc. for that *external API*.
|
||||
|
||||
### Create a callback `APIRouter`
|
||||
|
||||
First create a new `APIRouter` that will contain one or more callbacks.
|
||||
|
||||
This router will never be added to an actual `FastAPI` app (i.e. it will never be passed to `app.include_router(...)`).
|
||||
|
||||
Because of that, you need to declare what will be the `default_response_class`, and set it to `JSONResponse`.
|
||||
|
||||
!!! Note "Technical Details"
|
||||
The `response_class` is normally set by the `FastAPI` app during the call to `app.include_router(some_router)`.
|
||||
|
||||
But as we are never calling `app.include_router(some_router)`, we need to set the `default_response_class` during creation of the `APIRouter`.
|
||||
|
||||
```Python hl_lines="3 24"
|
||||
{!./src/openapi_callbacks/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Create the callback *path operation*
|
||||
|
||||
To create the callback *path operation* use the same `APIRouter` you created above.
|
||||
|
||||
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"
|
||||
{!./src/openapi_callbacks/tutorial001.py!}
|
||||
```
|
||||
|
||||
There are 2 main differences from a normal *path operation*:
|
||||
|
||||
* It doesn't need to have any actual code, because your app will never call this code. It's only used to document the *external API*. So, the function could just have `pass`.
|
||||
* The *path* can contain an <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#key-expression" target="_blank">OpenAPI 3 expression</a> (see more below) where it can use variables with parameters and parts of the original request sent to *your API*.
|
||||
|
||||
### The callback path expression
|
||||
|
||||
The callback *path* can have an <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#key-expression" target="_blank">OpenAPI 3 expression</a> that can contain parts of the original request sent to *your API*.
|
||||
|
||||
In this case, it's the `str`:
|
||||
|
||||
```Python
|
||||
"{$callback_url}/invoices/{$request.body.id}"
|
||||
```
|
||||
|
||||
So, if your API user (the external developer) sends a request to *your API* to:
|
||||
|
||||
```
|
||||
https://yourapi.com/invoices/?callback_url=https://www.external.org/events
|
||||
```
|
||||
|
||||
with a JSON body of:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"id": "2expen51ve",
|
||||
"customer": "Mr. Richie Rich",
|
||||
"total": "9999"
|
||||
}
|
||||
```
|
||||
|
||||
Then *your API* will process the invoice, and at some point later, send a callback request to the `callback_url` (the *external API*):
|
||||
|
||||
```
|
||||
https://www.external.org/events/invoices/2expen51ve
|
||||
```
|
||||
|
||||
with a JSON body containing something like:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"description": "Payment celebration",
|
||||
"paid": true
|
||||
}
|
||||
```
|
||||
|
||||
and it would expect a response from that *external API* with a JSON body like:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Notice how the callback URL used contains the URL received as a query parameter in `callback_url` (`https://www.external.org/events`) and also the invoice `id` from inside of the JSON body (`2expen51ve`).
|
||||
|
||||
### Add the callback router
|
||||
|
||||
At this point you have the *callback path operation(s)* needed (the one(s) that the *external developer* should implement in the *external API*) in the callback router you created above.
|
||||
|
||||
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"
|
||||
{!./src/openapi_callbacks/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Notice that you are not passing the router itself (`invoices_callback_router`) to `callback=`, but the attribute `.routes`, as in `invoices_callback_router.routes`.
|
||||
|
||||
### Check the docs
|
||||
|
||||
Now you can start your app with Uvicorn and go to <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
|
||||
You will see your docs including a "Callback" section for your *path operation* that shows how the *external API* should look like:
|
||||
|
||||
<img src="/img/tutorial/openapi-callbacks/image01.png">
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.44.0"
|
||||
__version__ = "0.45.0"
|
||||
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
|
||||
@@ -303,6 +303,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[routing.APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -327,6 +328,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
def put(
|
||||
@@ -351,6 +353,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[routing.APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -375,6 +378,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
def post(
|
||||
@@ -399,6 +403,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[routing.APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -423,6 +428,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
def delete(
|
||||
@@ -447,6 +453,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[routing.APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -471,6 +478,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
def options(
|
||||
@@ -495,6 +503,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[routing.APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -519,6 +528,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
def head(
|
||||
@@ -543,6 +553,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[routing.APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -567,6 +578,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
def patch(
|
||||
@@ -591,6 +603,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[routing.APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -615,6 +628,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
def trace(
|
||||
@@ -639,6 +653,7 @@ class FastAPI(Starlette):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[routing.APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -663,4 +678,5 @@ class FastAPI(Starlette):
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,8 @@ from enum import Enum
|
||||
from types import GeneratorType
|
||||
from typing import Any, Dict, List, Set, Union
|
||||
|
||||
from fastapi.utils import PYDANTIC_1, logger
|
||||
from fastapi.logger import logger
|
||||
from fastapi.utils import PYDANTIC_1
|
||||
from pydantic import BaseModel
|
||||
from pydantic.json import ENCODERS_BY_TYPE
|
||||
|
||||
@@ -23,9 +24,9 @@ def jsonable_encoder(
|
||||
) -> Any:
|
||||
if skip_defaults is not None:
|
||||
logger.warning( # pragma: nocover
|
||||
"skip_defaults in jsonable_encoder has been deprecated in \
|
||||
favor of exclude_unset to keep in line with Pydantic v1, support for it \
|
||||
will be removed soon."
|
||||
"skip_defaults in jsonable_encoder has been deprecated in favor of "
|
||||
"exclude_unset to keep in line with Pydantic v1, support for it will be "
|
||||
"removed soon."
|
||||
)
|
||||
if include is not None and not isinstance(include, set):
|
||||
include = set(include)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
@@ -19,5 +20,6 @@ async def request_validation_exception_handler(
|
||||
request: Request, exc: RequestValidationError
|
||||
) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=HTTP_422_UNPROCESSABLE_ENTITY, content={"detail": exc.errors()}
|
||||
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content={"detail": jsonable_encoder(exc.errors())},
|
||||
)
|
||||
|
||||
3
fastapi/logger.py
Normal file
3
fastapi/logger.py
Normal file
@@ -0,0 +1,3 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("fastapi")
|
||||
@@ -1,7 +1,7 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from fastapi.utils import logger
|
||||
from fastapi.logger import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
try:
|
||||
@@ -21,9 +21,9 @@ try:
|
||||
# TODO: remove when removing support for Pydantic < 1.0.0
|
||||
from pydantic.types import EmailStr # type: ignore
|
||||
except ImportError: # pragma: no cover
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"email-validator not installed, email fields will be treated as str.\n"
|
||||
+ "To install, run: pip install email-validator"
|
||||
"To install, run: pip install email-validator"
|
||||
)
|
||||
|
||||
class EmailStr(str): # type: ignore
|
||||
|
||||
@@ -187,6 +187,14 @@ def get_openapi_path(
|
||||
)
|
||||
if request_body_oai:
|
||||
operation["requestBody"] = request_body_oai
|
||||
if route.callbacks:
|
||||
callbacks = {}
|
||||
for callback in route.callbacks:
|
||||
cb_path, cb_security_schemes, cb_definitions, = get_openapi_path(
|
||||
route=callback, model_name_map=model_name_map
|
||||
)
|
||||
callbacks[callback.name] = {callback.path: cb_path}
|
||||
operation["callbacks"] = callbacks
|
||||
if route.responses:
|
||||
for (additional_status_code, response) in route.responses.items():
|
||||
assert isinstance(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import logging
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Type, Union
|
||||
|
||||
from fastapi import params
|
||||
@@ -13,6 +12,7 @@ from fastapi.dependencies.utils import (
|
||||
)
|
||||
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
|
||||
from fastapi.logger import logger
|
||||
from fastapi.openapi.constants import STATUS_CODES_WITH_NO_BODY
|
||||
from fastapi.utils import (
|
||||
PYDANTIC_1,
|
||||
@@ -108,7 +108,7 @@ def get_request_handler(
|
||||
if body_bytes:
|
||||
body = await request.json()
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting request body: {e}")
|
||||
logger.error(f"Error getting request body: {e}")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="There was an error parsing the body"
|
||||
) from e
|
||||
@@ -218,6 +218,7 @@ class APIRoute(routing.Route):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
dependency_overrides_provider: Any = None,
|
||||
callbacks: Optional[List["APIRoute"]] = None,
|
||||
) -> None:
|
||||
self.path = path
|
||||
self.endpoint = endpoint
|
||||
@@ -338,6 +339,7 @@ class APIRoute(routing.Route):
|
||||
)
|
||||
self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id)
|
||||
self.dependency_overrides_provider = dependency_overrides_provider
|
||||
self.callbacks = callbacks
|
||||
self.app = request_response(self.get_route_handler())
|
||||
|
||||
def get_route_handler(self) -> Callable:
|
||||
@@ -363,12 +365,14 @@ class APIRouter(routing.Router):
|
||||
default: ASGIApp = None,
|
||||
dependency_overrides_provider: Any = None,
|
||||
route_class: Type[APIRoute] = APIRoute,
|
||||
default_response_class: Type[Response] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
routes=routes, redirect_slashes=redirect_slashes, default=default
|
||||
)
|
||||
self.dependency_overrides_provider = dependency_overrides_provider
|
||||
self.route_class = route_class
|
||||
self.default_response_class = default_response_class
|
||||
|
||||
def add_api_route(
|
||||
self,
|
||||
@@ -395,6 +399,7 @@ class APIRouter(routing.Router):
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
route_class_override: Optional[Type[APIRoute]] = None,
|
||||
callbacks: List[APIRoute] = None,
|
||||
) -> None:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -420,9 +425,10 @@ class APIRouter(routing.Router):
|
||||
response_model_exclude_unset or response_model_skip_defaults
|
||||
),
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
dependency_overrides_provider=self.dependency_overrides_provider,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
self.routes.append(route)
|
||||
|
||||
@@ -449,6 +455,7 @@ class APIRouter(routing.Router):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -475,8 +482,9 @@ class APIRouter(routing.Router):
|
||||
response_model_exclude_unset or response_model_skip_defaults
|
||||
),
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
return func
|
||||
|
||||
@@ -586,6 +594,7 @@ class APIRouter(routing.Router):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -609,8 +618,9 @@ class APIRouter(routing.Router):
|
||||
response_model_exclude_unset or response_model_skip_defaults
|
||||
),
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
def put(
|
||||
@@ -635,6 +645,7 @@ class APIRouter(routing.Router):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -658,8 +669,9 @@ class APIRouter(routing.Router):
|
||||
response_model_exclude_unset or response_model_skip_defaults
|
||||
),
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
def post(
|
||||
@@ -684,6 +696,7 @@ class APIRouter(routing.Router):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -707,8 +720,9 @@ class APIRouter(routing.Router):
|
||||
response_model_exclude_unset or response_model_skip_defaults
|
||||
),
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
def delete(
|
||||
@@ -733,6 +747,7 @@ class APIRouter(routing.Router):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -756,8 +771,9 @@ class APIRouter(routing.Router):
|
||||
response_model_exclude_unset or response_model_skip_defaults
|
||||
),
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
def options(
|
||||
@@ -782,6 +798,7 @@ class APIRouter(routing.Router):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -805,8 +822,9 @@ class APIRouter(routing.Router):
|
||||
response_model_exclude_unset or response_model_skip_defaults
|
||||
),
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
def head(
|
||||
@@ -831,6 +849,7 @@ class APIRouter(routing.Router):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -854,8 +873,9 @@ class APIRouter(routing.Router):
|
||||
response_model_exclude_unset or response_model_skip_defaults
|
||||
),
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
def patch(
|
||||
@@ -880,6 +900,7 @@ class APIRouter(routing.Router):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -903,8 +924,9 @@ class APIRouter(routing.Router):
|
||||
response_model_exclude_unset or response_model_skip_defaults
|
||||
),
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
def trace(
|
||||
@@ -929,6 +951,7 @@ class APIRouter(routing.Router):
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
callbacks: List[APIRoute] = None,
|
||||
) -> Callable:
|
||||
if response_model_skip_defaults is not None:
|
||||
warning_response_model_skip_defaults_deprecated() # pragma: nocover
|
||||
@@ -952,6 +975,7 @@ class APIRouter(routing.Router):
|
||||
response_model_exclude_unset or response_model_skip_defaults
|
||||
),
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import is_dataclass
|
||||
from typing import Any, Dict, List, Sequence, Set, Type, cast
|
||||
|
||||
from fastapi import routing
|
||||
from fastapi.logger import logger
|
||||
from fastapi.openapi.constants import REF_PREFIX
|
||||
from pydantic import BaseConfig, BaseModel, create_model
|
||||
from pydantic.schema import get_flat_models_from_fields, model_process_schema
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from starlette.routing import BaseRoute
|
||||
|
||||
logger = logging.getLogger("fastapi")
|
||||
|
||||
try:
|
||||
from pydantic.fields import FieldInfo, ModelField
|
||||
|
||||
@@ -22,8 +20,8 @@ except ImportError: # pragma: nocover
|
||||
from pydantic import Schema as FieldInfo # type: ignore
|
||||
|
||||
logger.warning(
|
||||
"Pydantic versions < 1.0.0 are deprecated in FastAPI and support will be \
|
||||
removed soon"
|
||||
"Pydantic versions < 1.0.0 are deprecated in FastAPI and support will be "
|
||||
"removed soon."
|
||||
)
|
||||
PYDANTIC_1 = False
|
||||
|
||||
@@ -39,15 +37,16 @@ def get_field_info(field: ModelField) -> FieldInfo:
|
||||
# TODO: remove when removing support for Pydantic < 1.0.0
|
||||
def warning_response_model_skip_defaults_deprecated() -> None:
|
||||
logger.warning( # pragma: nocover
|
||||
"response_model_skip_defaults has been deprecated in favor \
|
||||
of response_model_exclude_unset to keep in line with Pydantic v1, \
|
||||
support for it will be removed soon."
|
||||
"response_model_skip_defaults has been deprecated in favor of "
|
||||
"response_model_exclude_unset to keep in line with Pydantic v1, support for "
|
||||
"it will be removed soon."
|
||||
)
|
||||
|
||||
|
||||
def get_flat_models_from_routes(routes: Sequence[BaseRoute]) -> Set[Type[BaseModel]]:
|
||||
body_fields_from_routes: List[ModelField] = []
|
||||
responses_from_routes: List[ModelField] = []
|
||||
callback_flat_models: Set[Type[BaseModel]] = set()
|
||||
for route in routes:
|
||||
if getattr(route, "include_in_schema", None) and isinstance(
|
||||
route, routing.APIRoute
|
||||
@@ -61,7 +60,9 @@ def get_flat_models_from_routes(routes: Sequence[BaseRoute]) -> Set[Type[BaseMod
|
||||
responses_from_routes.append(route.response_field)
|
||||
if route.response_fields:
|
||||
responses_from_routes.extend(route.response_fields.values())
|
||||
flat_models = get_flat_models_from_fields(
|
||||
if route.callbacks:
|
||||
callback_flat_models |= get_flat_models_from_routes(route.callbacks)
|
||||
flat_models = callback_flat_models | get_flat_models_from_fields(
|
||||
body_fields_from_routes + responses_from_routes, known_models=set()
|
||||
)
|
||||
return flat_models
|
||||
@@ -155,6 +156,6 @@ def create_cloned_field(field: ModelField) -> ModelField:
|
||||
|
||||
def generate_operation_id_for_path(*, name: str, path: str, method: str) -> str:
|
||||
operation_id = name + path
|
||||
operation_id = operation_id.replace("{", "_").replace("}", "_").replace("/", "_")
|
||||
operation_id = re.sub("[^0-9a-zA-Z_]", "_", operation_id)
|
||||
operation_id = operation_id + "_" + method.lower()
|
||||
return operation_id
|
||||
|
||||
@@ -30,7 +30,7 @@ nav:
|
||||
- Query Parameters and String Validations: 'tutorial/query-params-str-validations.md'
|
||||
- Path Parameters and Numeric Validations: 'tutorial/path-params-numeric-validations.md'
|
||||
- Body - Multiple Parameters: 'tutorial/body-multiple-params.md'
|
||||
- Body - Schema: 'tutorial/body-schema.md'
|
||||
- Body - Fields: 'tutorial/body-fields.md'
|
||||
- Body - Nested Models: 'tutorial/body-nested-models.md'
|
||||
- Extra data types: 'tutorial/extra-data-types.md'
|
||||
- Cookie Parameters: 'tutorial/cookie-params.md'
|
||||
@@ -88,6 +88,7 @@ nav:
|
||||
- Testing Dependencies with Overrides: 'tutorial/testing-dependencies.md'
|
||||
- Debugging: 'tutorial/debugging.md'
|
||||
- Extending OpenAPI: 'tutorial/extending-openapi.md'
|
||||
- OpenAPI Callbacks: 'tutorial/openapi-callbacks.md'
|
||||
- Concurrency and async / await: 'async.md'
|
||||
- Deployment: 'deployment.md'
|
||||
- Project Generation - Template: 'project-generation.md'
|
||||
|
||||
@@ -8,15 +8,28 @@ author = "Sebastián Ramírez"
|
||||
author-email = "tiangolo@gmail.com"
|
||||
home-page = "https://github.com/tiangolo/fastapi"
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: MIT License",
|
||||
'Intended Audience :: Information Technology',
|
||||
'Intended Audience :: System Administrators',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python',
|
||||
'Topic :: Internet',
|
||||
'Topic :: Software Development :: Libraries :: Application Frameworks',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
'Topic :: Software Development :: Libraries',
|
||||
'Topic :: Software Development',
|
||||
'Typing :: Typed',
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: AsyncIO",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
]
|
||||
requires = [
|
||||
"starlette >=0.12.9,<=0.12.9",
|
||||
|
||||
@@ -244,7 +244,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Required Id",
|
||||
"operationId": "get_path_param_required_id_path_param-required__item_id__get",
|
||||
"operationId": "get_path_param_required_id_path_param_required__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -274,7 +274,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Min Length",
|
||||
"operationId": "get_path_param_min_length_path_param-minlength__item_id__get",
|
||||
"operationId": "get_path_param_min_length_path_param_minlength__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -308,7 +308,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Max Length",
|
||||
"operationId": "get_path_param_max_length_path_param-maxlength__item_id__get",
|
||||
"operationId": "get_path_param_max_length_path_param_maxlength__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -342,7 +342,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Min Max Length",
|
||||
"operationId": "get_path_param_min_max_length_path_param-min_maxlength__item_id__get",
|
||||
"operationId": "get_path_param_min_max_length_path_param_min_maxlength__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -377,7 +377,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Gt",
|
||||
"operationId": "get_path_param_gt_path_param-gt__item_id__get",
|
||||
"operationId": "get_path_param_gt_path_param_gt__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -411,7 +411,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Gt0",
|
||||
"operationId": "get_path_param_gt0_path_param-gt0__item_id__get",
|
||||
"operationId": "get_path_param_gt0_path_param_gt0__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -445,7 +445,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Ge",
|
||||
"operationId": "get_path_param_ge_path_param-ge__item_id__get",
|
||||
"operationId": "get_path_param_ge_path_param_ge__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -479,7 +479,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Lt",
|
||||
"operationId": "get_path_param_lt_path_param-lt__item_id__get",
|
||||
"operationId": "get_path_param_lt_path_param_lt__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -513,7 +513,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Lt0",
|
||||
"operationId": "get_path_param_lt0_path_param-lt0__item_id__get",
|
||||
"operationId": "get_path_param_lt0_path_param_lt0__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -547,7 +547,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Le",
|
||||
"operationId": "get_path_param_le_path_param-le__item_id__get",
|
||||
"operationId": "get_path_param_le_path_param_le__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -581,7 +581,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Lt Gt",
|
||||
"operationId": "get_path_param_lt_gt_path_param-lt-gt__item_id__get",
|
||||
"operationId": "get_path_param_lt_gt_path_param_lt_gt__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -616,7 +616,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Le Ge",
|
||||
"operationId": "get_path_param_le_ge_path_param-le-ge__item_id__get",
|
||||
"operationId": "get_path_param_le_ge_path_param_le_ge__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -651,7 +651,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Lt Int",
|
||||
"operationId": "get_path_param_lt_int_path_param-lt-int__item_id__get",
|
||||
"operationId": "get_path_param_lt_int_path_param_lt_int__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -685,7 +685,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Gt Int",
|
||||
"operationId": "get_path_param_gt_int_path_param-gt-int__item_id__get",
|
||||
"operationId": "get_path_param_gt_int_path_param_gt_int__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -719,7 +719,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Le Int",
|
||||
"operationId": "get_path_param_le_int_path_param-le-int__item_id__get",
|
||||
"operationId": "get_path_param_le_int_path_param_le_int__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -753,7 +753,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Ge Int",
|
||||
"operationId": "get_path_param_ge_int_path_param-ge-int__item_id__get",
|
||||
"operationId": "get_path_param_ge_int_path_param_ge_int__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -787,7 +787,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Lt Gt Int",
|
||||
"operationId": "get_path_param_lt_gt_int_path_param-lt-gt-int__item_id__get",
|
||||
"operationId": "get_path_param_lt_gt_int_path_param_lt_gt_int__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -822,7 +822,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Le Ge Int",
|
||||
"operationId": "get_path_param_le_ge_int_path_param-le-ge-int__item_id__get",
|
||||
"operationId": "get_path_param_le_ge_int_path_param_le_ge_int__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -1037,7 +1037,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Query Param Required",
|
||||
"operationId": "get_query_param_required_query_param-required_get",
|
||||
"operationId": "get_query_param_required_query_param_required_get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
@@ -1067,7 +1067,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Query Param Required Type",
|
||||
"operationId": "get_query_param_required_type_query_param-required_int_get",
|
||||
"operationId": "get_query_param_required_type_query_param_required_int_get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
|
||||
@@ -259,7 +259,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Get Not Decorated",
|
||||
"operationId": "get_not_decorated_items-not-decorated__item_id__get",
|
||||
"operationId": "get_not_decorated_items_not_decorated__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from decimal import Decimal
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, condecimal
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
@@ -9,7 +10,7 @@ app = FastAPI()
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
age: condecimal(gt=Decimal(0.0))
|
||||
|
||||
|
||||
@app.post("/items/")
|
||||
@@ -67,7 +68,7 @@ openapi_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"age": {"title": "Age", "type": "integer"},
|
||||
"age": {"title": "Age", "exclusiveMinimum": 0.0, "type": "number"},
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
@@ -99,6 +100,17 @@ openapi_schema = {
|
||||
},
|
||||
}
|
||||
|
||||
single_error = {
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"limit_value": 0.0},
|
||||
"loc": ["body", "item", 0, "age"],
|
||||
"msg": "ensure this value is greater than 0",
|
||||
"type": "value_error.number.not_gt",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
multiple_errors = {
|
||||
"detail": [
|
||||
{
|
||||
@@ -108,8 +120,8 @@ multiple_errors = {
|
||||
},
|
||||
{
|
||||
"loc": ["body", "item", 0, "age"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
"msg": "value is not a valid decimal",
|
||||
"type": "type_error.decimal",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "item", 1, "name"],
|
||||
@@ -118,8 +130,8 @@ multiple_errors = {
|
||||
},
|
||||
{
|
||||
"loc": ["body", "item", 1, "age"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
"msg": "value is not a valid decimal",
|
||||
"type": "type_error.decimal",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -137,7 +149,13 @@ def test_put_correct_body():
|
||||
assert response.json() == {"item": [{"name": "Foo", "age": 5}]}
|
||||
|
||||
|
||||
def test_put_incorrect_body():
|
||||
def test_jsonable_encoder_requiring_error():
|
||||
response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == single_error
|
||||
|
||||
|
||||
def test_put_incorrect_body_multiple():
|
||||
response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == multiple_errors
|
||||
|
||||
@@ -80,7 +80,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Create Item",
|
||||
"operationId": "create_item_starlette-items__item_id__get",
|
||||
"operationId": "create_item_starlette_items__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from body_schema.tutorial001 import app
|
||||
from body_fields.tutorial001 import app
|
||||
|
||||
# TODO: remove when removing support for Pydantic < 1.0.0
|
||||
try:
|
||||
@@ -27,7 +27,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Create Index Weights",
|
||||
"operationId": "create_index_weights_index-weights__post",
|
||||
"operationId": "create_index_weights_index_weights__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
|
||||
@@ -16,7 +16,7 @@ openapi_schema = {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "Response Read Keyword Weights Keyword-Weights Get",
|
||||
"title": "Response Read Keyword Weights Keyword Weights Get",
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "number"},
|
||||
}
|
||||
@@ -25,7 +25,7 @@ openapi_schema = {
|
||||
}
|
||||
},
|
||||
"summary": "Read Keyword Weights",
|
||||
"operationId": "read_keyword_weights_keyword-weights__get",
|
||||
"operationId": "read_keyword_weights_keyword_weights__get",
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
"summary": "Read Item Header",
|
||||
"operationId": "read_item_header_items-header__item_id__get",
|
||||
"operationId": "read_item_header_items_header__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
|
||||
174
tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py
Normal file
174
tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from openapi_callbacks.tutorial001 import app, invoice_notification
|
||||
|
||||
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({})
|
||||
Reference in New Issue
Block a user