mirror of https://github.com/tiangolo/fastapi.git
✨ Add support for shared/top-level parameters (dependencies, tags, etc) (#2434)
* ✨ Add Default and DefaultPlaceholder data structures to handle defaults and overrides * ✨ Add utils to get values by priority handling DefaultPlaceholders * ✨ Add support for top-level parameters in FastAPI, APIRouter, include_router including: prefix, tags, dependencies, deprecated, include_in_schema, responses, default_response_class, callbacks * ♻️ Update openapi utils to handle DefaultPlaceholder for response_class * 📝 Update bigger-application example code to use top-level params and showcase them in APIRouter, FastAPI, include_router * 📝 Update docs for Bigger Applications, include diagrams, top-level params * 🔥 Simplify code and docs for callbacks as default_response_class is no longer required * 📝 Add docs for top-level dependencies, in FastAPI() * 📝 Add docs reference to top-level dependencies in docs for decorator * ✅ Update/increase tests for Bigger Applications including shared parameters * ✅ Add tests for top-level dependencies in FastAPI() * ✅ Add tests for internal DefaultPlaceholder * ✅ Update/increase tests for callbacks with top-level parameters * ✅ Add LOTS of tests covering branches and cases for shared parameters in top-level FastAPI, path operations, include_router, APIRouter, its path operations, nested include_router, nested APIRouter, and its path operations * 🎨 Format/reorder parameters for consistency in FastAPI, APIRouter, include_router
This commit is contained in:
parent
d550738fa2
commit
313bbe802f
|
|
@ -83,15 +83,6 @@ So we are going to use that same knowledge to document how the *external API* sh
|
|||
|
||||
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="5 26"
|
||||
{!../../../docs_src/openapi_callbacks/tutorial001.py!}
|
||||
```
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 73 KiB |
|
|
@ -0,0 +1,43 @@
|
|||
<mxfile host="65bd71144e" modified="2020-11-28T18:13:19.199Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.51.1 Chrome/83.0.4103.122 Electron/9.3.3 Safari/537.36" etag="KPHuXUeExV3PdWouu_3U" version="13.6.5">
|
||||
<diagram id="zB4-QXJZ7ScUzHSLnJ1i" name="Page-1">
|
||||
<mxGraphModel dx="1154" dy="780" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1920" pageHeight="1200" math="0" shadow="0" extFonts="Roboto^https://fonts.googleapis.com/css?family=Roboto|Roboto Mono, mono^https://fonts.googleapis.com/css?family=Roboto+Mono%2C+mono">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="2" value="" style="rounded=0;whiteSpace=wrap;html=1;fontStyle=1;strokeWidth=4;" parent="1" vertex="1">
|
||||
<mxGeometry x="110" y="280" width="1350" height="620" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="<font style="font-size: 24px" face="Roboto">Package app<br>app/__init__.py</font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;strokeWidth=3;fontFamily=Roboto Mono, mono;FType=g;" parent="1" vertex="1">
|
||||
<mxGeometry x="635" y="310" width="300" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="<span style="font-family: &#34;roboto&#34; ; font-size: 24px">Module app.main</span><br style="font-family: &#34;roboto&#34; ; font-size: 24px"><span style="font-family: &#34;roboto&#34; ; font-size: 24px">app/main.py</span>" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;strokeWidth=3;" parent="1" vertex="1">
|
||||
<mxGeometry x="140" y="430" width="360" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="16" value="<span style="font-family: &#34;roboto&#34; ; font-size: 24px">Module app.dependencies</span><br style="font-family: &#34;roboto&#34; ; font-size: 24px"><span style="font-family: &#34;roboto&#34; ; font-size: 24px">app/dependencies.py</span>" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;strokeWidth=3;" parent="1" vertex="1">
|
||||
<mxGeometry x="130" y="565" width="370" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="" style="rounded=0;whiteSpace=wrap;html=1;fontStyle=1;strokeWidth=4;" parent="1" vertex="1">
|
||||
<mxGeometry x="1030" y="430" width="400" height="260" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="<font style="font-size: 24px" face="Roboto">Subpackage app.internal<br></font><span style="font-family: &#34;roboto&#34; ; font-size: 24px">app/internal/__init__.py</span><font style="font-size: 24px" face="Roboto"><br></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;strokeWidth=3;fontFamily=Roboto Mono, mono;FType=g;" parent="1" vertex="1">
|
||||
<mxGeometry x="1083.8438461538462" y="460" width="292.3076923076923" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="19" value="<span style="font-family: &#34;roboto&#34; ; font-size: 24px">Module app.internal.admin</span><br style="font-family: &#34;roboto&#34; ; font-size: 24px"><span style="font-family: &#34;roboto&#34; ; font-size: 24px">app/internal/admin.py</span>" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;strokeWidth=3;" parent="1" vertex="1">
|
||||
<mxGeometry x="1050" y="570" width="360" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="" style="rounded=0;whiteSpace=wrap;html=1;fontStyle=1;strokeWidth=4;" parent="1" vertex="1">
|
||||
<mxGeometry x="540" y="430" width="440" height="410" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="<font style="font-size: 24px" face="Roboto">Subpackage app.routers<br>app/routers/__init__.py<br></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;strokeWidth=3;fontFamily=Roboto Mono, mono;FType=g;" parent="1" vertex="1">
|
||||
<mxGeometry x="599.2307692307693" y="460" width="321.53846153846155" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="17" value="<span style="font-family: &#34;roboto&#34; ; font-size: 24px">Module app.routers.items</span><br style="font-family: &#34;roboto&#34; ; font-size: 24px"><span style="font-family: &#34;roboto&#34; ; font-size: 24px">app/routers/items.py</span>" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;strokeWidth=3;" parent="1" vertex="1">
|
||||
<mxGeometry x="580" y="570" width="360" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="18" value="<span style="font-family: &#34;roboto&#34; ; font-size: 24px">Module app.routers.users</span><br style="font-family: &#34;roboto&#34; ; font-size: 24px"><span style="font-family: &#34;roboto&#34; ; font-size: 24px">app/routers/users.py</span>" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;strokeWidth=3;" parent="1" vertex="1">
|
||||
<mxGeometry x="580" y="700" width="360" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -16,14 +16,18 @@ Let's say you have a file structure like this:
|
|||
├── app
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py
|
||||
│ ├── dependencies.py
|
||||
│ └── routers
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── items.py
|
||||
│ │ └── users.py
|
||||
│ └── internal
|
||||
│ ├── __init__.py
|
||||
│ ├── items.py
|
||||
│ └── users.py
|
||||
│ └── admin.py
|
||||
```
|
||||
|
||||
!!! tip
|
||||
There are two `__init__.py` files: one in each directory or subdirectory.
|
||||
There are several `__init__.py` files: one in each directory or subdirectory.
|
||||
|
||||
This is what allows importing code from one file into another.
|
||||
|
||||
|
|
@ -33,18 +37,33 @@ Let's say you have a file structure like this:
|
|||
from app.routers import items
|
||||
```
|
||||
|
||||
* The `app` directory contains everything.
|
||||
* This `app` directory has an empty file `app/__init__.py`.
|
||||
* So, the `app` directory is a "Python package" (a collection of "Python modules").
|
||||
* The `app` directory also has a `app/main.py` file.
|
||||
* As it is inside a Python package directory (because there's a file `__init__.py`), it is a "module" of that package: `app.main`.
|
||||
* There's a subdirectory `app/routers/`.
|
||||
* The subdirectory `app/routers` also has an empty file `__init__.py`.
|
||||
* So, it is a "Python subpackage".
|
||||
* The file `app/routers/items.py` is beside the `app/routers/__init__.py`.
|
||||
* So, it's a submodule: `app.routers.items`.
|
||||
* The file `app/routers/users.py` is beside the `app/routers/__init__.py`.
|
||||
* So, it's a submodule: `app.routers.users`.
|
||||
* The `app` directory contains everything. And it has an empty file `app/__init__.py`, so it is a "Python package" (a collection of "Python modules"): `app`.
|
||||
* It contains an `app/main.py` file. As it is inside a Python package (a directory with a file `__init__.py`), it is a "module" of that package: `app.main`.
|
||||
* There's also an `app/dependencies.py` file, just like `app/main.py`, it is a "module": `app.dependencies`.
|
||||
* There's a subdirectory `app/routers/` with another file `__init__.py`, so it's a "Python subpackage": `app.routers`.
|
||||
* The file `app/routers/items.py` is inside a package, `app/routers/`, so, it's a submodule: `app.routers.items`.
|
||||
* The same with `app/routers/users.py`, it's another submodule: `app.routers.users`.
|
||||
* There's also a subdirectory `app/internal/` with another file `__init__.py`, so it's another "Python subpackage": `app.internal`.
|
||||
* And the file `app/internal/admin.py` is another submodule: `app.internal.admin`.
|
||||
|
||||
<img src="/img/tutorial/bigger-applications/package.svg">
|
||||
|
||||
The same file structure with comments:
|
||||
|
||||
```
|
||||
.
|
||||
├── app # "app" is a Python package
|
||||
│ ├── __init__.py # this file makes "app" a "Python package"
|
||||
│ ├── main.py # "main" module, e.g. import app.main
|
||||
│ ├── dependencies.py # "dependencies" module, e.g. import app.dependencies
|
||||
│ └── routers # "routers" is a "Python subpackage"
|
||||
│ │ ├── __init__.py # makes "routers" a "Python subpackage"
|
||||
│ │ ├── items.py # "items" submodule, e.g. import app.routers.items
|
||||
│ │ └── users.py # "users" submodule, e.g. import app.routers.users
|
||||
│ └── internal # "internal" is a "Python subpackage"
|
||||
│ ├── __init__.py # makes "internal" a "Python subpackage"
|
||||
│ └── admin.py # "admin" submodule, e.g. import app.internal.admin
|
||||
```
|
||||
|
||||
## `APIRouter`
|
||||
|
||||
|
|
@ -78,16 +97,33 @@ You can think of `APIRouter` as a "mini `FastAPI`" class.
|
|||
|
||||
All the same options are supported.
|
||||
|
||||
All the same parameters, responses, dependencies, tags, etc.
|
||||
All the same `parameters`, `responses`, `dependencies`, `tags`, etc.
|
||||
|
||||
!!! tip
|
||||
In this example, the variable is called `router`, but you can name it however you want.
|
||||
|
||||
We are going to include this `APIRouter` in the main `FastAPI` app, but first, let's add another `APIRouter`.
|
||||
We are going to include this `APIRouter` in the main `FastAPI` app, but first, let's check the dependencies and another `APIRouter`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
We see that we are going to need some dependencies used in several places of the application.
|
||||
|
||||
So we put them in their own `dependencies` module (`app/dependencies.py`).
|
||||
|
||||
We will now use a simple dependency to read a custom `X-Token` header:
|
||||
|
||||
```Python hl_lines="1 4-6"
|
||||
{!../../../docs_src/bigger_applications/app/dependencies.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
We are using an invented header to simplify this example.
|
||||
|
||||
But in real cases you will get better results using the integrated [Security utilities](./security/index.md){.internal-link target=_blank}.
|
||||
|
||||
## Another module with `APIRouter`
|
||||
|
||||
Let's say you also have the endpoints dedicated to handling "Items" from your application in the module at `app/routers/items.py`.
|
||||
Let's say you also have the endpoints dedicated to handling "items" from your application in the module at `app/routers/items.py`.
|
||||
|
||||
You have *path operations* for:
|
||||
|
||||
|
|
@ -96,24 +132,148 @@ You have *path operations* for:
|
|||
|
||||
It's all the same structure as with `app/routers/users.py`.
|
||||
|
||||
But let's say that this time we are more lazy.
|
||||
But we want to be smarter and simplify the code a bit.
|
||||
|
||||
And we don't want to have to explicitly type `/items/` and `tags=["items"]` in every *path operation* (we will be able to do it later):
|
||||
We know all the *path operations* in this module have the same:
|
||||
|
||||
```Python hl_lines="6 11"
|
||||
* Path `prefix`: `/items`.
|
||||
* `tags`: (just one tag: `items`).
|
||||
* Extra `responses`.
|
||||
* `dependencies`: they all need that `X-Token` dependency we created.
|
||||
|
||||
So, instead of adding all that to each *path operation*, we can add it to the `APIRouter`.
|
||||
|
||||
```Python hl_lines="5-10 16 21"
|
||||
{!../../../docs_src/bigger_applications/app/routers/items.py!}
|
||||
```
|
||||
|
||||
As the path of each *path operation* has to start with `/`, like in:
|
||||
|
||||
```Python hl_lines="1"
|
||||
@router.get("/{item_id}")
|
||||
async def read_item(item_id: str):
|
||||
...
|
||||
```
|
||||
|
||||
...the prefix must not include a final `/`.
|
||||
|
||||
So, the prefix in this case is `/items`.
|
||||
|
||||
We can also add a list of `tags` and extra `responses` that will be applied to all the *path operations* included in this router.
|
||||
|
||||
And we can add a list of `dependencies` that will be added to all the *path operations* in the router and will be executed/solved for each request made to them.
|
||||
|
||||
!!! tip
|
||||
Note that, much like [dependencies in *path operation decorators*](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, no value will be passed to your *path operation function*.
|
||||
|
||||
The end result is that the item paths are now:
|
||||
|
||||
* `/items/`
|
||||
* `/items/{item_id}`
|
||||
|
||||
...as we intended.
|
||||
|
||||
* They will be marked with a list of tags that contain a single string `"items"`.
|
||||
* These "tags" are especially useful for the automatic interactive documentation systems (using OpenAPI).
|
||||
* All of them will include the predefined `responses`.
|
||||
* All these *path operations* will have the list of `dependencies` evaluated/executed before them.
|
||||
* If you also declare dependencies in a specific *path operation*, **they will be executed too**.
|
||||
* The router dependencies are executed first, then the [`dependencies` in the decorator](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, and then the normal parameter dependencies.
|
||||
* You can also add [`Security` dependencies with `scopes`](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}.
|
||||
|
||||
!!! tip
|
||||
Having `dependencies` in the `APIRouter` can be used, for example, to require authentication for a whole group of *path operations*. Even if the dependencies are not added individually to each one of them.
|
||||
|
||||
!!! check
|
||||
The `prefix`, `tags`, `responses`, and `dependencies` parameters are (as in many other cases) just a feature from **FastAPI** to help you avoid code duplication.
|
||||
|
||||
### Import the dependencies
|
||||
|
||||
This codes lives in the module `app.routers.items`, the file `app/routers/items.py`.
|
||||
|
||||
And we need to get the dependency function from the module `app.dependencies`, the file `app/dependencies.py`.
|
||||
|
||||
So we use a relative import with `..` for the dependencies:
|
||||
|
||||
```Python hl_lines="3"
|
||||
{!../../../docs_src/bigger_applications/app/routers/items.py!}
|
||||
```
|
||||
|
||||
#### How relative imports work
|
||||
|
||||
!!! tip
|
||||
If you know perfectly how imports work, continue to the next section below.
|
||||
|
||||
A single dot `.`, like in:
|
||||
|
||||
```Python
|
||||
from .dependencies import get_token_header
|
||||
```
|
||||
|
||||
would mean:
|
||||
|
||||
* Starting in the same package that this module (the file `app/routers/items.py`) lives in (the directory `app/routers/`)...
|
||||
* find the module `dependencies` (an imaginary file at `app/routers/dependencies.py`)...
|
||||
* and from it, import the function `get_token_header`.
|
||||
|
||||
But that file doesn't exist, our dependencies are in a file at `app/dependencies.py`.
|
||||
|
||||
Remember how our app/file structure looks like:
|
||||
|
||||
<img src="/img/tutorial/bigger-applications/package.svg">
|
||||
|
||||
---
|
||||
|
||||
The two dots `..`, like in:
|
||||
|
||||
```Python
|
||||
from ..dependencies import get_token_header
|
||||
```
|
||||
|
||||
mean:
|
||||
|
||||
* Starting in the same package that this module (the file `app/routers/items.py`) lives in (the directory `app/routers/`)...
|
||||
* go to the parent package (the directory `app/`)...
|
||||
* and in there, find the module `dependencies` (the file at `app/routers/dependencies.py`)...
|
||||
* and from it, import the function `get_token_header`.
|
||||
|
||||
That works correctly! 🎉
|
||||
|
||||
---
|
||||
|
||||
The same way, if we had used three dots `...`, like in:
|
||||
|
||||
```Python
|
||||
from ...dependencies import get_token_header
|
||||
```
|
||||
|
||||
that would mean:
|
||||
|
||||
* Starting in the same package that this module (the file `app/routers/items.py`) lives in (the directory `app/routers/`)...
|
||||
* go to the parent package (the directory `app/`)...
|
||||
* then go to the parent of that package (there's no parent package, `app` is the top level 😱)...
|
||||
* and in there, find the module `dependencies` (the file at `app/routers/dependencies.py`)...
|
||||
* and from it, import the function `get_token_header`.
|
||||
|
||||
That would refer to some package above `app/`, with its own file `__init__.py`, etc. But we don't have that. So, that would throw an error in our example. 🚨
|
||||
|
||||
But now you know how it works, so you can use relative imports in your own apps no matter how complex they are. 🤓
|
||||
|
||||
### Add some custom `tags`, `responses`, and `dependencies`
|
||||
|
||||
We are not adding the prefix `/items/` nor the `tags=["items"]` to add them later.
|
||||
We are not adding the prefix `/items` nor the `tags=["items"]` to each *path operation* because we added them to the `APIRouter`.
|
||||
|
||||
But we can add custom `tags` and `responses` that will be applied to a specific *path operation*:
|
||||
But we can still add _more_ `tags` that will be applied to a specific *path operation*, and also some extra `responses` specific to that *path operation*:
|
||||
|
||||
```Python hl_lines="18-19"
|
||||
```Python hl_lines="30-31"
|
||||
{!../../../docs_src/bigger_applications/app/routers/items.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
This last path operation will have the combination of tags: `["items", "custom"]`.
|
||||
|
||||
And it will also have both responses in the documentation, one for `404` and one for `403`.
|
||||
|
||||
## The main `FastAPI`
|
||||
|
||||
Now, let's see the module at `app/main.py`.
|
||||
|
|
@ -122,25 +282,27 @@ Here's where you import and use the class `FastAPI`.
|
|||
|
||||
This will be the main file in your application that ties everything together.
|
||||
|
||||
And as most of your logic will now live in its own specific module, the main file will be quite simple.
|
||||
|
||||
### Import `FastAPI`
|
||||
|
||||
You import and create a `FastAPI` class as normally:
|
||||
You import and create a `FastAPI` class as normally.
|
||||
|
||||
```Python hl_lines="1 5"
|
||||
And we can even declare [global dependencies](dependencies/global-dependencies.md){.internal-link target=_blank} that will be combined with the dependencies for each `APIRouter`:
|
||||
|
||||
```Python hl_lines="1 3 7"
|
||||
{!../../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
|
||||
### Import the `APIRouter`
|
||||
|
||||
But this time we are not adding *path operations* directly with the `FastAPI` `app`.
|
||||
Now we import the other submodules that have `APIRouter`s:
|
||||
|
||||
We import the other submodules that have `APIRouter`s:
|
||||
|
||||
```Python hl_lines="3"
|
||||
```Python hl_lines="5"
|
||||
{!../../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
|
||||
As the file `app/routers/items.py` is part of the same Python package, we can import it using "dot notation".
|
||||
As the files `app/routers/users.py` and `app/routers/items.py` are submodules that are part of the same Python package `app`, we can use a single dot `.` to import them using "relative imports".
|
||||
|
||||
### How the importing works
|
||||
|
||||
|
|
@ -156,7 +318,9 @@ Means:
|
|||
* look for the subpackage `routers` (the directory at `app/routers/`)...
|
||||
* and from it, import the submodule `items` (the file at `app/routers/items.py`) and `users` (the file at `app/routers/users.py`)...
|
||||
|
||||
The module `items` will have a variable `router` (`items.router`). This is the same one we created in the file `app/routers/items.py`. It's an `APIRouter`. The same for the module `users`.
|
||||
The module `items` will have a variable `router` (`items.router`). This is the same one we created in the file `app/routers/items.py`, it's an `APIRouter` object.
|
||||
|
||||
And then we do the same for the module `users`.
|
||||
|
||||
We could also import them like:
|
||||
|
||||
|
|
@ -165,9 +329,17 @@ from app.routers import items, users
|
|||
```
|
||||
|
||||
!!! info
|
||||
The first version is a "relative import".
|
||||
The first version is a "relative import":
|
||||
|
||||
The second version is an "absolute import".
|
||||
```Python
|
||||
from .routers import items, users
|
||||
```
|
||||
|
||||
The second version is an "absolute import":
|
||||
|
||||
```Python
|
||||
from app.routers import items, users
|
||||
```
|
||||
|
||||
To learn more about Python Packages and Modules, read <a href="https://docs.python.org/3/tutorial/modules.html" class="external-link" target="_blank">the official Python documentation about Modules</a>.
|
||||
|
||||
|
|
@ -188,22 +360,24 @@ The `router` from `users` would overwrite the one from `items` and we wouldn't b
|
|||
|
||||
So, to be able to use both of them in the same file, we import the submodules directly:
|
||||
|
||||
```Python hl_lines="3"
|
||||
```Python hl_lines="4"
|
||||
{!../../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
|
||||
### Include an `APIRouter`
|
||||
### Include the `APIRouter`s for `users` and `items`
|
||||
|
||||
Now, let's include the `router` from the submodule `users`:
|
||||
Now, let's include the `router`s from the submodules `users` and `items`:
|
||||
|
||||
```Python hl_lines="13"
|
||||
```Python hl_lines="10-11"
|
||||
{!../../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
`users.router` contains the `APIRouter` inside of the file `app/routers/users.py`.
|
||||
|
||||
With `app.include_router()` we can add an `APIRouter` to the main `FastAPI` application.
|
||||
And `items.router` contains the `APIRouter` inside of the file `app/routers/items.py`.
|
||||
|
||||
With `app.include_router()` we can add each `APIRouter` to the main `FastAPI` application.
|
||||
|
||||
It will include all the routes from that router as part of it.
|
||||
|
||||
|
|
@ -217,67 +391,52 @@ It will include all the routes from that router as part of it.
|
|||
|
||||
This will take microseconds and will only happen at startup.
|
||||
|
||||
So it won't affect performance.
|
||||
So it won't affect performance. ⚡
|
||||
|
||||
### Include an `APIRouter` with a `prefix`, `tags`, `responses`, and `dependencies`
|
||||
### Include an `APIRouter` with a custom `prefix`, `tags`, `responses`, and `dependencies`
|
||||
|
||||
Now, let's include the router from the `items` submodule.
|
||||
Now, let's imagine your organization gave you the `app/internal/admin.py` file.
|
||||
|
||||
But, remember that we were lazy and didn't add `/items/` nor `tags` to all the *path operations*?
|
||||
It contains an `APIRouter` with some admin *path operations* that your organization shares between several projects.
|
||||
|
||||
We can add a prefix to all the *path operations* using the parameter `prefix` of `app.include_router()`.
|
||||
For this example it will be super simple. But let's say that because it is shared with other projects in the organization, we cannot modify it and add a `prefix`, `dependencies`, `tags`, etc. directly to the `APIRouter`:
|
||||
|
||||
As the path of each *path operation* has to start with `/`, like in:
|
||||
|
||||
```Python hl_lines="1"
|
||||
@router.get("/{item_id}")
|
||||
async def read_item(item_id: str):
|
||||
...
|
||||
```Python hl_lines="3"
|
||||
{!../../../docs_src/bigger_applications/app/internal/admin.py!}
|
||||
```
|
||||
|
||||
...the prefix must not include a final `/`.
|
||||
But we still want to set a custom `prefix` when including the `APIRouter` so that all its *path operations* start with `/admin`, we want to secure it with the `dependencies` we already have for this project, and we want to include `tags` and `responses`.
|
||||
|
||||
So, the prefix in this case would be `/items`.
|
||||
We can declare all that without having to modify the original `APIRouter` by passing those parameters to `app.include_router()`:
|
||||
|
||||
We can also add a list of `tags` that will be applied to all the *path operations* included in this router.
|
||||
|
||||
And we can add predefined `responses` that will be included in all the *path operations* too.
|
||||
|
||||
And we can add a list of `dependencies` that will be added to all the *path operations* in the router and will be executed/solved for each request made to them. Note that, much like dependencies in *path operation decorators*, no value will be passed to your *path operation function*.
|
||||
|
||||
```Python hl_lines="8-10 14-20"
|
||||
```Python hl_lines="14-17"
|
||||
{!../../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
|
||||
The end result is that the item paths are now:
|
||||
That way, the original `APIRouter` will keep unmodified, so we can still share that same `app/internal/admin.py` file with other projects in the organization.
|
||||
|
||||
* `/items/`
|
||||
* `/items/{item_id}`
|
||||
The result is that in our app, each of the *path operations* from the `admin` module will have:
|
||||
|
||||
...as we intended.
|
||||
* The prefix `/admin`.
|
||||
* The tag `admin`.
|
||||
* The dependency `get_token_header`.
|
||||
* The response `418`. 🍵
|
||||
|
||||
* They will be marked with a list of tags that contain a single string `"items"`.
|
||||
* The *path operation* that declared a `"custom"` tag will have both tags, `items` and `custom`.
|
||||
* These "tags" are especially useful for the automatic interactive documentation systems (using OpenAPI).
|
||||
* All of them will include the predefined `responses`.
|
||||
* The *path operation* that declared a custom `403` response will have both the predefined responses (`404`) and the `403` declared in it directly.
|
||||
* All these *path operations* will have the list of `dependencies` evaluated/executed before them.
|
||||
* If you also declare dependencies in a specific *path operation*, **they will be executed too**.
|
||||
* The router dependencies are executed first, then the [`dependencies` in the decorator](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, and then the normal parameter dependencies.
|
||||
* You can also add [`Security` dependencies with `scopes`](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}.
|
||||
But that will only affect that `APIRouter` in our app, not in any other code that uses it.
|
||||
|
||||
!!! tip
|
||||
Having `dependencies` in a decorator can be used, for example, to require authentication for a whole group of *path operations*. Even if the dependencies are not added individually to each one of them.
|
||||
So, for example, other projects could use the same `APIRouter` with a different authentication method.
|
||||
|
||||
!!! check
|
||||
The `prefix`, `tags`, `responses` and `dependencies` parameters are (as in many other cases) just a feature from **FastAPI** to help you avoid code duplication.
|
||||
### Include a *path operation*
|
||||
|
||||
!!! tip
|
||||
You could also add *path operations* directly, for example with: `@app.get(...)`.
|
||||
We can also add *path operations* directly to the `FastAPI` app.
|
||||
|
||||
Apart from `app.include_router()`, in the same **FastAPI** app.
|
||||
Here we do it... just to show that we can 🤷:
|
||||
|
||||
It would still work the same.
|
||||
```Python hl_lines="21-23"
|
||||
{!../../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
|
||||
and it will work correctly, together with all the other *path operations* added with `app.include_router()`.
|
||||
|
||||
!!! info "Very Technical Details"
|
||||
**Note**: this is a very technical detail that you probably can **just skip**.
|
||||
|
|
@ -317,3 +476,13 @@ You can also use `.include_router()` multiple times with the *same* router using
|
|||
This could be useful, for example, to expose the same API under different prefixes, e.g. `/api/v1` and `/api/latest`.
|
||||
|
||||
This is an advanced usage that you might not really need, but it's there in case you do.
|
||||
|
||||
## Include an `APIRouter` in another
|
||||
|
||||
The same way you can include an `APIRouter` in a `FastAPI` application, you can include an `APIRouter` in another `APIRouter` using:
|
||||
|
||||
```Python
|
||||
router.include_router(other_router)
|
||||
```
|
||||
|
||||
Make sure you do it before including `router` in the `FastAPI` app, so that the *path operations* from `other_router` are also included.
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ These dependencies will be executed/solved the same way normal dependencies. But
|
|||
|
||||
It might also help avoid confusion for new developers that see an unused parameter in your code and could think it's unnecessary.
|
||||
|
||||
!!! info
|
||||
In this example we use invented custom headers `X-Key` and `X-Token`.
|
||||
|
||||
But in real cases, when implementing security, you would get more benefits from using the integrated [Security utilities (the next chapter)](../security/index.md){.internal-link target=_blank}.
|
||||
|
||||
## Dependencies errors and return values
|
||||
|
||||
You can use the same dependency *functions* you use normally.
|
||||
|
|
@ -60,3 +65,7 @@ So, you can re-use a normal dependency (that returns a value) you already use so
|
|||
## Dependencies for a group of *path operations*
|
||||
|
||||
Later, when reading about how to structure bigger applications ([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank}), possibly with multiple files, you will learn how to declare a single `dependencies` parameter for a group of *path operations*.
|
||||
|
||||
## Global Dependencies
|
||||
|
||||
Next we will see how to add dependencies to the whole `FastAPI` application, so that they apply to each *path operation*.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
# Global Dependencies
|
||||
|
||||
For some types of applications you might want to add dependencies to the whole application.
|
||||
|
||||
Similar to the way you can [add `dependencies` to the *path operation decorators*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, you can add them to the `FastAPI` application.
|
||||
|
||||
In that case, they will be applied to all the *path operations* in the application:
|
||||
|
||||
```Python hl_lines="15"
|
||||
{!../../../docs_src/dependencies/tutorial012.py!}
|
||||
```
|
||||
|
||||
And all the ideas in the section about [adding `dependencies` to the *path operation decorators*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} still apply, but in this case, to all of the *path operations* in the app.
|
||||
|
||||
## Dependencies for groups of *path operations*
|
||||
|
||||
Later, when reading about how to structure bigger applications ([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank}), possibly with multiple files, you will learn how to declare a single `dependencies` parameter for a group of *path operations*.
|
||||
|
|
@ -81,6 +81,7 @@ nav:
|
|||
- tutorial/dependencies/classes-as-dependencies.md
|
||||
- tutorial/dependencies/sub-dependencies.md
|
||||
- tutorial/dependencies/dependencies-in-path-operation-decorators.md
|
||||
- tutorial/dependencies/global-dependencies.md
|
||||
- tutorial/dependencies/dependencies-with-yield.md
|
||||
- Security:
|
||||
- tutorial/security/index.md
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
from fastapi import Header, HTTPException
|
||||
|
||||
|
||||
async def get_token_header(x_token: str = Header(...)):
|
||||
if x_token != "fake-super-secret-token":
|
||||
raise HTTPException(status_code=400, detail="X-Token header invalid")
|
||||
|
||||
|
||||
async def get_query_token(token: str):
|
||||
if token != "jessica":
|
||||
raise HTTPException(status_code=400, detail="No Jessica token provided")
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def update_admin():
|
||||
return {"message": "Admin getting schwifty"}
|
||||
|
|
@ -1,20 +1,23 @@
|
|||
from fastapi import Depends, FastAPI, Header, HTTPException
|
||||
from fastapi import Depends, FastAPI
|
||||
|
||||
from .dependencies import get_query_token, get_token_header
|
||||
from .internal import admin
|
||||
from .routers import items, users
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
async def get_token_header(x_token: str = Header(...)):
|
||||
if x_token != "fake-super-secret-token":
|
||||
raise HTTPException(status_code=400, detail="X-Token header invalid")
|
||||
app = FastAPI(dependencies=[Depends(get_query_token)])
|
||||
|
||||
|
||||
app.include_router(users.router)
|
||||
app.include_router(items.router)
|
||||
app.include_router(
|
||||
items.router,
|
||||
prefix="/items",
|
||||
tags=["items"],
|
||||
admin.router,
|
||||
prefix="/admin",
|
||||
tags=["admin"],
|
||||
dependencies=[Depends(get_token_header)],
|
||||
responses={404: {"description": "Not found"}},
|
||||
responses={418: {"description": "I'm a teapot"}},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Hello Bigger Applications!"}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,28 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
router = APIRouter()
|
||||
from ..dependencies import get_token_header
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/items",
|
||||
tags=["items"],
|
||||
dependencies=[Depends(get_token_header)],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
|
||||
fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def read_items():
|
||||
return [{"name": "Item Foo"}, {"name": "item Bar"}]
|
||||
return fake_items_db
|
||||
|
||||
|
||||
@router.get("/{item_id}")
|
||||
async def read_item(item_id: str):
|
||||
return {"name": "Fake Specific Item", "item_id": item_id}
|
||||
if item_id not in fake_items_db:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
return {"name": fake_items_db[item_id]["name"], "item_id": item_id}
|
||||
|
||||
|
||||
@router.put(
|
||||
|
|
@ -19,6 +31,8 @@ async def read_item(item_id: str):
|
|||
responses={403: {"description": "Operation forbidden"}},
|
||||
)
|
||||
async def update_item(item_id: str):
|
||||
if item_id != "foo":
|
||||
raise HTTPException(status_code=403, detail="You can only update the item: foo")
|
||||
return {"item_id": item_id, "name": "The Fighters"}
|
||||
if item_id != "plumbus":
|
||||
raise HTTPException(
|
||||
status_code=403, detail="You can only update the item: plumbus"
|
||||
)
|
||||
return {"item_id": item_id, "name": "The great Plumbus"}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ router = APIRouter()
|
|||
|
||||
@router.get("/users/", tags=["users"])
|
||||
async def read_users():
|
||||
return [{"username": "Foo"}, {"username": "Bar"}]
|
||||
return [{"username": "Rick"}, {"username": "Morty"}]
|
||||
|
||||
|
||||
@router.get("/users/me", tags=["users"])
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
from fastapi import Depends, FastAPI, Header, HTTPException
|
||||
|
||||
|
||||
async def verify_token(x_token: str = Header(...)):
|
||||
if x_token != "fake-super-secret-token":
|
||||
raise HTTPException(status_code=400, detail="X-Token header invalid")
|
||||
|
||||
|
||||
async def verify_key(x_key: str = Header(...)):
|
||||
if x_key != "fake-super-secret-key":
|
||||
raise HTTPException(status_code=400, detail="X-Key header invalid")
|
||||
return x_key
|
||||
|
||||
|
||||
app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items():
|
||||
return [{"item": "Portal Gun"}, {"item": "Plumbus"}]
|
||||
|
||||
|
||||
@app.get("/users/")
|
||||
async def read_users():
|
||||
return [{"username": "Rick"}, {"username": "Morty"}]
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
|
||||
app = FastAPI()
|
||||
|
|
@ -23,7 +22,7 @@ class InvoiceEventReceived(BaseModel):
|
|||
ok: bool
|
||||
|
||||
|
||||
invoices_callback_router = APIRouter(default_response_class=JSONResponse)
|
||||
invoices_callback_router = APIRouter()
|
||||
|
||||
|
||||
@invoices_callback_router.post(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Type, Union
|
|||
|
||||
from fastapi import routing
|
||||
from fastapi.concurrency import AsyncExitStack
|
||||
from fastapi.datastructures import Default, DefaultPlaceholder
|
||||
from fastapi.encoders import DictIntStrAny, SetIntStr
|
||||
from fastapi.exception_handlers import (
|
||||
http_exception_handler,
|
||||
|
|
@ -38,7 +39,8 @@ class FastAPI(Starlette):
|
|||
openapi_url: Optional[str] = "/openapi.json",
|
||||
openapi_tags: Optional[List[Dict[str, Any]]] = None,
|
||||
servers: Optional[List[Dict[str, Union[str, Any]]]] = None,
|
||||
default_response_class: Type[Response] = JSONResponse,
|
||||
dependencies: Optional[Sequence[Depends]] = None,
|
||||
default_response_class: Type[Response] = Default(JSONResponse),
|
||||
docs_url: Optional[str] = "/docs",
|
||||
redoc_url: Optional[str] = "/redoc",
|
||||
swagger_ui_oauth2_redirect_url: Optional[str] = "/docs/oauth2-redirect",
|
||||
|
|
@ -52,16 +54,25 @@ class FastAPI(Starlette):
|
|||
openapi_prefix: str = "",
|
||||
root_path: str = "",
|
||||
root_path_in_servers: bool = True,
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
callbacks: Optional[List[routing.APIRoute]] = None,
|
||||
deprecated: bool = None,
|
||||
include_in_schema: bool = True,
|
||||
**extra: Any,
|
||||
) -> None:
|
||||
self.default_response_class = default_response_class
|
||||
self._debug = debug
|
||||
self.state = State()
|
||||
self.router: routing.APIRouter = routing.APIRouter(
|
||||
routes,
|
||||
routes=routes,
|
||||
dependency_overrides_provider=self,
|
||||
on_startup=on_startup,
|
||||
on_shutdown=on_shutdown,
|
||||
default_response_class=default_response_class,
|
||||
dependencies=dependencies,
|
||||
callbacks=callbacks,
|
||||
deprecated=deprecated,
|
||||
include_in_schema=include_in_schema,
|
||||
responses=responses,
|
||||
)
|
||||
self.exception_handlers = (
|
||||
{} if exception_handlers is None else dict(exception_handlers)
|
||||
|
|
@ -203,7 +214,9 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Union[Type[Response], DefaultPlaceholder] = Default(
|
||||
JSONResponse
|
||||
),
|
||||
name: Optional[str] = None,
|
||||
) -> None:
|
||||
self.router.add_api_route(
|
||||
|
|
@ -211,12 +224,12 @@ class FastAPI(Starlette):
|
|||
endpoint=endpoint,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
methods=methods,
|
||||
operation_id=operation_id,
|
||||
|
|
@ -227,7 +240,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
|
|
@ -253,7 +266,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
) -> Callable:
|
||||
def decorator(func: Callable) -> Callable:
|
||||
|
|
@ -262,12 +275,12 @@ class FastAPI(Starlette):
|
|||
func,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
methods=methods,
|
||||
operation_id=operation_id,
|
||||
|
|
@ -278,7 +291,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
return func
|
||||
|
|
@ -305,16 +318,21 @@ class FastAPI(Starlette):
|
|||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[Depends]] = None,
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
default_response_class: Optional[Type[Response]] = None,
|
||||
deprecated: bool = None,
|
||||
include_in_schema: bool = True,
|
||||
default_response_class: Type[Response] = Default(JSONResponse),
|
||||
callbacks: Optional[List[routing.APIRoute]] = None,
|
||||
) -> None:
|
||||
self.router.include_router(
|
||||
router,
|
||||
prefix=prefix,
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
responses=responses or {},
|
||||
default_response_class=default_response_class
|
||||
or self.default_response_class,
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
include_in_schema=include_in_schema,
|
||||
default_response_class=default_response_class,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
def get(
|
||||
|
|
@ -338,7 +356,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[routing.APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -346,12 +364,12 @@ class FastAPI(Starlette):
|
|||
path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
response_model_include=response_model_include,
|
||||
|
|
@ -361,7 +379,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
@ -387,7 +405,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[routing.APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -395,12 +413,12 @@ class FastAPI(Starlette):
|
|||
path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
response_model_include=response_model_include,
|
||||
|
|
@ -410,7 +428,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
@ -436,7 +454,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[routing.APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -444,12 +462,12 @@ class FastAPI(Starlette):
|
|||
path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
response_model_include=response_model_include,
|
||||
|
|
@ -459,7 +477,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
@ -485,7 +503,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[routing.APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -493,12 +511,12 @@ class FastAPI(Starlette):
|
|||
path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
response_model_include=response_model_include,
|
||||
response_model_exclude=response_model_exclude,
|
||||
|
|
@ -508,7 +526,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
@ -534,7 +552,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[routing.APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -542,12 +560,12 @@ class FastAPI(Starlette):
|
|||
path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
response_model_include=response_model_include,
|
||||
|
|
@ -557,7 +575,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
@ -583,7 +601,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[routing.APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -591,12 +609,12 @@ class FastAPI(Starlette):
|
|||
path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
response_model_include=response_model_include,
|
||||
|
|
@ -606,7 +624,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
@ -632,7 +650,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[routing.APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -640,12 +658,12 @@ class FastAPI(Starlette):
|
|||
path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
response_model_include=response_model_include,
|
||||
|
|
@ -655,7 +673,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
@ -681,7 +699,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[routing.APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -689,12 +707,12 @@ class FastAPI(Starlette):
|
|||
path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
response_model_include=response_model_include,
|
||||
|
|
@ -704,7 +722,7 @@ class FastAPI(Starlette):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any, Callable, Iterable, Type
|
||||
from typing import Any, Callable, Iterable, Type, TypeVar
|
||||
|
||||
from starlette.datastructures import UploadFile as StarletteUploadFile
|
||||
|
||||
|
|
@ -13,3 +13,34 @@ class UploadFile(StarletteUploadFile):
|
|||
if not isinstance(v, StarletteUploadFile):
|
||||
raise ValueError(f"Expected UploadFile, received: {type(v)}")
|
||||
return v
|
||||
|
||||
|
||||
class DefaultPlaceholder:
|
||||
"""
|
||||
You shouldn't use this class directly.
|
||||
|
||||
It's used internally to recognize when a default value has been overwritten, even
|
||||
if the overriden default value was truthy.
|
||||
"""
|
||||
|
||||
def __init__(self, value: Any):
|
||||
self.value = value
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.value)
|
||||
|
||||
def __eq__(self, o: object) -> bool:
|
||||
return isinstance(o, DefaultPlaceholder) and o.value == self.value
|
||||
|
||||
|
||||
DefaultType = TypeVar("DefaultType")
|
||||
|
||||
|
||||
def Default(value: DefaultType) -> DefaultType:
|
||||
"""
|
||||
You shouldn't use this function directly.
|
||||
|
||||
It's used internally to recognize when a default value has been overwritten, even
|
||||
if the overriden default value was truthy.
|
||||
"""
|
||||
return DefaultPlaceholder(value) # type: ignore
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from enum import Enum
|
|||
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
|
||||
|
||||
from fastapi import routing
|
||||
from fastapi.datastructures import DefaultPlaceholder
|
||||
from fastapi.dependencies.models import Dependant
|
||||
from fastapi.dependencies.utils import get_flat_dependant, get_flat_params
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
|
@ -159,8 +160,12 @@ def get_openapi_path(
|
|||
security_schemes: Dict[str, Any] = {}
|
||||
definitions: Dict[str, Any] = {}
|
||||
assert route.methods is not None, "Methods must be a list"
|
||||
assert route.response_class, "A response class is needed to generate OpenAPI"
|
||||
route_response_media_type: Optional[str] = route.response_class.media_type
|
||||
if isinstance(route.response_class, DefaultPlaceholder):
|
||||
current_response_class: Type[routing.Response] = route.response_class.value
|
||||
else:
|
||||
current_response_class = route.response_class
|
||||
assert current_response_class, "A response class is needed to generate OpenAPI"
|
||||
route_response_media_type: Optional[str] = current_response_class.media_type
|
||||
if route.include_in_schema:
|
||||
for method in route.methods:
|
||||
operation = get_openapi_operation_metadata(route=route, method=method)
|
||||
|
|
@ -205,7 +210,7 @@ def get_openapi_path(
|
|||
and route.status_code not in STATUS_CODES_WITH_NO_BODY
|
||||
):
|
||||
response_schema = {"type": "string"}
|
||||
if lenient_issubclass(route.response_class, JSONResponse):
|
||||
if lenient_issubclass(current_response_class, JSONResponse):
|
||||
if route.response_field:
|
||||
response_schema, _, _ = field_schema(
|
||||
route.response_field,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import json
|
|||
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Type, Union
|
||||
|
||||
from fastapi import params
|
||||
from fastapi.datastructures import Default, DefaultPlaceholder
|
||||
from fastapi.dependencies.models import Dependant
|
||||
from fastapi.dependencies.utils import (
|
||||
get_body_field,
|
||||
|
|
@ -19,6 +20,7 @@ from fastapi.utils import (
|
|||
create_cloned_field,
|
||||
create_response_field,
|
||||
generate_operation_id_for_path,
|
||||
get_value_or_default,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from pydantic.error_wrappers import ErrorWrapper, ValidationError
|
||||
|
|
@ -139,7 +141,7 @@ def get_request_handler(
|
|||
dependant: Dependant,
|
||||
body_field: Optional[ModelField] = None,
|
||||
status_code: int = 200,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse),
|
||||
response_field: Optional[ModelField] = None,
|
||||
response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
|
||||
|
|
@ -152,6 +154,10 @@ def get_request_handler(
|
|||
assert dependant.call is not None, "dependant.call must be a function"
|
||||
is_coroutine = asyncio.iscoroutinefunction(dependant.call)
|
||||
is_body_form = body_field and isinstance(body_field.field_info, params.Form)
|
||||
if isinstance(response_class, DefaultPlaceholder):
|
||||
actual_response_class: Type[Response] = response_class.value
|
||||
else:
|
||||
actual_response_class = response_class
|
||||
|
||||
async def app(request: Request) -> Response:
|
||||
try:
|
||||
|
|
@ -198,7 +204,7 @@ def get_request_handler(
|
|||
exclude_none=response_model_exclude_none,
|
||||
is_coroutine=is_coroutine,
|
||||
)
|
||||
response = response_class(
|
||||
response = actual_response_class(
|
||||
content=response_data,
|
||||
status_code=status_code,
|
||||
background=background_tasks,
|
||||
|
|
@ -277,7 +283,9 @@ class APIRoute(routing.Route):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Union[Type[Response], DefaultPlaceholder] = Default(
|
||||
JSONResponse
|
||||
),
|
||||
dependency_overrides_provider: Optional[Any] = None,
|
||||
callbacks: Optional[List["APIRoute"]] = None,
|
||||
) -> None:
|
||||
|
|
@ -372,7 +380,7 @@ class APIRoute(routing.Route):
|
|||
dependant=self.dependant,
|
||||
body_field=self.body_field,
|
||||
status_code=self.status_code,
|
||||
response_class=self.response_class or JSONResponse,
|
||||
response_class=self.response_class,
|
||||
response_field=self.secure_cloned_response_field,
|
||||
response_model_include=self.response_model_include,
|
||||
response_model_exclude=self.response_model_exclude,
|
||||
|
|
@ -387,14 +395,22 @@ class APIRoute(routing.Route):
|
|||
class APIRouter(routing.Router):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
prefix: str = "",
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[params.Depends]] = None,
|
||||
default_response_class: Type[Response] = Default(JSONResponse),
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
callbacks: Optional[List[APIRoute]] = None,
|
||||
routes: Optional[List[routing.BaseRoute]] = None,
|
||||
redirect_slashes: bool = True,
|
||||
default: Optional[ASGIApp] = None,
|
||||
dependency_overrides_provider: Optional[Any] = None,
|
||||
route_class: Type[APIRoute] = APIRoute,
|
||||
default_response_class: Optional[Type[Response]] = None,
|
||||
on_startup: Optional[Sequence[Callable]] = None,
|
||||
on_shutdown: Optional[Sequence[Callable]] = None,
|
||||
deprecated: bool = None,
|
||||
include_in_schema: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
routes=routes,
|
||||
|
|
@ -403,6 +419,18 @@ class APIRouter(routing.Router):
|
|||
on_startup=on_startup,
|
||||
on_shutdown=on_shutdown,
|
||||
)
|
||||
if prefix:
|
||||
assert prefix.startswith("/"), "A path prefix must start with '/'"
|
||||
assert not prefix.endswith(
|
||||
"/"
|
||||
), "A path prefix must not end with '/', as the routes will start with '/'"
|
||||
self.prefix = prefix
|
||||
self.tags: List[str] = tags or []
|
||||
self.dependencies = list(dependencies or []) or []
|
||||
self.deprecated = deprecated
|
||||
self.include_in_schema = include_in_schema
|
||||
self.responses = responses or {}
|
||||
self.callbacks = callbacks or []
|
||||
self.dependency_overrides_provider = dependency_overrides_provider
|
||||
self.route_class = route_class
|
||||
self.default_response_class = default_response_class
|
||||
|
|
@ -430,24 +458,40 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Union[Type[Response], DefaultPlaceholder] = Default(
|
||||
JSONResponse
|
||||
),
|
||||
name: Optional[str] = None,
|
||||
route_class_override: Optional[Type[APIRoute]] = None,
|
||||
callbacks: Optional[List[APIRoute]] = None,
|
||||
) -> None:
|
||||
route_class = route_class_override or self.route_class
|
||||
responses = responses or {}
|
||||
combined_responses = {**self.responses, **responses}
|
||||
current_response_class = get_value_or_default(
|
||||
response_class, self.default_response_class
|
||||
)
|
||||
current_tags = self.tags.copy()
|
||||
if tags:
|
||||
current_tags.extend(tags)
|
||||
current_dependencies = self.dependencies.copy()
|
||||
if dependencies:
|
||||
current_dependencies.extend(dependencies)
|
||||
current_callbacks = self.callbacks.copy()
|
||||
if callbacks:
|
||||
current_callbacks.extend(callbacks)
|
||||
route = route_class(
|
||||
path,
|
||||
self.prefix + path,
|
||||
endpoint=endpoint,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies,
|
||||
tags=current_tags,
|
||||
dependencies=current_dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
responses=combined_responses,
|
||||
deprecated=deprecated or self.deprecated,
|
||||
methods=methods,
|
||||
operation_id=operation_id,
|
||||
response_model_include=response_model_include,
|
||||
|
|
@ -456,11 +500,11 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_unset=response_model_exclude_unset,
|
||||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
include_in_schema=include_in_schema and self.include_in_schema,
|
||||
response_class=current_response_class,
|
||||
name=name,
|
||||
dependency_overrides_provider=self.dependency_overrides_provider,
|
||||
callbacks=callbacks,
|
||||
callbacks=current_callbacks,
|
||||
)
|
||||
self.routes.append(route)
|
||||
|
||||
|
|
@ -486,7 +530,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -496,12 +540,12 @@ class APIRouter(routing.Router):
|
|||
func,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
methods=methods,
|
||||
operation_id=operation_id,
|
||||
|
|
@ -512,7 +556,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
@ -545,8 +589,11 @@ class APIRouter(routing.Router):
|
|||
prefix: str = "",
|
||||
tags: Optional[List[str]] = None,
|
||||
dependencies: Optional[Sequence[params.Depends]] = None,
|
||||
default_response_class: Type[Response] = Default(JSONResponse),
|
||||
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
||||
default_response_class: Optional[Type[Response]] = None,
|
||||
callbacks: Optional[List[APIRoute]] = None,
|
||||
deprecated: bool = None,
|
||||
include_in_schema: bool = True,
|
||||
) -> None:
|
||||
if prefix:
|
||||
assert prefix.startswith("/"), "A path prefix must start with '/'"
|
||||
|
|
@ -566,19 +613,39 @@ class APIRouter(routing.Router):
|
|||
for route in router.routes:
|
||||
if isinstance(route, APIRoute):
|
||||
combined_responses = {**responses, **route.responses}
|
||||
use_response_class = get_value_or_default(
|
||||
route.response_class,
|
||||
router.default_response_class,
|
||||
default_response_class,
|
||||
self.default_response_class,
|
||||
)
|
||||
current_tags = []
|
||||
if tags:
|
||||
current_tags.extend(tags)
|
||||
if route.tags:
|
||||
current_tags.extend(route.tags)
|
||||
current_dependencies: List[params.Depends] = []
|
||||
if dependencies:
|
||||
current_dependencies.extend(dependencies)
|
||||
if route.dependencies:
|
||||
current_dependencies.extend(route.dependencies)
|
||||
current_callbacks = []
|
||||
if callbacks:
|
||||
current_callbacks.extend(callbacks)
|
||||
if route.callbacks:
|
||||
current_callbacks.extend(route.callbacks)
|
||||
self.add_api_route(
|
||||
prefix + route.path,
|
||||
route.endpoint,
|
||||
response_model=route.response_model,
|
||||
status_code=route.status_code,
|
||||
tags=(route.tags or []) + (tags or []),
|
||||
dependencies=list(dependencies or [])
|
||||
+ list(route.dependencies or []),
|
||||
tags=current_tags,
|
||||
dependencies=current_dependencies,
|
||||
summary=route.summary,
|
||||
description=route.description,
|
||||
response_description=route.response_description,
|
||||
responses=combined_responses,
|
||||
deprecated=route.deprecated,
|
||||
deprecated=route.deprecated or deprecated or self.deprecated,
|
||||
methods=route.methods,
|
||||
operation_id=route.operation_id,
|
||||
response_model_include=route.response_model_include,
|
||||
|
|
@ -587,11 +654,13 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_unset=route.response_model_exclude_unset,
|
||||
response_model_exclude_defaults=route.response_model_exclude_defaults,
|
||||
response_model_exclude_none=route.response_model_exclude_none,
|
||||
include_in_schema=route.include_in_schema,
|
||||
response_class=route.response_class or default_response_class,
|
||||
include_in_schema=route.include_in_schema
|
||||
and self.include_in_schema
|
||||
and include_in_schema,
|
||||
response_class=use_response_class,
|
||||
name=route.name,
|
||||
route_class_override=type(route),
|
||||
callbacks=route.callbacks,
|
||||
callbacks=current_callbacks,
|
||||
)
|
||||
elif isinstance(route, routing.Route):
|
||||
self.add_route(
|
||||
|
|
@ -635,7 +704,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -643,12 +712,12 @@ class APIRouter(routing.Router):
|
|||
path=path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
methods=["GET"],
|
||||
operation_id=operation_id,
|
||||
|
|
@ -659,7 +728,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
@ -685,7 +754,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -693,12 +762,12 @@ class APIRouter(routing.Router):
|
|||
path=path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
methods=["PUT"],
|
||||
operation_id=operation_id,
|
||||
|
|
@ -709,7 +778,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
@ -735,7 +804,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -743,12 +812,12 @@ class APIRouter(routing.Router):
|
|||
path=path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
methods=["POST"],
|
||||
operation_id=operation_id,
|
||||
|
|
@ -759,7 +828,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
@ -785,7 +854,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -793,12 +862,12 @@ class APIRouter(routing.Router):
|
|||
path=path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
methods=["DELETE"],
|
||||
operation_id=operation_id,
|
||||
|
|
@ -809,7 +878,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
@ -835,7 +904,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -843,12 +912,12 @@ class APIRouter(routing.Router):
|
|||
path=path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
methods=["OPTIONS"],
|
||||
operation_id=operation_id,
|
||||
|
|
@ -859,7 +928,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
@ -885,7 +954,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -893,12 +962,12 @@ class APIRouter(routing.Router):
|
|||
path=path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
methods=["HEAD"],
|
||||
operation_id=operation_id,
|
||||
|
|
@ -909,7 +978,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
@ -935,7 +1004,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -943,12 +1012,12 @@ class APIRouter(routing.Router):
|
|||
path=path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
methods=["PATCH"],
|
||||
operation_id=operation_id,
|
||||
|
|
@ -959,7 +1028,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
@ -985,7 +1054,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults: bool = False,
|
||||
response_model_exclude_none: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
response_class: Type[Response] = Default(JSONResponse),
|
||||
name: Optional[str] = None,
|
||||
callbacks: Optional[List[APIRoute]] = None,
|
||||
) -> Callable:
|
||||
|
|
@ -994,12 +1063,12 @@ class APIRouter(routing.Router):
|
|||
path=path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
responses=responses,
|
||||
deprecated=deprecated,
|
||||
methods=["TRACE"],
|
||||
operation_id=operation_id,
|
||||
|
|
@ -1010,7 +1079,7 @@ class APIRouter(routing.Router):
|
|||
response_model_exclude_defaults=response_model_exclude_defaults,
|
||||
response_model_exclude_none=response_model_exclude_none,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class or self.default_response_class,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from enum import Enum
|
|||
from typing import Any, Dict, Optional, Set, Type, Union, cast
|
||||
|
||||
import fastapi
|
||||
from fastapi.datastructures import DefaultPlaceholder, DefaultType
|
||||
from fastapi.openapi.constants import REF_PREFIX
|
||||
from pydantic import BaseConfig, BaseModel, create_model
|
||||
from pydantic.class_validators import Validator
|
||||
|
|
@ -136,3 +137,21 @@ def deep_dict_update(main_dict: dict, update_dict: dict) -> None:
|
|||
deep_dict_update(main_dict[key], update_dict[key])
|
||||
else:
|
||||
main_dict[key] = update_dict[key]
|
||||
|
||||
|
||||
def get_value_or_default(
|
||||
first_item: Union[DefaultPlaceholder, DefaultType],
|
||||
*extra_items: Union[DefaultPlaceholder, DefaultType],
|
||||
) -> Union[DefaultPlaceholder, DefaultType]:
|
||||
"""
|
||||
Pass items or `DefaultPlaceholder`s by descending priority.
|
||||
|
||||
The first one to _not_ be a `DefaultPlaceholder` will be returned.
|
||||
|
||||
Otherwise, the first item (a `DefaultPlaceholder`) will be returned.
|
||||
"""
|
||||
items = (first_item,) + extra_items
|
||||
for item in items:
|
||||
if not isinstance(item, DefaultPlaceholder):
|
||||
return item
|
||||
return first_item
|
||||
|
|
|
|||
|
|
@ -1,7 +1,22 @@
|
|||
import pytest
|
||||
from fastapi import UploadFile
|
||||
from fastapi.datastructures import Default
|
||||
|
||||
|
||||
def test_upload_file_invalid():
|
||||
with pytest.raises(ValueError):
|
||||
UploadFile.validate("not a Starlette UploadFile")
|
||||
|
||||
|
||||
def test_default_placeholder_equals():
|
||||
placeholder_1 = Default("a")
|
||||
placeholder_2 = Default("a")
|
||||
assert placeholder_1 == placeholder_2
|
||||
assert placeholder_1.value == placeholder_2.value
|
||||
|
||||
|
||||
def test_default_placeholder_bool():
|
||||
placeholder_a = Default("a")
|
||||
placeholder_b = Default("")
|
||||
assert placeholder_a
|
||||
assert not placeholder_b
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,6 @@
|
|||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
|
||||
|
|
@ -24,14 +23,27 @@ class InvoiceEventReceived(BaseModel):
|
|||
ok: bool
|
||||
|
||||
|
||||
invoices_callback_router = APIRouter(default_response_class=JSONResponse)
|
||||
invoices_callback_router = APIRouter()
|
||||
|
||||
|
||||
@invoices_callback_router.post(
|
||||
"{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived,
|
||||
)
|
||||
def invoice_notification(body: InvoiceEvent):
|
||||
pass
|
||||
pass # pragma: nocover
|
||||
|
||||
|
||||
class Event(BaseModel):
|
||||
name: str
|
||||
total: float
|
||||
|
||||
|
||||
events_callback_router = APIRouter()
|
||||
|
||||
|
||||
@events_callback_router.get("{$callback_url}/events/{$request.body.title}")
|
||||
def event_callback(event: Event):
|
||||
pass # pragma: nocover
|
||||
|
||||
|
||||
subrouter = APIRouter()
|
||||
|
|
@ -58,7 +70,7 @@ def create_invoice(invoice: Invoice, callback_url: Optional[HttpUrl] = None):
|
|||
return {"msg": "Invoice received"}
|
||||
|
||||
|
||||
app.include_router(subrouter)
|
||||
app.include_router(subrouter, callbacks=events_callback_router.routes)
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
|
@ -110,6 +122,40 @@ openapi_schema = {
|
|||
},
|
||||
},
|
||||
"callbacks": {
|
||||
"event_callback": {
|
||||
"{$callback_url}/events/{$request.body.title}": {
|
||||
"get": {
|
||||
"summary": "Event Callback",
|
||||
"operationId": "event_callback__callback_url__events___request_body_title__get",
|
||||
"requestBody": {
|
||||
"required": True,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Event"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"invoice_notification": {
|
||||
"{$callback_url}/invoices/{$request.body.id}": {
|
||||
"post": {
|
||||
|
|
@ -149,13 +195,22 @@ openapi_schema = {
|
|||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Event": {
|
||||
"title": "Event",
|
||||
"required": ["name", "total"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"total": {"title": "Total", "type": "number"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
|
|
@ -225,8 +280,3 @@ def test_get():
|
|||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"msg": "Invoice received"}
|
||||
|
||||
|
||||
def test_dummy_callback():
|
||||
# Just for coverage
|
||||
invoice_notification({})
|
||||
|
|
|
|||
|
|
@ -11,32 +11,17 @@ openapi_schema = {
|
|||
"paths": {
|
||||
"/users/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"summary": "Read Users",
|
||||
"operationId": "read_users_users__get",
|
||||
}
|
||||
},
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Token", "type": "string"},
|
||||
"name": "token",
|
||||
"in": "query",
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"summary": "Read User Me",
|
||||
"operationId": "read_user_me_users_me_get",
|
||||
}
|
||||
},
|
||||
"/users/{username}": {
|
||||
"get": {
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
|
|
@ -53,6 +38,41 @@ openapi_schema = {
|
|||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"tags": ["users"],
|
||||
"summary": "Read User Me",
|
||||
"operationId": "read_user_me_users_me_get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Token", "type": "string"},
|
||||
"name": "token",
|
||||
"in": "query",
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/users/{username}": {
|
||||
"get": {
|
||||
"tags": ["users"],
|
||||
"summary": "Read User",
|
||||
"operationId": "read_user_users__username__get",
|
||||
|
|
@ -62,14 +82,15 @@ openapi_schema = {
|
|||
"schema": {"title": "Username", "type": "string"},
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
}
|
||||
},
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Token", "type": "string"},
|
||||
"name": "token",
|
||||
"in": "query",
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"404": {"description": "Not found"},
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
|
|
@ -85,27 +106,33 @@ openapi_schema = {
|
|||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/items/": {
|
||||
"get": {
|
||||
"tags": ["items"],
|
||||
"summary": "Read Items",
|
||||
"operationId": "read_items_items__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Token", "type": "string"},
|
||||
"name": "token",
|
||||
"in": "query",
|
||||
},
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "X-Token", "type": "string"},
|
||||
"name": "x-token",
|
||||
"in": "header",
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
"/items/{item_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"404": {"description": "Not found"},
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"404": {"description": "Not found"},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
|
|
@ -117,6 +144,10 @@ openapi_schema = {
|
|||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/items/{item_id}": {
|
||||
"get": {
|
||||
"tags": ["items"],
|
||||
"summary": "Read Item",
|
||||
"operationId": "read_item_items__item_id__get",
|
||||
|
|
@ -127,6 +158,12 @@ openapi_schema = {
|
|||
"name": "item_id",
|
||||
"in": "path",
|
||||
},
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Token", "type": "string"},
|
||||
"name": "token",
|
||||
"in": "query",
|
||||
},
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "X-Token", "type": "string"},
|
||||
|
|
@ -134,11 +171,119 @@ openapi_schema = {
|
|||
"in": "header",
|
||||
},
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"404": {"description": "Not found"},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"put": {
|
||||
"tags": ["items", "custom"],
|
||||
"summary": "Update Item",
|
||||
"operationId": "update_item_items__item_id__put",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
},
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Token", "type": "string"},
|
||||
"name": "token",
|
||||
"in": "query",
|
||||
},
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "X-Token", "type": "string"},
|
||||
"name": "x-token",
|
||||
"in": "header",
|
||||
},
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"404": {"description": "Not found"},
|
||||
"403": {"description": "Operation forbidden"},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/admin/": {
|
||||
"post": {
|
||||
"tags": ["admin"],
|
||||
"summary": "Update Admin",
|
||||
"operationId": "update_admin_admin__post",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Token", "type": "string"},
|
||||
"name": "token",
|
||||
"in": "query",
|
||||
},
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "X-Token", "type": "string"},
|
||||
"name": "x-token",
|
||||
"in": "header",
|
||||
},
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"418": {"description": "I'm a teapot"},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/": {
|
||||
"get": {
|
||||
"summary": "Root",
|
||||
"operationId": "root__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Token", "type": "string"},
|
||||
"name": "token",
|
||||
"in": "query",
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
|
|
@ -154,28 +299,22 @@ openapi_schema = {
|
|||
},
|
||||
},
|
||||
},
|
||||
"tags": ["custom", "items"],
|
||||
"summary": "Update Item",
|
||||
"operationId": "update_item_items__item_id__put",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
},
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "X-Token", "type": "string"},
|
||||
"name": "x-token",
|
||||
"in": "header",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
|
|
@ -190,49 +329,64 @@ openapi_schema = {
|
|||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
no_jessica = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_status,expected_response,headers",
|
||||
[
|
||||
("/users", 200, [{"username": "Foo"}, {"username": "Bar"}], {}),
|
||||
("/users/foo", 200, {"username": "foo"}, {}),
|
||||
("/users/me", 200, {"username": "fakecurrentuser"}, {}),
|
||||
(
|
||||
"/items",
|
||||
"/users?token=jessica",
|
||||
200,
|
||||
[{"name": "Item Foo"}, {"name": "item Bar"}],
|
||||
[{"username": "Rick"}, {"username": "Morty"}],
|
||||
{},
|
||||
),
|
||||
("/users", 422, no_jessica, {}),
|
||||
("/users/foo?token=jessica", 200, {"username": "foo"}, {}),
|
||||
("/users/foo", 422, no_jessica, {}),
|
||||
("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}),
|
||||
("/users/me", 422, no_jessica, {}),
|
||||
(
|
||||
"/items?token=jessica",
|
||||
200,
|
||||
{"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}},
|
||||
{"X-Token": "fake-super-secret-token"},
|
||||
),
|
||||
("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
|
||||
(
|
||||
"/items/bar",
|
||||
"/items/plumbus?token=jessica",
|
||||
200,
|
||||
{"name": "Fake Specific Item", "item_id": "bar"},
|
||||
{"name": "Plumbus", "item_id": "plumbus"},
|
||||
{"X-Token": "fake-super-secret-token"},
|
||||
),
|
||||
("/items", 400, {"detail": "X-Token header invalid"}, {"X-Token": "invalid"}),
|
||||
("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}),
|
||||
(
|
||||
"/items/bar",
|
||||
"/items?token=jessica",
|
||||
400,
|
||||
{"detail": "X-Token header invalid"},
|
||||
{"X-Token": "invalid"},
|
||||
),
|
||||
(
|
||||
"/items",
|
||||
"/items/bar?token=jessica",
|
||||
400,
|
||||
{"detail": "X-Token header invalid"},
|
||||
{"X-Token": "invalid"},
|
||||
),
|
||||
(
|
||||
"/items?token=jessica",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
|
|
@ -246,7 +400,7 @@ openapi_schema = {
|
|||
{},
|
||||
),
|
||||
(
|
||||
"/items/bar",
|
||||
"/items/plumbus?token=jessica",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
|
|
@ -259,6 +413,8 @@ openapi_schema = {
|
|||
},
|
||||
{},
|
||||
),
|
||||
("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}),
|
||||
("/", 422, no_jessica, {}),
|
||||
("/openapi.json", 200, openapi_schema, {}),
|
||||
],
|
||||
)
|
||||
|
|
@ -273,11 +429,16 @@ def test_put_no_header():
|
|||
assert response.status_code == 422, response.text
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -289,12 +450,30 @@ def test_put_invalid_header():
|
|||
|
||||
|
||||
def test_put():
|
||||
response = client.put("/items/foo", headers={"X-Token": "fake-super-secret-token"})
|
||||
response = client.put(
|
||||
"/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"item_id": "foo", "name": "The Fighters"}
|
||||
assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"}
|
||||
|
||||
|
||||
def test_put_forbidden():
|
||||
response = client.put("/items/bar", headers={"X-Token": "fake-super-secret-token"})
|
||||
response = client.put(
|
||||
"/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
assert response.status_code == 403, response.text
|
||||
assert response.json() == {"detail": "You can only update the item: foo"}
|
||||
assert response.json() == {"detail": "You can only update the item: plumbus"}
|
||||
|
||||
|
||||
def test_admin():
|
||||
response = client.post(
|
||||
"/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"}
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"message": "Admin getting schwifty"}
|
||||
|
||||
|
||||
def test_admin_invalid_header():
|
||||
response = client.post("/admin/", headers={"X-Token": "invalid"})
|
||||
assert response.status_code == 400, response.text
|
||||
assert response.json() == {"detail": "X-Token header invalid"}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,209 @@
|
|||
from fastapi.testclient import TestClient
|
||||
|
||||
from docs_src.dependencies.tutorial012 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"summary": "Read Items",
|
||||
"operationId": "read_items_items__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "X-Token", "type": "string"},
|
||||
"name": "x-token",
|
||||
"in": "header",
|
||||
},
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "X-Key", "type": "string"},
|
||||
"name": "x-key",
|
||||
"in": "header",
|
||||
},
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/users/": {
|
||||
"get": {
|
||||
"summary": "Read Users",
|
||||
"operationId": "read_users_users__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "X-Token", "type": "string"},
|
||||
"name": "x-token",
|
||||
"in": "header",
|
||||
},
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "X-Key", "type": "string"},
|
||||
"name": "x-key",
|
||||
"in": "header",
|
||||
},
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"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"},
|
||||
}
|
||||
},
|
||||
},
|
||||
"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_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_get_no_headers_items():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["header", "x-key"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_get_no_headers_users():
|
||||
response = client.get("/users/")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "x-token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["header", "x-key"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_get_invalid_one_header_items():
|
||||
response = client.get("/items/", headers={"X-Token": "invalid"})
|
||||
assert response.status_code == 400, response.text
|
||||
assert response.json() == {"detail": "X-Token header invalid"}
|
||||
|
||||
|
||||
def test_get_invalid_one_users():
|
||||
response = client.get("/users/", headers={"X-Token": "invalid"})
|
||||
assert response.status_code == 400, response.text
|
||||
assert response.json() == {"detail": "X-Token header invalid"}
|
||||
|
||||
|
||||
def test_get_invalid_second_header_items():
|
||||
response = client.get(
|
||||
"/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"}
|
||||
)
|
||||
assert response.status_code == 400, response.text
|
||||
assert response.json() == {"detail": "X-Key header invalid"}
|
||||
|
||||
|
||||
def test_get_invalid_second_header_users():
|
||||
response = client.get(
|
||||
"/users/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"}
|
||||
)
|
||||
assert response.status_code == 400, response.text
|
||||
assert response.json() == {"detail": "X-Key header invalid"}
|
||||
|
||||
|
||||
def test_get_valid_headers_items():
|
||||
response = client.get(
|
||||
"/items/",
|
||||
headers={
|
||||
"X-Token": "fake-super-secret-token",
|
||||
"X-Key": "fake-super-secret-key",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == [{"item": "Portal Gun"}, {"item": "Plumbus"}]
|
||||
|
||||
|
||||
def test_get_valid_headers_users():
|
||||
response = client.get(
|
||||
"/users/",
|
||||
headers={
|
||||
"X-Token": "fake-super-secret-token",
|
||||
"X-Key": "fake-super-secret-key",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == [{"username": "Rick"}, {"username": "Morty"}]
|
||||
Loading…
Reference in New Issue