Browse Source

Add parameter dependencies to path operation decorators and include_router (#235)

*  Implement dependencies in decorator and .include_router

* 📝 Add docs for parameter dependencies

*  Add tests for dependencies parameter

* 🔥 Remove debugging prints in tests

* 📝 Update release notes
pull/241/head
Sebastián Ramírez 6 years ago
committed by GitHub
parent
commit
e92b43b5c8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      docs/release-notes.md
  2. 9
      docs/src/bigger_applications/app/main.py
  3. 24
      docs/src/dependencies/tutorial006.py
  4. 21
      docs/src/dependencies/tutorial007.py
  5. 36
      docs/tutorial/bigger-applications.md
  6. 8
      docs/tutorial/dependencies/advanced-dependencies.md
  7. 60
      docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md
  8. 4
      docs/tutorial/security/oauth2-scopes.md
  9. 28
      fastapi/applications.py
  10. 34
      fastapi/dependencies/utils.py
  11. 35
      fastapi/routing.py
  12. 1
      mkdocs.yml
  13. 121
      tests/test_tutorial/test_bigger_applications/test_main.py
  14. 128
      tests/test_tutorial/test_dependencies/test_tutorial006.py
  15. 2
      tests/test_tutorial/test_request_files/test_tutorial002.py
  16. 2
      tests/test_tutorial/test_security/test_tutorial005.py

12
docs/release-notes.md

@ -1,5 +1,17 @@
## Next release
* Add support for `dependencies` parameter:
* A parameter in *path operation decorators*, for dependencies that should be executed but the return value is not important or not used in the *path operation function*.
* A parameter in the `.include_router()` method of FastAPI applications and routers, to include dependencies that should be executed in each *path operation* in a router.
* This is useful, for example, to require authentication or permissions in specific group of *path operations*.
* Different `dependencies` can be applied to different routers.
* These `dependencies` are run before the normal parameter dependencies. And normal dependencies are run too. They can be combined.
* Dependencies declared in a router are executed first, then the ones defined in *path operation decorators*, and then the ones declared in normal parameters. They are all combined and executed.
* All this also supports using `Security` with `scopes` in those `dependencies` parameters, for more advanced OAuth 2.0 security scenarios with scopes.
* New documentation about [dependencies in *path operation decorators*](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/).
* New documentation about [dependencies in the `include_router()` method](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-prefix-tags-responses-and-dependencies).
* PR [#235](https://github.com/tiangolo/fastapi/pull/235).
* Fix OpenAPI documentation of Starlette URL convertors. Specially useful when using `path` convertors, to take a whole path as a parameter, like `/some/url/{p:path}`. PR [#234](https://github.com/tiangolo/fastapi/pull/234) by [@euri10](https://github.com/euri10).
* Make default parameter utilities exported from `fastapi` be functions instead of classes (the new functions return instances of those classes). To be able to override the return types and fix `mypy` errors in FastAPI's users' code. Applies to `Path`, `Query`, `Header`, `Cookie`, `Body`, `Form`, `File`, `Depends`, and `Security`. PR [#226](https://github.com/tiangolo/fastapi/pull/226) and PR [#231](https://github.com/tiangolo/fastapi/pull/231).

9
docs/src/bigger_applications/app/main.py

@ -1,13 +1,20 @@
from fastapi import FastAPI
from fastapi import Depends, FastAPI, Header, HTTPException
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.include_router(users.router)
app.include_router(
items.router,
prefix="/items",
tags=["items"],
dependencies=[Depends(get_token_header)],
responses={404: {"description": "Not found"}},
)

24
docs/src/dependencies/tutorial006.py

@ -1,21 +1,19 @@
from fastapi import Depends, FastAPI
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
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")
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
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
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: bool = Depends(checker)):
return {"fixed_content_in_query": fixed_content_included}
@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
return [{"item": "Foo"}, {"item": "Bar"}]

21
docs/src/dependencies/tutorial007.py

@ -0,0 +1,21 @@
from fastapi import Depends, FastAPI
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: bool = Depends(checker)):
return {"fixed_content_in_query": fixed_content_included}

36
docs/tutorial/bigger-applications.md

@ -31,7 +31,6 @@ 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").
@ -107,7 +106,7 @@ And we don't want to have to explicitly type `/items/` and `tags=["items"]` in e
{!./src/bigger_applications/app/routers/items.py!}
```
### Add some custom `tags` and `responses`
### Add some custom `tags`, `responses`, and `dependencies`
We are not adding the prefix `/items/` nor the `tags=["items"]` to add them later.
@ -197,12 +196,11 @@ So, to be able to use both of them in the same file, we import the submodules di
{!./src/bigger_applications/app/main.py!}
```
### Include an `APIRouter`
Now, let's include the `router` from the submodule `users`:
```Python hl_lines="7"
```Python hl_lines="13"
{!./src/bigger_applications/app/main.py!}
```
@ -226,8 +224,7 @@ It will include all the routes from that router as part of it.
So it won't affect performance.
### Include an `APIRouter` with a `prefix`, `tags`, and `responses`
### Include an `APIRouter` with a `prefix`, `tags`, `responses`, and `dependencies`
Now, let's include the router from the `items` submodule.
@ -251,7 +248,9 @@ We can also add a list of `tags` that will be applied to all the *path operation
And we can add predefined `responses` that will be included in all the *path operations* too.
```Python hl_lines="8 9 10 11 12 13"
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.
```Python hl_lines="8 9 10 14 15 16 17 18 19 20"
{!./src/bigger_applications/app/main.py!}
```
@ -262,19 +261,21 @@ The end result is that the item paths are now:
...as we intended.
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`.
* 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 <a href="https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-decorator/" target="_blank">`dependencies` in the decorator</a>, and then the normal parameter dependencies.
* You can also add <a href="https://fastapi.tiangolo.com/tutorial/security/oauth2-scopes/" target="_blank">`Security` dependencies with `scopes`</a>.
These "tags" are especially useful for the automatic interactive documentation systems (using OpenAPI).
And all of them will include the 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.
!!! 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.
!!! check
The `prefix`, `tags`, and `responses` parameters are (as in many other cases) just a feature from **FastAPI** to help you avoid code duplication.
The `prefix`, `tags`, `responses` and `dependencies` parameters are (as in many other cases) just a feature from **FastAPI** to help you avoid code duplication.
!!! tip
You could also add path operations directly, for example with: `@app.get(...)`.
@ -283,7 +284,6 @@ The *path operation* that declared a custom `403` response will have both the pr
It would still work the same.
!!! info "Very Technical Details"
**Note**: this is a very technical detail that you probably can **just skip**.

8
docs/tutorial/dependencies/advanced-dependencies.md

@ -22,7 +22,7 @@ Not the class itself (which is already a callable), but an instance of that clas
To do that, we declare a method `__call__`:
```Python hl_lines="10"
{!./src/dependencies/tutorial006.py!}
{!./src/dependencies/tutorial007.py!}
```
In this case, this `__call__` is what **FastAPI** will use to check for additional parameters and sub-dependencies, and this is what will be called to pass a value to the parameter in your *path operation function* later.
@ -32,7 +32,7 @@ In this case, this `__call__` is what **FastAPI** will use to check for addition
And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency:
```Python hl_lines="7"
{!./src/dependencies/tutorial006.py!}
{!./src/dependencies/tutorial007.py!}
```
In this case, **FastAPI** won't ever touch or care about `__init__`, we will use it directly in our code.
@ -42,7 +42,7 @@ In this case, **FastAPI** won't ever touch or care about `__init__`, we will use
We could create an instance of this class with:
```Python hl_lines="16"
{!./src/dependencies/tutorial006.py!}
{!./src/dependencies/tutorial007.py!}
```
And that way we are able to "parameterize" our dependency, that now has `"bar"` inside of it, as the attribute `checker.fixed_content`.
@ -60,7 +60,7 @@ checker(q="somequery")
...and pass whatever that returns as the value of the dependency in our path operation function as the parameter `fixed_content_included`:
```Python hl_lines="20"
{!./src/dependencies/tutorial006.py!}
{!./src/dependencies/tutorial007.py!}
```
!!! tip

60
docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md

@ -0,0 +1,60 @@
In some cases you don't really need the return value of a dependency inside your *path operation function*.
Or the dependency doesn't return a value.
But you still need it to be executed/solved.
For those cases, instead of declaring a *path operation function* parameter with `Depends`, you can add a `list` of `dependencies` to the *path operation decorator*.
## Add `dependencies` to the *path operation decorator*
The *path operation decorator* receives an optional argument `dependencies`.
It should be a `list` of `Depends()`:
```Python hl_lines="17"
{!./src/dependencies/tutorial006.py!}
```
These dependencies will be executed/solved the same way normal dependencies. But their value (if they return any) won't be passed to your *path operation function*.
!!! tip
Some editors check for unused function parameters, and show them as errors.
Using these `dependencies` in the *path operation decorator* you can make sure they are executed while avoiding editor/tooling errors.
It might also help avoiding confusion for new developers that see an un-used parameter in your code and could think it's unnecessary.
## Dependencies errors and return values
You can use the same dependency *functions* you use normally.
### Dependency requirements
They can declare request requirements (like headers) or other sub-dependencies:
```Python hl_lines="6 11"
{!./src/dependencies/tutorial006.py!}
```
### Raise exceptions
These dependencies can `raise` exceptions, the same as normal dependencies:
```Python hl_lines="8 13"
{!./src/dependencies/tutorial006.py!}
```
### Return values
And they can return values or not, the values won't be used.
So, you can re-use a normal dependency (that returns a value) you already use somewhere else, and even though the value won't be used, the dependency will be executed:
```Python hl_lines="9 14"
{!./src/dependencies/tutorial006.py!}
```
## Dependencies for a group of *path operations*
Later, when reading about how to <a href="https://fastapi.tiangolo.com/tutorial/bigger-applications/" target="_blank">structure bigger applications</a>, possibly with multiple files, you will learn how to declare a single `dependencies` parameter for a group of *path operations*.

4
docs/tutorial/security/oauth2-scopes.md

@ -244,3 +244,7 @@ The most secure is the code flow, but is more complex to implement as it require
But in the end, they are implementing the same OAuth2 standard.
**FastAPI** includes utilities for all these OAuth2 authentication flows in `fastapi.security.oauth2`.
## `Security` in decorator `dependencies`
The same way you can define a `list` of <a href="https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-decorator/" target="_blank">`Depends` in the decorator's `dependencies` parameter</a>, you could also use `Security` with `scopes` there.

28
fastapi/applications.py

@ -3,6 +3,7 @@ from typing import Any, Callable, Dict, List, Optional, Type, Union
from fastapi import routing
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends
from pydantic import BaseModel
from starlette.applications import Starlette
from starlette.exceptions import ExceptionMiddleware, HTTPException
@ -111,6 +112,7 @@ class FastAPI(Starlette):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -128,6 +130,7 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -147,6 +150,7 @@ class FastAPI(Starlette):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -165,6 +169,7 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -186,10 +191,15 @@ class FastAPI(Starlette):
*,
prefix: str = "",
tags: List[str] = None,
dependencies: List[Depends] = None,
responses: Dict[Union[int, str], Dict[str, Any]] = None,
) -> None:
self.router.include_router(
router, prefix=prefix, tags=tags, responses=responses or {}
router,
prefix=prefix,
tags=tags,
dependencies=dependencies,
responses=responses or {},
)
def get(
@ -199,6 +209,7 @@ class FastAPI(Starlette):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -214,6 +225,7 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -232,6 +244,7 @@ class FastAPI(Starlette):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -247,6 +260,7 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -265,6 +279,7 @@ class FastAPI(Starlette):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -280,6 +295,7 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -298,6 +314,7 @@ class FastAPI(Starlette):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -313,6 +330,7 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -331,6 +349,7 @@ class FastAPI(Starlette):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -346,6 +365,7 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -364,6 +384,7 @@ class FastAPI(Starlette):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -379,6 +400,7 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -397,6 +419,7 @@ class FastAPI(Starlette):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -412,6 +435,7 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -430,6 +454,7 @@ class FastAPI(Starlette):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -445,6 +470,7 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,

34
fastapi/dependencies/utils.py

@ -52,7 +52,7 @@ sequence_types = (list, set, tuple)
sequence_shape_to_type = {Shape.LIST: list, Shape.SET: set, Shape.TUPLE: tuple}
def get_sub_dependant(
def get_param_sub_dependant(
*, param: inspect.Parameter, path: str, security_scopes: List[str] = None
) -> Dependant:
depends: params.Depends = param.default
@ -60,6 +60,30 @@ def get_sub_dependant(
dependency = depends.dependency
else:
dependency = param.annotation
return get_sub_dependant(
depends=depends,
dependency=dependency,
path=path,
name=param.name,
security_scopes=security_scopes,
)
def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> Dependant:
assert callable(
depends.dependency
), "A parameter-less dependency must have a callable dependency"
return get_sub_dependant(depends=depends, dependency=depends.dependency, path=path)
def get_sub_dependant(
*,
depends: params.Depends,
dependency: Callable,
path: str,
name: str = None,
security_scopes: List[str] = None,
) -> Dependant:
security_requirement = None
security_scopes = security_scopes or []
if isinstance(depends, params.Security):
@ -73,7 +97,7 @@ def get_sub_dependant(
security_scheme=dependency, scopes=use_scopes
)
sub_dependant = get_dependant(
path=path, call=dependency, name=param.name, security_scopes=security_scopes
path=path, call=dependency, name=name, security_scopes=security_scopes
)
if security_requirement:
sub_dependant.security_requirements.append(security_requirement)
@ -111,7 +135,7 @@ def get_dependant(
for param_name in signature_params:
param = signature_params[param_name]
if isinstance(param.default, params.Depends):
sub_dependant = get_sub_dependant(
sub_dependant = get_param_sub_dependant(
param=param, path=path, security_scopes=security_scopes
)
dependant.dependencies.append(sub_dependant)
@ -277,8 +301,8 @@ async def solve_dependencies(
solved = await sub_dependant.call(**sub_values)
else:
solved = await run_in_threadpool(sub_dependant.call, **sub_values)
assert sub_dependant.name is not None, "Subdependants always have a name"
values[sub_dependant.name] = solved
if sub_dependant.name is not None:
values[sub_dependant.name] = solved
path_values, path_errors = request_params_to_args(
dependant.path_params, request.path_params
)

35
fastapi/routing.py

@ -5,7 +5,12 @@ from typing import Any, Callable, Dict, List, Optional, Type, Union
from fastapi import params
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import get_body_field, get_dependant, solve_dependencies
from fastapi.dependencies.utils import (
get_body_field,
get_dependant,
get_parameterless_sub_dependant,
solve_dependencies,
)
from fastapi.encoders import jsonable_encoder
from pydantic import BaseConfig, BaseModel, Schema
from pydantic.error_wrappers import ErrorWrapper, ValidationError
@ -101,6 +106,7 @@ class APIRoute(routing.Route):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -135,6 +141,7 @@ class APIRoute(routing.Route):
self.response_field = None
self.status_code = status_code
self.tags = tags or []
self.dependencies = dependencies or []
self.summary = summary
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
self.response_description = response_description
@ -175,6 +182,10 @@ class APIRoute(routing.Route):
endpoint
), f"An endpoint must be a function or method"
self.dependant = get_dependant(path=path, call=self.endpoint)
for depends in self.dependencies[::-1]:
self.dependant.dependencies.insert(
0, get_parameterless_sub_dependant(depends=depends, path=path)
)
self.body_field = get_body_field(dependant=self.dependant, name=self.name)
self.app = request_response(
get_app(
@ -196,6 +207,7 @@ class APIRouter(routing.Router):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -213,6 +225,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -233,6 +246,7 @@ class APIRouter(routing.Router):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -251,6 +265,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -272,6 +287,7 @@ class APIRouter(routing.Router):
*,
prefix: str = "",
tags: List[str] = None,
dependencies: List[params.Depends] = None,
responses: Dict[Union[int, str], Dict[str, Any]] = None,
) -> None:
if prefix:
@ -290,6 +306,7 @@ class APIRouter(routing.Router):
response_model=route.response_model,
status_code=route.status_code,
tags=(route.tags or []) + (tags or []),
dependencies=(dependencies or []) + (route.dependencies or []),
summary=route.summary,
description=route.description,
response_description=route.response_description,
@ -321,6 +338,7 @@ class APIRouter(routing.Router):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -336,6 +354,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -355,6 +374,7 @@ class APIRouter(routing.Router):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -370,6 +390,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -389,6 +410,7 @@ class APIRouter(routing.Router):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -404,6 +426,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -423,6 +446,7 @@ class APIRouter(routing.Router):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -438,6 +462,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -457,6 +482,7 @@ class APIRouter(routing.Router):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -472,6 +498,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -491,6 +518,7 @@ class APIRouter(routing.Router):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -506,6 +534,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -525,6 +554,7 @@ class APIRouter(routing.Router):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -540,6 +570,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,
@ -559,6 +590,7 @@ class APIRouter(routing.Router):
response_model: Type[BaseModel] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: List[params.Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@ -574,6 +606,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies or [],
summary=summary,
description=description,
response_description=response_description,

1
mkdocs.yml

@ -54,6 +54,7 @@ nav:
- First Steps: 'tutorial/dependencies/first-steps.md'
- Classes as Dependencies: 'tutorial/dependencies/classes-as-dependencies.md'
- Sub-dependencies: 'tutorial/dependencies/sub-dependencies.md'
- Dependencies in path operation decorators: 'tutorial/dependencies/dependencies-in-path-operation-decorators.md'
- Advanced Dependencies: 'tutorial/dependencies/advanced-dependencies.md'
- Security:
- Security Intro: 'tutorial/security/intro.md'

121
tests/test_tutorial/test_bigger_applications/test_main.py

@ -74,10 +74,28 @@ openapi_schema = {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"tags": ["items"],
"summary": "Read Items",
"operationId": "read_items_items__get",
"parameters": [
{
"required": True,
"schema": {"title": "X-Token", "type": "string"},
"name": "x-token",
"in": "header",
}
],
}
},
"/items/{item_id}": {
@ -108,7 +126,13 @@ openapi_schema = {
"schema": {"title": "Item_Id", "type": "string"},
"name": "item_id",
"in": "path",
}
},
{
"required": True,
"schema": {"title": "X-Token", "type": "string"},
"name": "x-token",
"in": "header",
},
],
},
"put": {
@ -139,7 +163,13 @@ openapi_schema = {
"schema": {"title": "Item_Id", "type": "string"},
"name": "item_id",
"in": "path",
}
},
{
"required": True,
"schema": {"title": "X-Token", "type": "string"},
"name": "x-token",
"in": "header",
},
],
},
},
@ -177,29 +207,94 @@ openapi_schema = {
@pytest.mark.parametrize(
"path,expected_status,expected_response",
"path,expected_status,expected_response,headers",
[
("/users", 200, [{"username": "Foo"}, {"username": "Bar"}]),
("/users/foo", 200, {"username": "foo"}),
("/users/me", 200, {"username": "fakecurrentuser"}),
("/items", 200, [{"name": "Item Foo"}, {"name": "item Bar"}]),
("/items/bar", 200, {"name": "Fake Specific Item", "item_id": "bar"}),
("/openapi.json", 200, openapi_schema),
("/users", 200, [{"username": "Foo"}, {"username": "Bar"}], {}),
("/users/foo", 200, {"username": "foo"}, {}),
("/users/me", 200, {"username": "fakecurrentuser"}, {}),
(
"/items",
200,
[{"name": "Item Foo"}, {"name": "item Bar"}],
{"X-Token": "fake-super-secret-token"},
),
(
"/items/bar",
200,
{"name": "Fake Specific Item", "item_id": "bar"},
{"X-Token": "fake-super-secret-token"},
),
("/items", 400, {"detail": "X-Token header invalid"}, {"X-Token": "invalid"}),
(
"/items/bar",
400,
{"detail": "X-Token header invalid"},
{"X-Token": "invalid"},
),
(
"/items",
422,
{
"detail": [
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
{},
),
(
"/items/bar",
422,
{
"detail": [
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
}
]
},
{},
),
("/openapi.json", 200, openapi_schema, {}),
],
)
def test_get_path(path, expected_status, expected_response):
response = client.get(path)
def test_get_path(path, expected_status, expected_response, headers):
response = client.get(path, headers=headers)
assert response.status_code == expected_status
assert response.json() == expected_response
def test_put():
def test_put_no_header():
response = client.put("/items/foo")
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"loc": ["header", "x-token"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
def test_put_invalid_header():
response = client.put("/items/foo", headers={"X-Token": "invalid"})
assert response.status_code == 400
assert response.json() == {"detail": "X-Token header invalid"}
def test_put():
response = client.put("/items/foo", headers={"X-Token": "fake-super-secret-token"})
assert response.status_code == 200
assert response.json() == {"item_id": "foo", "name": "The Fighters"}
def test_put_forbidden():
response = client.put("/items/bar")
response = client.put("/items/bar", headers={"X-Token": "fake-super-secret-token"})
assert response.status_code == 403
assert response.json() == {"detail": "You can only update the item: foo"}

128
tests/test_tutorial/test_dependencies/test_tutorial006.py

@ -0,0 +1,128 @@
from starlette.testclient import TestClient
from dependencies.tutorial006 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"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",
},
],
}
}
},
"components": {
"schemas": {
"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"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_get_no_headers():
response = client.get("/items/")
assert response.status_code == 422
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():
response = client.get("/items/", headers={"X-Token": "invalid"})
assert response.status_code == 400
assert response.json() == {"detail": "X-Token header invalid"}
def test_get_invalid_second_header():
response = client.get(
"/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"}
)
assert response.status_code == 400
assert response.json() == {"detail": "X-Key header invalid"}
def test_get_valid_headers():
response = client.get(
"/items/",
headers={
"X-Token": "fake-super-secret-token",
"X-Key": "fake-super-secret-key",
},
)
assert response.status_code == 200
assert response.json() == [{"item": "Foo"}, {"item": "Bar"}]

2
tests/test_tutorial/test_request_files/test_tutorial002.py

@ -166,8 +166,6 @@ def test_post_form_no_body():
def test_post_body_json():
response = client.post("/files/", json={"file": "Foo"})
print(response)
print(response.content)
assert response.status_code == 422
assert response.json() == file_required

2
tests/test_tutorial/test_security/test_tutorial005.py

@ -227,7 +227,6 @@ def test_token():
response = client.get(
"/users/me", headers={"Authorization": f"Bearer {access_token}"}
)
print(response.json())
assert response.status_code == 200
assert response.json() == {
"username": "johndoe",
@ -319,7 +318,6 @@ def test_token_inactive_user():
response = client.get(
"/users/me", headers={"Authorization": f"Bearer {access_token}"}
)
print(response.json())
assert response.status_code == 400
assert response.json() == {"detail": "Inactive user"}

Loading…
Cancel
Save