diff --git a/docs/release-notes.md b/docs/release-notes.md index 180f8c6d5..eb8506069 100644 --- a/docs/release-notes.md +++ b/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). diff --git a/docs/src/bigger_applications/app/main.py b/docs/src/bigger_applications/app/main.py index 2cebd4244..fdff13947 100644 --- a/docs/src/bigger_applications/app/main.py +++ b/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"}}, ) diff --git a/docs/src/dependencies/tutorial006.py b/docs/src/dependencies/tutorial006.py index 5d22f6823..a71d7cce6 100644 --- a/docs/src/dependencies/tutorial006.py +++ b/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"}] diff --git a/docs/src/dependencies/tutorial007.py b/docs/src/dependencies/tutorial007.py new file mode 100644 index 000000000..5d22f6823 --- /dev/null +++ b/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} diff --git a/docs/tutorial/bigger-applications.md b/docs/tutorial/bigger-applications.md index 7adbab77c..f9e2f6aa7 100644 --- a/docs/tutorial/bigger-applications.md +++ b/docs/tutorial/bigger-applications.md @@ -22,16 +22,15 @@ Let's say you have a file structure like this: !!! tip There are two `__init__.py` files: one in each directory or subdirectory. - + This is what allows importing code from one file into another. For example, in `app/main.py` you could have a line like: - + ``` 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!} ``` @@ -221,13 +219,12 @@ It will include all the routes from that router as part of it. !!! check You don't have to worry about performance when including routers. - + 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`, 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,27 +261,28 @@ 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"`. +* 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, and then the normal parameter dependencies. + * You can also add `Security` dependencies with `scopes`. -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). - -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(...)`. - + Apart from `app.include_router()`, in the same **FastAPI** app. - - It would still work the same. + It would still work the same. !!! info "Very Technical Details" **Note**: this is a very technical detail that you probably can **just skip**. diff --git a/docs/tutorial/dependencies/advanced-dependencies.md b/docs/tutorial/dependencies/advanced-dependencies.md index 824006441..903090f23 100644 --- a/docs/tutorial/dependencies/advanced-dependencies.md +++ b/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 diff --git a/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md new file mode 100644 index 000000000..477bb3d1e --- /dev/null +++ b/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 structure bigger applications, possibly with multiple files, you will learn how to declare a single `dependencies` parameter for a group of *path operations*. diff --git a/docs/tutorial/security/oauth2-scopes.md b/docs/tutorial/security/oauth2-scopes.md index ef4f6798e..89c973e8f 100644 --- a/docs/tutorial/security/oauth2-scopes.md +++ b/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 `Depends` in the decorator's `dependencies` parameter, you could also use `Security` with `scopes` there. diff --git a/fastapi/applications.py b/fastapi/applications.py index 076ebfcc6..e4b9ab967 100644 --- a/fastapi/applications.py +++ b/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, diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index a87f23c69..0530fd209 100644 --- a/fastapi/dependencies/utils.py +++ b/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 ) diff --git a/fastapi/routing.py b/fastapi/routing.py index ac8192baf..ef8d9bed2 100644 --- a/fastapi/routing.py +++ b/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, diff --git a/mkdocs.yml b/mkdocs.yml index d5b81d543..8fa41d012 100644 --- a/mkdocs.yml +++ b/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' diff --git a/tests/test_tutorial/test_bigger_applications/test_main.py b/tests/test_tutorial/test_bigger_applications/test_main.py index db094df7d..d0eff5b65 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main.py +++ b/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"} diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006.py b/tests/test_tutorial/test_dependencies/test_tutorial006.py new file mode 100644 index 000000000..fdc878977 --- /dev/null +++ b/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"}] diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index e6b7ba479..15ea952ba 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/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 diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index 96f9fa9c1..403130e49 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/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"}