Compare commits

..

12 Commits

Author SHA1 Message Date
Sebastián Ramírez
e5b341c7dd 🔖 Bump version after fix for constrained bytes 2019-01-05 17:38:59 +04:00
Sebastián Ramírez
577c5a84db 🐛 Fix constrained bytes, from defaults in Pydantic
#2
2019-01-05 17:30:27 +04:00
Sebastián Ramírez
a5cfee434d 📝 Update docs for dependencies 2019-01-05 17:19:41 +04:00
Sebastián Ramírez
9a8349bf96 📝 Improve explanation of dependencies 2019-01-01 19:27:02 +04:00
Sebastián Ramírez
a59408f68c ✏️ Fix typo in dependencies docs 2018-12-30 22:18:45 +04:00
Sebastián Ramírez
3c08b05ea6 🔖 Bump version, after query and header as lists
and bug fixes for Python 3.7
2018-12-30 21:46:49 +04:00
Sebastián Ramírez
60599bad99 🐛 Fix Python 3.7 specific list query handling 2018-12-30 21:43:34 +04:00
Sebastián Ramírez
ccf30b5c2e 📝 Update docs, 100% coverage 2018-12-30 00:17:22 +04:00
Sebastián Ramírez
ca0652aebf 🐛 Fix type checks for Python 3.7 2018-12-30 00:14:39 +04:00
Sebastián Ramírez
be957e7c99 Allow lists of query or header params
and add tests for them
2018-12-30 00:07:31 +04:00
Sebastián Ramírez
90af868146 Add security checks for HTTP utils
and tests for them
2018-12-29 23:04:54 +04:00
Sebastián Ramírez
660f917d79 ✏️ Fix typos and docs notes 2018-12-29 18:43:58 +04:00
35 changed files with 951 additions and 197 deletions

View File

@@ -153,7 +153,7 @@ Any integration is designed to be so simple to use (with dependencies) that you
### Tested
* 100% <abbr title="The amount of code that is automatically tested">test coverage</abbr> (* not yet, in a couple days).
* 100% <abbr title="The amount of code that is automatically tested">test coverage</abbr>.
* 100% <abbr title="Python type annotations, with this your editor and external tools can give you better support">type annotated</abbr> code base.
* Used in production applications.

View File

@@ -10,3 +10,8 @@ async def common_parameters(q: str = None, skip: int = 0, limit: int = 100):
@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
return commons
@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
return commons

View File

@@ -33,7 +33,7 @@ But you can also declare multiple body parameters, e.g. `item` and `user`:
{!./src/body_multiple_params/tutorial002.py!}
```
In this case, **FastAPI** will notice that there are more than one body parameter in the function (two parameters that are Pydantic models).
In this case, **FastAPI** will notice that there are more than one body parameters in the function (two parameters that are Pydantic models).
So, it will then use the parameter names as keys (field names) in the body, and expect a body like:

View File

@@ -9,7 +9,7 @@ First, you have to import it:
```
!!! warning
Notice that `Schema` is imported directly from `pydantic`, not form `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc).
Notice that `Schema` is imported directly from `pydantic`, not from `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc).
## Declare model attributes
@@ -37,7 +37,7 @@ In `Schema`, `Path`, `Query`, `Body` and others you'll see later, you can declar
Those parameters will be added as-is to the output JSON Schema.
If you know JSON Schema and want to add extra information appart from what we have discussed here, you can pass that as extra keyword arguments.
If you know JSON Schema and want to add extra information apart from what we have discussed here, you can pass that as extra keyword arguments.
!!! warning
Have in mind that extra parameters passed won't add any validation, only annotation, for documentation purposes.

View File

@@ -86,7 +86,7 @@ And will be also used in the API docs inside each path operation that needs them
## Editor support
In your editor, inside your function you will get type hints and completion everywhere (this wouldn't happen if your received a `dict` instead of a Pydantic model):
In your editor, inside your function you will get type hints and completion everywhere (this wouldn't happen if you received a `dict` instead of a Pydantic model):
<img src="/img/tutorial/body/image03.png">

View File

@@ -1,4 +1,4 @@
You can define Cookie parameters the same way you define `Query` and `Path` parameteres.
You can define Cookie parameters the same way you define `Query` and `Path` parameters.
## Import `Cookie`
@@ -8,11 +8,11 @@ First import `Cookie`:
{!./src/cookie_params/tutorial001.py!}
```
## Declare `Cookie` parameteres
## Declare `Cookie` parameters
Then declare the cookie parameters using the same structure as with `Path` and `Query`.
The first value is the default value, you can pass all the extra validation or annotation parameteres:
The first value is the default value, you can pass all the extra validation or annotation parameters:
```Python hl_lines="7"
{!./src/cookie_params/tutorial001.py!}
@@ -22,7 +22,7 @@ The first value is the default value, you can pass all the extra validation or a
`Cookie` is a "sister" class of `Path` and `Query`. It also inherits from the same common `Param` class.
!!! info
To declare cookies, you need to use `Cookie`, because otherwise the parameters would be interpreted as query parameteres.
To declare cookies, you need to use `Cookie`, because otherwise the parameters would be interpreted as query parameters.
## Recap

View File

@@ -63,7 +63,7 @@ Pass `HTMLResponse` as the parameter `content_type` of your path operation:
And it will be documented as such in OpenAPI.
### return a Starlette `Response`
### Return a Starlette `Response`
You can also override the response directly in your path operation.

View File

@@ -25,6 +25,8 @@ To do that, we declare a method `__call__`:
{!./src/dependencies/tutorial006.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.
## Parameterize the instance
And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency:

View File

@@ -10,7 +10,7 @@ In the previous example, we where returning a `dict` from our dependency ("depen
But then we get a `dict` in the parameter `commons` of the path operation function.
And we know that `dict`s can't provide a lot of editor support because they can't know their keys and value types.
And we know that editors can't provide a lot of support (like completion) for `dict`s, because they can't know their keys and value types.
We can do better...
@@ -24,7 +24,7 @@ The key factor is that a dependency should be a "callable".
A "**callable**" in Python is anything that Python can "call" like a function.
So, if you have an object `something` (that might _not_ be a function) and you can do:
So, if you have an object `something` (that might _not_ be a function) and you can "call" it (execute it) like:
```Python
something()
@@ -42,6 +42,21 @@ then it is a "callable".
You might notice that to create an instance of a Python class, you use that same syntax.
For example:
```Python
class Cat:
def __init__(self, name: str):
self.name = name
fluffy = Cat(name="Mr Fluffy")
```
In this case, `fluffy` is an instance of the class `Cat`.
And to create `fluffy`, you are "calling" `Cat`.
So, a Python class is also a **callable**.
Then, in **FastAPI**, you could use a Python class as a dependency.
@@ -50,7 +65,7 @@ What FastAPI actually checks is that it is a "callable" (function, class or anyt
If you pass a "callable" as a dependency in **FastAPI**, it will analyze the parameters for that "callable", and process them in the same way as the parameters for a path operation function. Including sub-dependencies.
That also applies to callables with no parameters at all. The same as would be for path operation functions with no parameteres.
That also applies to callables with no parameters at all. The same as it would be for path operation functions with no parameters.
Then, we can change the dependency "dependable" `common_parameters` from above to the class `CommonQueryParameters`:

View File

@@ -1,91 +0,0 @@
Let's see a very simple example of the **Dependency Injection** system.
It will be so simple that it is not very useful, for now.
But this way we can focus on how the **Dependency Injection** system works.
In the next chapters we'll extend it to see how can it be so useful.
## Create a dependency, or "dependable"
Let's first focus on the dependency.
It is just a function that can take all the same parameters that a path operation function can take:
```Python hl_lines="6 7"
{!./src/dependencies/tutorial001.py!}
```
That's it.
**2 lines**.
And it has the same shape and structure that all your path operation functions.
You can think of it as a path operation function without the "decorator" (without the `@app.get("/some-path")`).
And it can return anything you want.
In this case, this dependency expects:
* An optional query parameter `q` that is a `str`.
* An optional query parameter `skip` that is an `int`, and by default is `0`.
* An optional query parameter `limit` that is an `int`, and by default is `100`.
And then it just returns a `dict` containing those values.
## Import `Depends`
```Python hl_lines="1"
{!./src/dependencies/tutorial001.py!}
```
## Declare the dependency, in the "dependant"
The same way you use `Body`, `Query`, etc. with your path operation function parameters, use `Depends` with a new parameter:
```Python hl_lines="11"
{!./src/dependencies/tutorial001.py!}
```
Although you use it in the parameters of your function too, `Depends` works a bit differently.
You only give `Depends` a single parameter.
This parameter must be a function with the same parameters that can be taken by a path operation function.
Whenever a new request arrives, **FastAPI** will take care of:
* Calling your dependency ("dependable") function with the correct parameters.
* Get the result from your function.
* Assign that result to the parameter in your path operation function.
!!! note
Notice that you don't have to create a special class and pass it somewhere to **FastAPI** or anything similar.
You just pass it to `Depends` and **FastAPI** knows how to do the rest.
## To `async` or not to `async`
As dependencies will also be called by **FastAPI** (the same as your path operation functions), the same rules apply while defining your functions.
You can use `async def` or normal `def`.
And you can declare dependencies with `async def` inside of normal `def` path operation functions, or `def` dependencies inside of `async def` path operation functions.
It doesn't matter. **FastAPI** will know what to do.
!!! note
If you don't know, check the _"In a hurry?"_ section about <a href="https://fastapi.tiangolo.com/async/#in-a-hurry" target="_blank">`async` and `await` in the docs</a>.
## Integrated wiht OpenAPI
All the request declarations, validations and requirements of your dependencies (and sub-dependencies) will be integrated in the same OpenAPI schema.
So, the interactive docs will have all the information they need, while you keep all the flexibility of the dependencies:
<img src="/img/tutorial/dependencies/image01.png">
## Recap
Create Dependencies with **2 lines** of code.

View File

@@ -0,0 +1,167 @@
**FastAPI** has a very powerful but intuitive **<abbr title="also known as components, resources, providers, services, injectables">Dependency Injection</abbr>** system.
It is designed to be very simple to use, and to make it very easy for any developer to integrate other components with **FastAPI**.
## "Dependency Injection"?
**"Dependency Injection"** means, in programming, that there is a way for your code (in this case, your path operation functions) to declare things that it requires to work and use: "dependencies".
And then, that system (in this case **FastAPI**) will take care of doing whatever is needed to provide your code with those needed dependencies ("inject" the dependencies).
This is very useful when you need to:
* Have shared logic (the same code logic again and again).
* Share database connections.
* Enforce security, authentication, role requirements, etc.
* etc.
All these, while minimizing code repetition.
## First Steps
Let's see a very simple example. It will be so simple that it is not very useful, for now.
But this way we can focus on how the **Dependency Injection** system works.
### Create a dependency, or "dependable"
Let's first focus on the dependency.
It is just a function that can take all the same parameters that a path operation function can take:
```Python hl_lines="6 7"
{!./src/dependencies/tutorial001.py!}
```
That's it.
**2 lines**.
And it has the same shape and structure that all your path operation functions.
You can think of it as a path operation function without the "decorator" (without the `@app.get("/some-path")`).
And it can return anything you want.
In this case, this dependency expects:
* An optional query parameter `q` that is a `str`.
* An optional query parameter `skip` that is an `int`, and by default is `0`.
* An optional query parameter `limit` that is an `int`, and by default is `100`.
And then it just returns a `dict` containing those values.
### Import `Depends`
```Python hl_lines="1"
{!./src/dependencies/tutorial001.py!}
```
### Declare the dependency, in the "dependant"
The same way you use `Body`, `Query`, etc. with your path operation function parameters, use `Depends` with a new parameter:
```Python hl_lines="11 16"
{!./src/dependencies/tutorial001.py!}
```
Although you use `Depends` in the parameters of your function the same way you use `Body`, `Query`, etc, `Depends` works a bit differently.
You only give `Depends` a single parameter.
This parameter must be something like a function.
And that function takes parameters in the same way that path operation functions do.
!!! tip
You'll see what other "things", apart from functions, can be used as dependencies in the next chapter.
Whenever a new request arrives, **FastAPI** will take care of:
* Calling your dependency ("dependable") function with the correct parameters.
* Get the result from your function.
* Assign that result to the parameter in your path operation function.
!!! note
Notice that you don't have to create a special class and pass it somewhere to **FastAPI** to "register" it or anything similar.
You just pass it to `Depends` and **FastAPI** knows how to do the rest.
## To `async` or not to `async`
As dependencies will also be called by **FastAPI** (the same as your path operation functions), the same rules apply while defining your functions.
You can use `async def` or normal `def`.
And you can declare dependencies with `async def` inside of normal `def` path operation functions, or `def` dependencies inside of `async def` path operation functions, etc.
It doesn't matter. **FastAPI** will know what to do.
!!! note
If you don't know, check the _"In a hurry?"_ section about <a href="https://fastapi.tiangolo.com/async/#in-a-hurry" target="_blank">`async` and `await` in the docs</a>.
## Integrated with OpenAPI
All the request declarations, validations and requirements of your dependencies (and sub-dependencies) will be integrated in the same OpenAPI schema.
So, the interactive docs will have all the information from these dependencies too:
<img src="/img/tutorial/dependencies/image01.png">
## Simple usage
If you look at it, *path operation functions* are declared to be used whenever a *path* and *operation* matches, and then **FastAPI** takes care of calling the function with the correct parameters and use the response.
Actually, all (or most) of the web frameworks work in this same way.
You never call those functions directly. They are called by your framework (in this case, **FastAPI**).
With the Dependency Injection system, you can also tell **FastAPI** that your path operation function also "depends" on something else that should be executed before your *path operation function*, and **FastAPI** will take care of executing it and "injecting" the results.
Other common terms for this same idea of "dependency injection" are:
* resources
* providers
* services
* injectables
* components
## **FastAPI** plug-ins
Integrations and "plug-in"s can be built using the **Dependency Injection** system. But in fact, there is actually **no need to create "plug-ins"**, as by using dependencies it's possible to declare an infinite number of integrations and interactions that become available to your path operation functions.
And dependencies can be created in a very simple and intuitive way that allow you to just import the Python packages you need, and integrate them with your API functions in a couple of lines of code, _literally_.
You will see examples of this in the next chapters, about relational and NoSQL databases, security, etc.
## **FastAPI** compatibility
The simplicity of the dependency injection system makes **FastAPI** compatible with:
* all the relational databases
* NoSQL databases
* external packages
* external APIs
* authentication and authorization systems
* API usage monitoring systems
* response data injection systems
* etc.
## Simple and Powerful
Although the hierarchical dependency injection system is very simple to define and use, it's still very powerful.
You can define dependencies that in turn can define dependencies themselves.
In the end, a hierarchical tree of dependencies is built, and the **Dependency Injection** system takes care of solving all these dependencies for you (and your dependencies) and providing (injecting) the results at each step.
## Integrated with **OpenAPI**
All these dependencies, while declaring their requirements, add parameters, validations, etc. to your path operations.
**FastAPI** will take care of adding it all to the OpenAPI schema, so that it is shown in the interactive documentation systems.

View File

@@ -1,58 +0,0 @@
**FastAPI** has a very powerful but intuitive **<abbr title="also known as components, resources, providers, services, injectables">Dependency Injection</abbr>** system.
It is designed to be very simple to use, and to make it very easy for any developer to integrate other components with **FastAPI**.
## "Dependency Injection"?
**"Dependency Injection"** means, in programming, that there is a way for your code (in this case, your path operation functions) to declare things that it requires to work and use.
And then, that system (in this case **FastAPI**) will take care of doing whatever is needed to provide your code with that thing that it needs.
If you look at it, path operation functions are declared to be used whenever a path and operation matches, and then **FastAPI** will take care of calling the function with the correct parameters and use the response.
Actually, all (or most) of the web frameworks work in this same way.
You never call those functions directly. The are called by your framework (in this case, **FastAPI**).
With the Dependency Injection system, you can also tell **FastAPI** that your path operation function also "depends" on something else that should be executed before your path operation function, and **FastAPI** will take care of executing it and "injecting" the results.
Other common terms for this same idea are:
* resources
* providers
* services
* injectables
## **FastAPI** plug-ins
Integrations and "plug-in"s can be built using the **Dependency Injection** system. But in fact, there is actually **no need to create "plug-ins"**, as by using dependencies it's possible to declare an infinite number of integrations and interactions that become available to your path operation functions.
And dependencies can be created in a very simple and intuitive way that allow you to just import the Python packages you need, and integrate them with your API functions in a couple of lines of code, _literally_.
## **FastAPI** compatibility
The simplicity of the dependency injection system makes **FastAPI** compatible with:
* all the relational databases
* NoSQL databases
* external packages
* external APIs
* authentication and authorization systems
* API usage monitoring systems
* response data injection systems
* etc.
## Simple and Powerful
Although the hierarchical dependency injection system is very simple to define and use, it's still very powerful.
You can define dependencies that in turn can define dependencies themselves.
In the end, a hierarchical tree of dependencies is built, and the **Dependency Injection** system takes care of solving all these dependencies for you (and your dependencies) and providing the results at each step.
## Integrated with **OpenAPI**
All these dependencies, while declaring their requirements, might have been adding parameters, validations, etc. to your path operations.
**FastAPI** will take care of adding it all to the OpenAPI schema, so that it is shown in the interactive documentation systems.

View File

@@ -1,4 +1,4 @@
You can define Header parameters the same way you define `Query`, `Path` and `Cookie` parameteres.
You can define Header parameters the same way you define `Query`, `Path` and `Cookie` parameters.
## Import `Header`
@@ -8,11 +8,11 @@ First import `Header`:
{!./src/header_params/tutorial001.py!}
```
## Declare `Header` parameteres
## Declare `Header` parameters
Then declare the header parameters using the same structure as with `Path`, `Query` and `Cookie`.
The first value is the default value, you can pass all the extra validation or annotation parameteres:
The first value is the default value, you can pass all the extra validation or annotation parameters:
```Python hl_lines="7"
{!./src/header_params/tutorial001.py!}
@@ -22,7 +22,7 @@ The first value is the default value, you can pass all the extra validation or a
`Header` is a "sister" class of `Path`, `Query` and `Cookie`. It also inherits from the same common `Param` class.
!!! info
To declare headers, you need to use `Header`, because otherwise the parameters would be interpreted as query parameteres.
To declare headers, you need to use `Header`, because otherwise the parameters would be interpreted as query parameters.
## Automatic conversion
@@ -49,6 +49,6 @@ If for some reason you need to disable automatic conversion of underscores to hy
## Recap
Declare headeres with `Header`, using the same common pattern as `Query`, `Path` and `Cookie`.
Declare headers with `Header`, using the same common pattern as `Query`, `Path` and `Cookie`.
And don't worry about underscores in your variables, **FastAPI** will take care of converting them.

View File

@@ -130,6 +130,11 @@ You can add more information about the parameter.
That information will be included in the generated OpenAPI and used by the documentation user interfaces and external tools.
!!! note
Have in mind that different tools might have different levels of OpenAPI support.
Some of them might not show all the extra information declared yet, although in most of the cases, the missing feature is already planned for development.
You can add a `title`:
```Python hl_lines="7"

View File

@@ -129,7 +129,7 @@ When you declare a default value for non-path parameters (for now, we have only
If you don't want to add a specific value but just make it optional, set the default as `None`.
But when you want to make a query parameter required, you can just do not declare any default value:
But when you want to make a query parameter required, you can just not declare any default value:
```Python hl_lines="6 7"
{!./src/query_params/tutorial005.py!}

View File

@@ -22,7 +22,7 @@ The files will be uploaded as form data and you will receive the contents as `by
`File` is a class that inherits directly from `Form`.
!!! info
To declare File bodies, you need to use `File`, because otherwise the parameters would be interpreted as query parameteres or body (JSON) parameters.
To declare File bodies, you need to use `File`, because otherwise the parameters would be interpreted as query parameters or body (JSON) parameters.
## "Form Data"?

View File

@@ -26,7 +26,7 @@ With `Form` you can declare the same metadata and validation as with `Body` (and
`Form` is a class that inherits directly from `Body`.
!!! info
To declare form bodies, you need to use `Form` explicitly, because without it the parameters would be interpreted as query parameteres or body (JSON) parameters.
To declare form bodies, you need to use `Form` explicitly, because without it the parameters would be interpreted as query parameters or body (JSON) parameters.
## "Form Fields"?

View File

@@ -42,7 +42,7 @@ Now, whenever a browser is creating a user with a password, the API will return
In this case, it might not be a problem, becase the user himself is sending the password.
But if we use sthe same model for another path operation, we could be sending the passwords of our users to every client.
But if we use the same model for another path operation, we could be sending the passwords of our users to every client.
!!! danger
Never send the plain password of a user in a response.

View File

@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.1.15"
__version__ = "0.1.17"
from .applications import FastAPI
from .routing import APIRouter

View File

@@ -3,21 +3,21 @@ import inspect
from copy import deepcopy
from datetime import date, datetime, time, timedelta
from decimal import Decimal
from typing import Any, Callable, Dict, List, Mapping, Sequence, Tuple, Type
from typing import Any, Callable, Dict, List, Mapping, Sequence, Tuple, Type, Union
from uuid import UUID
from fastapi import params
from fastapi.dependencies.models import Dependant, SecurityRequirement
from fastapi.security.base import SecurityBase
from fastapi.utils import get_path_param_names
from pydantic import BaseConfig, Schema, create_model
from fastapi.utils import UnconstrainedConfig, get_path_param_names
from pydantic import Schema, create_model
from pydantic.error_wrappers import ErrorWrapper
from pydantic.errors import MissingError
from pydantic.fields import Field, Required
from pydantic.fields import Field, Required, Shape
from pydantic.schema import get_annotation_from_schema
from pydantic.utils import lenient_issubclass
from starlette.concurrency import run_in_threadpool
from starlette.requests import Request
from starlette.requests import Headers, QueryParams, Request
param_supported_types = (
str,
@@ -107,9 +107,18 @@ def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant:
)
elif isinstance(param.default, params.Param):
if param.annotation != param.empty:
assert lenient_issubclass(
param.annotation, param_supported_types
), f"Parameters for Path, Query, Header and Cookies must be of type str, int, float or bool: {param}"
origin = getattr(param.annotation, "__origin__", None)
param_all_types = param_supported_types + (list, tuple, set)
if isinstance(param.default, (params.Query, params.Header)):
assert lenient_issubclass(
param.annotation, param_all_types
) or lenient_issubclass(
origin, param_all_types
), f"Parameters for Query and Header must be of type str, int, float, bool, list, tuple or set: {param}"
else:
assert lenient_issubclass(
param.annotation, param_supported_types
), f"Parameters for Path and Cookies must be of type str, int, float, bool: {param}"
add_param_to_fields(
param=param, dependant=dependant, default_schema=params.Query
)
@@ -154,7 +163,7 @@ def add_param_to_fields(
default=None if required else default_value,
alias=alias,
required=required,
model_config=BaseConfig(),
model_config=UnconstrainedConfig,
class_validators=[],
schema=schema,
)
@@ -188,7 +197,7 @@ def add_param_to_body_fields(*, param: inspect.Parameter, dependant: Dependant)
default=None if required else default_value,
alias=schema.alias or param.name,
required=required,
model_config=BaseConfig,
model_config=UnconstrainedConfig,
class_validators=[],
schema=schema,
)
@@ -252,12 +261,18 @@ async def solve_dependencies(
def request_params_to_args(
required_params: Sequence[Field], received_params: Mapping[str, Any]
required_params: Sequence[Field],
received_params: Union[Mapping[str, Any], QueryParams, Headers],
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
values = {}
errors = []
for field in required_params:
value = received_params.get(field.alias)
if field.shape in {Shape.LIST, Shape.SET, Shape.TUPLE} and isinstance(
received_params, (QueryParams, Headers)
):
value = received_params.getlist(field.alias)
else:
value = received_params.get(field.alias)
schema: params.Param = field.schema
assert isinstance(schema, params.Param), "Params must be subclasses of Param"
if value is None:
@@ -266,7 +281,7 @@ def request_params_to_args(
ErrorWrapper(
MissingError(),
loc=(schema.in_.value, field.alias),
config=BaseConfig,
config=UnconstrainedConfig,
)
)
else:
@@ -300,7 +315,9 @@ async def request_body_to_args(
if field.required:
errors.append(
ErrorWrapper(
MissingError(), loc=("body", field.alias), config=BaseConfig
MissingError(),
loc=("body", field.alias),
config=UnconstrainedConfig,
)
)
else:
@@ -341,7 +358,7 @@ def get_body_field(*, dependant: Dependant, name: str) -> Field:
type_=BodyModel,
default=None,
required=required,
model_config=BaseConfig,
model_config=UnconstrainedConfig,
class_validators=[],
alias="body",
schema=BodySchema(None),

View File

@@ -322,7 +322,7 @@ class OpenIdConnect(SecurityBase):
openIdConnectUrl: str
SecurityScheme = Union[APIKey, HTTPBase, HTTPBearer, OAuth2, OpenIdConnect]
SecurityScheme = Union[APIKey, HTTPBase, OAuth2, OpenIdConnect, HTTPBearer]
class Components(BaseModel):

View File

@@ -7,7 +7,8 @@ from fastapi import params
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import get_body_field, get_dependant, solve_dependencies
from fastapi.encoders import jsonable_encoder
from pydantic import BaseConfig, BaseModel, Schema
from fastapi.utils import UnconstrainedConfig
from pydantic import BaseModel, Schema
from pydantic.error_wrappers import ErrorWrapper, ValidationError
from pydantic.fields import Field
from pydantic.utils import lenient_issubclass
@@ -130,7 +131,7 @@ class APIRoute(routing.Route):
class_validators=[],
default=None,
required=False,
model_config=BaseConfig(),
model_config=UnconstrainedConfig,
schema=Schema(None),
)
else:

View File

@@ -1,4 +1,10 @@
from .api_key import APIKeyQuery, APIKeyHeader, APIKeyCookie
from .http import HTTPBasic, HTTPBearer, HTTPDigest
from .http import (
HTTPBasic,
HTTPBearer,
HTTPDigest,
HTTPBasicCredentials,
HTTPAuthorizationCredentials,
)
from .oauth2 import OAuth2PasswordRequestForm, OAuth2, OAuth2PasswordBearer
from .open_id_connect_url import OpenIdConnect

View File

@@ -78,6 +78,11 @@ class HTTPBearer(HTTPBase):
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
if scheme.lower() != "bearer":
raise HTTPException(
status_code=HTTP_403_FORBIDDEN,
detail="Invalid authentication credentials",
)
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
@@ -93,4 +98,9 @@ class HTTPDigest(HTTPBase):
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
if scheme.lower() != "digest":
raise HTTPException(
status_code=HTTP_403_FORBIDDEN,
detail="Invalid authentication credentials",
)
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)

View File

@@ -3,12 +3,17 @@ from typing import Any, Dict, List, Sequence, Set, Type
from fastapi import routing
from fastapi.openapi.constants import REF_PREFIX
from pydantic import BaseModel
from pydantic import BaseConfig, BaseModel
from pydantic.fields import Field
from pydantic.schema import get_flat_models_from_fields, model_process_schema
from starlette.routing import BaseRoute
class UnconstrainedConfig(BaseConfig):
min_anystr_length = None
max_anystr_length = None
def get_flat_models_from_routes(
routes: Sequence[Type[BaseRoute]]
) -> Set[Type[BaseModel]]:

View File

@@ -23,8 +23,8 @@ nav:
- Path Parameters: 'tutorial/path-params.md'
- Query Parameters: 'tutorial/query-params.md'
- Request Body: 'tutorial/body.md'
- Query Parameters - String Validations: 'tutorial/query-params-str-validations.md'
- Path Parameters - Numeric Validations: 'tutorial/path-params-numeric-validations.md'
- Query Parameters and String Validations: 'tutorial/query-params-str-validations.md'
- Path Parameters and Numeric Validations: 'tutorial/path-params-numeric-validations.md'
- Body - Multiple Parameters: 'tutorial/body-multiple-params.md'
- Body - Schema: 'tutorial/body-schema.md'
- Body - Nested Models: 'tutorial/body-nested-models.md'
@@ -40,8 +40,7 @@ nav:
- Path Operation Advanced Configuration: 'tutorial/path-operation-advanced-configuration.md'
- Custom Response: 'tutorial/custom-response.md'
- Dependencies:
- Dependencies Intro: 'tutorial/dependencies/intro.md'
- First Steps - Functions: 'tutorial/dependencies/first-steps-functions.md'
- First Steps: 'tutorial/dependencies/first-steps.md'
- Classes as Dependencies: 'tutorial/dependencies/classes-as-dependencies.md'
- Sub-dependencies: 'tutorial/dependencies/sub-dependencies.md'
- Advanced Dependencies: 'tutorial/dependencies/advanced-dependencies.md'

View File

@@ -0,0 +1,143 @@
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.testclient import TestClient
app = FastAPI()
class Item(BaseModel):
name: str
age: int
@app.post("/items/")
def save_item_no_body(item: List[Item]):
return {"item": item}
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/": {
"post": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Save Item No Body Post",
"operationId": "save_item_no_body_items__post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"title": "Item",
"type": "array",
"items": {"$ref": "#/components/schemas/Item"},
}
}
},
"required": True,
},
}
}
},
"components": {
"schemas": {
"Item": {
"title": "Item",
"required": ["name", "age"],
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"age": {"title": "Age", "type": "integer"},
},
},
"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"},
}
},
},
}
},
}
multiple_errors = {
"detail": [
{
"loc": ["body", "item", 0, "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "item", 0, "age"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
{
"loc": ["body", "item", 1, "name"],
"msg": "field required",
"type": "value_error.missing",
},
{
"loc": ["body", "item", 1, "age"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
]
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_put_correct_body():
response = client.post("/items/", json=[{"name": "Foo", "age": 5}])
assert response.status_code == 200
assert response.json() == {"item": [{"name": "Foo", "age": 5}]}
def test_put_incorrect_body():
response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}])
assert response.status_code == 422
assert response.json() == multiple_errors

View File

@@ -0,0 +1,118 @@
from typing import List
from fastapi import FastAPI, Query
from starlette.testclient import TestClient
app = FastAPI()
@app.get("/items/")
def read_items(q: List[int] = Query(None)):
return {"q": q}
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 Get",
"operationId": "read_items_items__get",
"parameters": [
{
"required": False,
"schema": {
"title": "Q",
"type": "array",
"items": {"type": "integer"},
},
"name": "q",
"in": "query",
}
],
}
}
},
"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"},
}
},
},
}
},
}
multiple_errors = {
"detail": [
{
"loc": ["query", "q", 0],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
{
"loc": ["query", "q", 1],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
]
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_multi_query():
response = client.get("/items/?q=5&q=6")
assert response.status_code == 200
assert response.json() == {"q": [5, 6]}
def test_multi_query_incorrect():
response = client.get("/items/?q=five&q=six")
assert response.status_code == 422
assert response.json() == multiple_errors

97
tests/test_put_no_body.py Normal file
View File

@@ -0,0 +1,97 @@
from fastapi import FastAPI
from starlette.testclient import TestClient
app = FastAPI()
@app.put("/items/{item_id}")
def save_item_no_body(item_id: str):
return {"item_id": item_id}
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/{item_id}": {
"put": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Save Item No Body Put",
"operationId": "save_item_no_body_items__item_id__put",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"name": "item_id",
"in": "path",
}
],
}
}
},
"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_put_no_body():
response = client.put("/items/foo")
assert response.status_code == 200
assert response.json() == {"item_id": "foo"}
def test_put_no_body_with_body():
response = client.put("/items/foo", json={"name": "Foo"})
assert response.status_code == 200
assert response.json() == {"item_id": "foo"}

View File

@@ -0,0 +1,56 @@
from fastapi import FastAPI, Security
from fastapi.security.http import HTTPAuthorizationCredentials, HTTPBase
from starlette.testclient import TestClient
app = FastAPI()
security = HTTPBase(scheme="Other")
@app.get("/users/me")
def read_current_user(credentials: HTTPAuthorizationCredentials = Security(security)):
return {"scheme": credentials.scheme, "credentials": credentials.credentials}
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/users/me": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Read Current User Get",
"operationId": "read_current_user_users_me_get",
"security": [{"HTTPBase": []}],
}
}
},
"components": {
"securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_security_http_base():
response = client.get("/users/me", headers={"Authorization": "Other foobar"})
assert response.status_code == 200
assert response.json() == {"scheme": "Other", "credentials": "foobar"}
def test_security_http_base_no_credentials():
response = client.get("/users/me")
assert response.status_code == 403
assert response.json() == {"detail": "Not authenticated"}

View File

@@ -0,0 +1,76 @@
from base64 import b64encode
from fastapi import FastAPI, Security
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from requests.auth import HTTPBasicAuth
from starlette.testclient import TestClient
app = FastAPI()
security = HTTPBasic()
@app.get("/users/me")
def read_current_user(credentials: HTTPBasicCredentials = Security(security)):
return {"username": credentials.username, "password": credentials.password}
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/users/me": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Read Current User Get",
"operationId": "read_current_user_users_me_get",
"security": [{"HTTPBasic": []}],
}
}
},
"components": {
"securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_security_http_basic():
auth = HTTPBasicAuth(username="john", password="secret")
response = client.get("/users/me", auth=auth)
assert response.status_code == 200
assert response.json() == {"username": "john", "password": "secret"}
def test_security_http_basic_no_credentials():
response = client.get("/users/me")
assert response.status_code == 403
assert response.json() == {"detail": "Not authenticated"}
def test_security_http_basic_invalid_credentials():
response = client.get(
"/users/me", headers={"Authorization": "Basic notabase64token"}
)
assert response.status_code == 403
assert response.json() == {"detail": "Invalid authentication credentials"}
def test_security_http_basic_non_basic_credentials():
payload = b64encode(b"johnsecret").decode("ascii")
auth_header = f"Basic {payload}"
response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 403
assert response.json() == {"detail": "Invalid authentication credentials"}

View File

@@ -0,0 +1,62 @@
from fastapi import FastAPI, Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from starlette.testclient import TestClient
app = FastAPI()
security = HTTPBearer()
@app.get("/users/me")
def read_current_user(credentials: HTTPAuthorizationCredentials = Security(security)):
return {"scheme": credentials.scheme, "credentials": credentials.credentials}
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/users/me": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Read Current User Get",
"operationId": "read_current_user_users_me_get",
"security": [{"HTTPBearer": []}],
}
}
},
"components": {
"securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_security_http_bearer():
response = client.get("/users/me", headers={"Authorization": "Bearer foobar"})
assert response.status_code == 200
assert response.json() == {"scheme": "Bearer", "credentials": "foobar"}
def test_security_http_bearer_no_credentials():
response = client.get("/users/me")
assert response.status_code == 403
assert response.json() == {"detail": "Not authenticated"}
def test_security_http_bearer_incorrect_scheme_credentials():
response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
assert response.status_code == 403
assert response.json() == {"detail": "Invalid authentication credentials"}

View File

@@ -0,0 +1,64 @@
from fastapi import FastAPI, Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPDigest
from starlette.testclient import TestClient
app = FastAPI()
security = HTTPDigest()
@app.get("/users/me")
def read_current_user(credentials: HTTPAuthorizationCredentials = Security(security)):
return {"scheme": credentials.scheme, "credentials": credentials.credentials}
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/users/me": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Read Current User Get",
"operationId": "read_current_user_users_me_get",
"security": [{"HTTPDigest": []}],
}
}
},
"components": {
"securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_security_http_digest():
response = client.get("/users/me", headers={"Authorization": "Digest foobar"})
assert response.status_code == 200
assert response.json() == {"scheme": "Digest", "credentials": "foobar"}
def test_security_http_digest_no_credentials():
response = client.get("/users/me")
assert response.status_code == 403
assert response.json() == {"detail": "Not authenticated"}
def test_security_http_digest_incorrect_scheme_credentials():
response = client.get(
"/users/me", headers={"Authorization": "Other invalidauthorization"}
)
assert response.status_code == 403
assert response.json() == {"detail": "Invalid authentication credentials"}

View File

@@ -50,7 +50,49 @@ openapi_schema = {
},
],
}
}
},
"/users/": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Users Get",
"operationId": "read_users_users__get",
"parameters": [
{
"required": False,
"schema": {"title": "Q", "type": "string"},
"name": "q",
"in": "query",
},
{
"required": False,
"schema": {"title": "Skip", "type": "integer", "default": 0},
"name": "skip",
"in": "query",
},
{
"required": False,
"schema": {"title": "Limit", "type": "integer", "default": 100},
"name": "limit",
"in": "query",
},
],
}
},
},
"components": {
"schemas": {
@@ -97,6 +139,7 @@ def test_openapi_schema():
("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}),
("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}),
("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}),
("/users", 200, {"q": None, "skip": 0, "limit": 100}),
("/openapi.json", 200, openapi_schema),
],
)

View File

@@ -119,3 +119,15 @@ def test_post_file(tmpdir):
response = client.post("/files/", files={"file": open(path, "rb")})
assert response.status_code == 200
assert response.json() == {"file_size": 14}
def test_post_large_file(tmpdir):
default_pydantic_max_size = 2 ** 16
path = os.path.join(tmpdir, "test.txt")
with open(path, "wb") as file:
file.write(b"x" * (default_pydantic_max_size + 1))
client = TestClient(app)
response = client.post("/files/", files={"file": open(path, "rb")})
assert response.status_code == 200
assert response.json() == {"file_size": default_pydantic_max_size + 1}