Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5278314f2f | ||
|
|
4a0316bcfe | ||
|
|
0393a093d3 | ||
|
|
27f530a7ff | ||
|
|
c3e5e65093 | ||
|
|
804ec460fc | ||
|
|
0125ea4f83 | ||
|
|
216770118a | ||
|
|
a935d66b10 | ||
|
|
dd2541bc97 | ||
|
|
098e629344 | ||
|
|
bbe5f28b77 | ||
|
|
4a0922ebab | ||
|
|
8f16868c6a | ||
|
|
bc3e7f2bbc | ||
|
|
58848be2de | ||
|
|
cfb65d0e15 | ||
|
|
855daa2e53 | ||
|
|
de54e85152 | ||
|
|
b8d3070daf | ||
|
|
471c9cfc2d | ||
|
|
b79c13baed | ||
|
|
332ee4aee1 | ||
|
|
ad40f4a457 | ||
|
|
6b9931f882 | ||
|
|
4c51bb6714 | ||
|
|
57ff677027 | ||
|
|
613c3f3e95 | ||
|
|
bf6d923ca8 | ||
|
|
252188c686 | ||
|
|
510fec9bee | ||
|
|
a73709507c | ||
|
|
75407b9295 | ||
|
|
3180f35bdd | ||
|
|
d498b7feb3 | ||
|
|
3269e6a95c | ||
|
|
f1808de18e | ||
|
|
748dc375db | ||
|
|
b38fb937b0 | ||
|
|
23ef570bf6 | ||
|
|
c25a71e352 | ||
|
|
0c5e684ff9 |
1
Pipfile
@@ -21,6 +21,7 @@ mkdocs-material = "*"
|
||||
markdown-include = "*"
|
||||
autoflake = "*"
|
||||
email-validator = "*"
|
||||
ujson = "*"
|
||||
|
||||
[packages]
|
||||
starlette = "*"
|
||||
|
||||
13
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "64539bfa9f03f10715a5f83b1d62776513ae44518c0cff011b7540c17eada955"
|
||||
"sha256": "a0f966a95cb84845ca4aad02c44fc0e7c5e2047fc44dcf19a95a4abaa02d0197"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -624,9 +624,9 @@
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
"sha256:05910b7ff43cec0a853c15da0bfaf2867faa95f29b08e71f5846a195f1f38c75"
|
||||
"sha256:59880cc33ac293515892b2969aa8f4ed2cec592cbd0be4c4e20f2410468bbc62"
|
||||
],
|
||||
"version": "==0.14.7"
|
||||
"version": "==0.14.8"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
@@ -819,6 +819,13 @@
|
||||
"markers": "python_version < '3.7' and implementation_name == 'cpython'",
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"ujson": {
|
||||
"hashes": [
|
||||
"sha256:f66073e5506e91d204ab0c614a148d5aa938bdbf104751be66f8ad7a222f5f86"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.35"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
|
||||
|
||||
14
README.md
@@ -28,7 +28,7 @@ FastAPI is a modern, fast (high-performance), web framework for building APIs wi
|
||||
|
||||
The key features are:
|
||||
|
||||
* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic).
|
||||
* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance).
|
||||
|
||||
* **Fast to code**: Increase the speed to develop features by about 200% to 300% *.
|
||||
* **Less bugs**: Reduce about 40% of human (developer) induced errors. *
|
||||
@@ -36,7 +36,7 @@ The key features are:
|
||||
* **Easy**: Designed to be easy to use and learn. Less time reading docs.
|
||||
* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Less bugs.
|
||||
* **Robust**: Get production-ready code. With automatic interactive documentation.
|
||||
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" target="_blank">OpenAPI</a> and <a href="http://json-schema.org/" target="_blank">JSON Schema</a>.
|
||||
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" target="_blank">OpenAPI</a> (previously known as Swagger) and <a href="http://json-schema.org/" target="_blank">JSON Schema</a>.
|
||||
|
||||
<small>* estimation based on tests on an internal development team, building production applications.</small>
|
||||
|
||||
@@ -291,7 +291,7 @@ Coming back to the previous code example, **FastAPI** will:
|
||||
* Check that it has an optional attribute `is_offer`, that should be a `bool`, if present.
|
||||
* All this would also work for deeply nested JSON objects.
|
||||
* Convert from and to JSON automatically.
|
||||
* Document everything as an OpenAPI schema, that can be used by:
|
||||
* Document everything with OpenAPI, that can be used by:
|
||||
* Interactive documentation sytems.
|
||||
* Automatic client code generation systems, for many languages.
|
||||
* Provide 2 interactive documentation web interfaces directly.
|
||||
@@ -321,7 +321,6 @@ Try changing the line with:
|
||||
|
||||
...and see how your editor will auto-complete the attributes and know their types:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
@@ -343,6 +342,11 @@ For a more complete example including more features, see the <a href="https://fa
|
||||
* ...and more.
|
||||
|
||||
|
||||
## Performance
|
||||
|
||||
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=a979de55-980d-4721-a46f-77298b3f3923&hw=ph&test=fortune&l=zijzen-7" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
|
||||
|
||||
To understand more about it, see the section <a href="https://fastapi.tiangolo.com/benchmarks/" target="_blank">Benchmarks</a>.
|
||||
|
||||
## Optional Dependencies
|
||||
|
||||
@@ -367,7 +371,7 @@ Used by FastAPI / Starlette:
|
||||
|
||||
* <a href="http://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - for the server that loads and serves your application.
|
||||
|
||||
You can install all of these with `pip3 install fastapi[full]`.
|
||||
You can install all of these with `pip3 install fastapi[all]`.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
333
docs/alternatives.md
Normal file
@@ -0,0 +1,333 @@
|
||||
What inspired **FastAPI**, how it compares to other alternatives and what it learned from them.
|
||||
|
||||
## Intro
|
||||
|
||||
**FastAPI** wouldn't exist if not for the previous work of others.
|
||||
|
||||
There have been many tools created before that have helped inspire its creation.
|
||||
|
||||
I have been avoiding the creation of a new framework for several years. First I tried to solve all the features covered by **FastAPI** using many different frameworks, plug-ins and tools.
|
||||
|
||||
But at some point, there was no other option than creating something that provided all these features, taking the best ideas from previous tools, and combining them in the best way possible, using language features that weren't even available before (Python 3.6+ type hints).
|
||||
|
||||
## Previous tools
|
||||
|
||||
### <a href="https://www.djangoproject.com/" target="_blank">Django</a>
|
||||
|
||||
It's the most popular Python framework and is widely trusted. It is used to build systems like Instagram.
|
||||
|
||||
It's relatively tightly coupled with relational databases (like MySQL or PostgreSQL), so, having a NoSQL database (like Couchbase, MongoDB, Cassandra, etc) as the main store engine is not very easy.
|
||||
|
||||
It was created to generate the HTML in the backend, not to create APIs used by a modern frontend (like React, Vue.js and Angular) or by other systems (like <abbr title="Internet of Things">IoT</abbr> devices) communicating with it.
|
||||
|
||||
### <a href="https://www.django-rest-framework.org/" target="_blank">Django REST Framework</a>
|
||||
|
||||
Django REST framework was created to be a flexible toolkit for building Web APIs using Django underneath, to improve its API capabilities.
|
||||
|
||||
It is used by many companies including Mozilla, Red Hat and Eventbrite.
|
||||
|
||||
It was one of the first examples of **automatic API documentation**, and this was specifically one of the first ideas that inspired "the search for" **FastAPI**.
|
||||
|
||||
!!! note
|
||||
Django REST Framework was created by Tom Christie. The same creator of Starlette and Uvicorn, on which **FastAPI** is based.
|
||||
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Have an automatic API documentation web user interface.
|
||||
|
||||
### <a href="http://flask.pocoo.org/" target="_blank">Flask</a>
|
||||
|
||||
Flask is a "microframework", it doesn't include database integrations nor many of the things that come by default in Django.
|
||||
|
||||
This simplicity and flexibility allow doing things like using NoSQL databases as the main data storage system.
|
||||
|
||||
As it is very simple, it's relatively intuitive to learn, although the documentation gets somewhat technical at some points.
|
||||
|
||||
It is also commonly used for other applications that don't necessarily need a database, user management, or any of the many features that come pre-built in Django. Although many of these features can be added with plug-ins.
|
||||
|
||||
This decoupling of parts, and being a "microframework" that could be extended to cover exactly what is needed was a key feature that I wanted to keep.
|
||||
|
||||
Given the simplicity of Flask, it seemed like a good match for building APIs. The next thing to find was a "Django REST Framework" for Flask.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Be a micro-framework. Making it easy to mix and match the tools and parts needed.
|
||||
|
||||
Have a simple and easy to use routing system.
|
||||
|
||||
### <a href="https://swagger.io/" target="_blank">Swagger</a> / <a href="https://github.com/OAI/OpenAPI-Specification/" target="_blank">OpenAPI</a>
|
||||
|
||||
The main feature I wanted from Django REST Framework was the automatic API documentation.
|
||||
|
||||
Then I found that there was a standard to document APIs, using JSON (or YAML, an extension of JSON) called Swagger.
|
||||
|
||||
And there was a web user interface for Swagger APIs already created. So, being able to generate Swagger documentation for an API would allow using this web user interface automatically.
|
||||
|
||||
At some point, Swagger was given to the Linux Foundation, to be renamed OpenAPI.
|
||||
|
||||
That's why when talking about version 2.0 it's common to say "Swagger", and for version 3+ "OpenAPI".
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Adopt and use an open standard for API specifications, instead of a custom schema.
|
||||
|
||||
And integrate standards-based user interface tools:
|
||||
|
||||
* <a href="https://github.com/swagger-api/swagger-ui" target="_blank">Swagger UI</a>
|
||||
* <a href="https://github.com/Rebilly/ReDoc" target="_blank">ReDoc</a>
|
||||
|
||||
These two were chosen for being fairly popular and stable, but doing a quick search, you could find dozens of additional alternative user interfaces for OpenAPI (that you can use with **FastAPI**).
|
||||
|
||||
### Flask REST frameworks
|
||||
|
||||
There are several Flask REST frameworks, but after investing the time and work into investigating them, I found that many are discontinued or abandoned, with several standing issues that made them unfit.
|
||||
|
||||
## <a href="https://marshmallow.readthedocs.io/en/3.0/" target="_blank">Marshmallow</a>
|
||||
|
||||
One of the main features needed by API systems is data "<abbr title="also called marshalling, convertion">serialization</abbr>" which is taking data from the code (Python) and converting it into something that can be sent through the network. For example, converting an object containing data from a database into a JSON object. Converting `datetime` objects into strings, etc.
|
||||
|
||||
Another big feature needed by APIs is data validation, making sure that the data is valid, given certain parameters. For example, that some field is an `int`, and not some random string. This is especially useful for incoming data.
|
||||
|
||||
Without a data validation system, you would have to do all the checks by hand, in code.
|
||||
|
||||
These features are what Marshmallow was built to provide. It is a great library, and I have used it a lot before.
|
||||
|
||||
But it was created before there existed Python type hints. So, to define every <abbr title="the definition of how data should be formed">schema</abbr> you need to use specific utils and classes provided by Marshmallow.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Use code to define "schemas" that provide data types and validation, automatically.
|
||||
|
||||
### <a href="https://webargs.readthedocs.io/en/latest/" target="_blank">Webargs</a>
|
||||
|
||||
Another big feature required by APIs is <abbr title="reading and converting to Python data">parsing</abbr> data from incoming requests.
|
||||
|
||||
Webargs is a tool that was made to provide that on top of several frameworks, including Flask.
|
||||
|
||||
It uses Marshmallow underneath to do the data validation. And it was created by the same guys.
|
||||
|
||||
It's a great tool and I have used it a lot too, before having **FastAPI**.
|
||||
|
||||
!!! info
|
||||
Webargs was created by the same Marshmallow guys.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Have automatic validation of incoming request data.
|
||||
|
||||
### <a href="https://apispec.readthedocs.io/en/stable/" target="_blank">APISpec</a>
|
||||
|
||||
Marshmallow and Webargs provide validation, parsing and serialization as plug-ins.
|
||||
|
||||
But documentation is still missing. Then APISpec was created.
|
||||
|
||||
It is a plug-in for many frameworks (and there's a plug-in for Starlette too).
|
||||
|
||||
The way it works is that you write the definition of the schema using YAML format inside the docstring of each function handling a route.
|
||||
|
||||
And it generates Swagger 2.0 schemas (OpenAPI 2.0).
|
||||
|
||||
That's how it works in Flask, Starlette, Responder, etc.
|
||||
|
||||
But then, we have again the problem of having a micro-syntax, inside of a Python string (a big YAML).
|
||||
|
||||
The editor can't help much with that. And if we modify parameters or Marshmallow schemas and forget to also modify that YAML docstring, the generated schema would be obsolete.
|
||||
|
||||
!!! info
|
||||
APISpec was created by the same Marshmallow guys.
|
||||
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Support the open standard for APIs, OpenAPI.
|
||||
|
||||
### <a href="https://flask-apispec.readthedocs.io/en/latest/" target="_blank">Flask-apispec</a>
|
||||
|
||||
It's a Flask plug-in, that ties together Webargs, Marshmallow and APISpec.
|
||||
|
||||
It uses the information from Webargs and Marshmallow to automatically generate Swagger 2.0 schemas, using APISpec.
|
||||
|
||||
It's a great tool, very under-rated. It should be way more popular than many Flask plug-ins out there. It might be due to its documentation being too concise and abstract.
|
||||
|
||||
This solved having to write YAML (another syntax) inside of Python docstrings.
|
||||
|
||||
This combination of Flask, Flask-apispec with Marshmallow and Webargs was my favorite backend stack until building **FastAPI**.
|
||||
|
||||
Using it led to the creation of several Flask full-stack generators. These are the main stack I (and several external teams) have been using up to now:
|
||||
|
||||
* <a href="https://github.com/tiangolo/full-stack" target="_blank">https://github.com/tiangolo/full-stack</a>
|
||||
* <a href="https://github.com/tiangolo/full-stack-flask-couchbase" target="_blank">https://github.com/tiangolo/full-stack-flask-couchbase</a>
|
||||
* <a href="https://github.com/tiangolo/full-stack-flask-couchdb" target="_blank">https://github.com/tiangolo/full-stack-flask-couchdb</a>
|
||||
|
||||
And these same full-stack generators were the base of the <a href="/project-generation/" target="_blank">**FastAPI** project generator</a>.
|
||||
|
||||
!!! info
|
||||
Flask-apispec was created by the same Marshmallow guys.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Generate the OpenAPI schema automatically, from the same code that defines serialization and validation.
|
||||
|
||||
### <a href="https://nestjs.com/" target="_blank">NestJS</a> (and <a href="https://angular.io/" target="_blank">Angular</a>)
|
||||
|
||||
This isn't even Python, NestJS is a JavaScript (TypeScript) NodeJS framework inspired by Angular.
|
||||
|
||||
It achieves something somewhat similar to what can be done with Flask-apispec.
|
||||
|
||||
It has an integrated dependency injection system, inspired by Angular two. It requires pre-registering the "injectables" (like all the other dependency injection systems I know), so, it adds to the verbosity and code repetition.
|
||||
|
||||
As the parameters are described with TypeScript types (similar to Python type hints), editor support is quite good.
|
||||
|
||||
But as TypeScript data is not preserved after compilation to JavaScript, it cannot rely on the types to define validation, serialization and documentation at the same time. Due to this and some design decisions, to get validation, serialization and automatic schema generation, it's needed to add decorators in many places. So, it becomes quite verbose.
|
||||
|
||||
It can't handle nested models very well. So, if the JSON body in the request is a JSON object that has inner fields that in turn are nested JSON objects, it cannot be properly documented and validated.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Use Python types to have great editor support.
|
||||
|
||||
Have a powerful dependency injection system. Find a way to minimize code repetition.
|
||||
|
||||
### <a href="https://sanic.readthedocs.io/en/latest/" target="_blank">Sanic</a>
|
||||
|
||||
It was one of the first extremely fast Python frameworks based on `asyncio`. It was made to be very similar to Flask.
|
||||
|
||||
!!! note "Technical Details"
|
||||
It used <a href="https://github.com/MagicStack/uvloop" target="_blank">`uvloop`</a> instead of the default Python `asyncio` loop. That's what made it so fast.
|
||||
|
||||
It <a href="https://github.com/huge-success/sanic/issues/761" target="_blank">still doesn't implement the ASGI spec for Python asynchronous web development</a>, but it clearly inspired Uvicorn and Starlette, that are currently faster than Sanic in open benchmarks.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Find a way to have a crazy performance.
|
||||
|
||||
That's why **FastAPI** is based on Starlette, as it is the fastest framework available (tested by third-party benchmarks).
|
||||
|
||||
### <a href="https://moltenframework.com/" target="_blank">Molten</a>
|
||||
|
||||
I discovered Molten in the first stages of building **FastAPI**. And it has quite similar ideas:
|
||||
|
||||
* Based on Python type hints.
|
||||
* Validation and documentation from these types.
|
||||
* Dependency Injection system.
|
||||
|
||||
It doesn't use a data validation, serialization and documentation third-party library like Pydantic, it has its own. So, these data type definitions would not be reusable as easily.
|
||||
|
||||
It requires a little bit more verbose configurations. And as it is based on WSGI (instead of ASGI), it is not designed to take advantage of the high-performance provided by tools like Uvicorn, Starlette and Sanic.
|
||||
|
||||
The dependency injection system requires pre-registration of the dependencies and the dependencies are solved based on the declared types. So, it's not possible to declare more than one "component" that provides a certain type.
|
||||
|
||||
Routes are declared in a single place, using functions declared in other places (instead of using decorators that can be placed right on top of the function that handles the endpoint). This is closer to how Django does it than to how Flask (and Starlette) does it. It separates in the code things that are relatively tightly coupled.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Define extra validations for data types using the "default" value of model attributes. This improves editor support, and it was not available in Pydantic before.
|
||||
|
||||
This actually inspired updating parts of Pydantic, to support the same validation declaration style (all this functionality is now already available in Pydantic).
|
||||
|
||||
### <a href="https://github.com/encode/apistar" target="_blank">APIStar</a> (<= 0.5)
|
||||
|
||||
Right before deciding to build **FastAPI** I found **APIStar** server. It had almost everything I was looking for and had a great design.
|
||||
|
||||
It was actually the first implementation of a framework using Python type hints to declare parameters and requests that I ever saw (before NestJS and Molten).
|
||||
|
||||
It had automatic data validation, data serialization and OpenAPI schema generation based on the same type hints in several places.
|
||||
|
||||
Body schema definitions didn't use the same Python type hints like Pydantic, it was a bit more similar to Marshmallow, so, editor support wouldn't be as good, but still, APIStar was the best available option.
|
||||
|
||||
It had the best performance benchmarks at the time (only surpassed by Starlette).
|
||||
|
||||
At first, it didn't have an automatic API documentation web UI, but I knew I could add Swagger UI to it.
|
||||
|
||||
It had a dependency injection system. It required pre-registration of components, as other tools discussed above. But still, it was a great feature.
|
||||
|
||||
I was never able to use it in a full project, as it didn't have security integration, so, I couldn't replace all the features I was having with the full-stack generators based on Flask-apispec. I had in my backlog of projects to create a pull request adding that functionality.
|
||||
|
||||
But then, the project's focus shifted.
|
||||
|
||||
It was no longer an API web framework, as the creator needed to focus on Starlette.
|
||||
|
||||
Now APIStar is a set of tools to validate OpenAPI specifications, not a web framework.
|
||||
|
||||
!!! info
|
||||
APIStar was created by Tom Christie. The same guy that created:
|
||||
|
||||
* Django REST Framework
|
||||
* Starlette (in which **FastAPI** is based)
|
||||
* Uvicorn (used by Starlette and **FastAPI**)
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Exist.
|
||||
|
||||
The idea of declaring multiple things (data validation, serialization and documentation) with the same Python types, that at the same time provided great editor support, was something I considered a brilliant idea.
|
||||
|
||||
And after searching for a long time for a similar framework and testing many different alternatives, APIStar was the best option available.
|
||||
|
||||
Then APIStar stopped to exist as a server and Starlette was created, and was a new better foundation for such a system. That was the final inspiration to build **FastAPI**.
|
||||
|
||||
I consider **FastAPI** a "spiritual successor" to APIStar, while improving and increasing the features, typing system, and other parts, based on the learnings from all these previous tools.
|
||||
|
||||
## Used by **FastAPI**
|
||||
|
||||
### <a href="https://pydantic-docs.helpmanual.io/" target="_blank">Pydantic</a>
|
||||
|
||||
Pydantic is a library to define data validation, serialization and documentation (using JSON Schema) based on Python type hints.
|
||||
|
||||
That makes it extremely intuitive.
|
||||
|
||||
It is comparable to Marshmallow. Although it's faster than Marshmallow in benchmarks. And as it is based on the same Python type hints, the editor support is great.
|
||||
|
||||
!!! check "**FastAPI** uses it to"
|
||||
Handle all the data validation, data serialization and automatic model documentation (based on JSON Schema).
|
||||
|
||||
**FastAPI** then takes that JSON Schema data and puts it in OpenAPI, apart from all the other things it does.
|
||||
|
||||
### <a href="https://www.starlette.io/" target="_blank">Starlette</a>
|
||||
|
||||
Starlette is a lightweight <abbr title="The new standard for building asynchronous Python web">ASGI</abbr> framework/toolkit, which is ideal for building high-performance asyncio services.
|
||||
|
||||
It is very simple and intuitive. It's designed to be easily extensible, and have modular components.
|
||||
|
||||
It has:
|
||||
|
||||
* Seriously impressive performance.
|
||||
* WebSocket support.
|
||||
* GraphQL support.
|
||||
* In-process background tasks.
|
||||
* Startup and shutdown events.
|
||||
* Test client built on requests.
|
||||
* CORS, GZip, Static Files, Streaming responses.
|
||||
* Session and Cookie support.
|
||||
* 100% test coverage.
|
||||
* 100% type annotated codebase.
|
||||
* Zero hard dependencies.
|
||||
|
||||
Starlette is currently the fastest Python framework tested. Only surpassed by Uvicorn, which is not a framework, but a server.
|
||||
|
||||
Starlette provides all the basic web microframework functionality.
|
||||
|
||||
But it doesn't provide automatic data validation, serialization or documentation.
|
||||
|
||||
That's one of the main things that **FastAPI** adds on top, all based on Python type hints (using Pydantic). That, plus the dependency injection system, security utilities, OpenAPI schema generation, etc.
|
||||
|
||||
!!! note "Technical Details"
|
||||
ASGI is a new "standard" being developed by Django core team members. It is still not a "Python standard" (a PEP), although they are in the process of doing that.
|
||||
|
||||
Nevertheless, it is already being used as a "standard" by several tools. This greatly improves interoperability, as you could switch Uvicorn for any other ASGI server (like Daphne or Hypercorn), or you could add ASGI compatible tools, like `python-socketio`.
|
||||
|
||||
!!! check "**FastAPI** uses it to"
|
||||
Handle all the core web parts. Adding features on top.
|
||||
|
||||
The class `FastAPI` itself inherits directly from the class `Starlette`.
|
||||
|
||||
So, anything that you can do with Starlette, you can do it directly with **FastAPI**, as it is basically Starlette on steroids.
|
||||
|
||||
### <a href="https://www.uvicorn.org/" target="_blank">Uvicorn</a>
|
||||
|
||||
Uvicorn is a lightning-fast ASGI server, built on uvloop and httptools.
|
||||
|
||||
It is not a web framework, but a server. For example, it doesn't provide tools for routing by paths. That's something that a framework like Starlette (or **FastAPI**) would provide on top.
|
||||
|
||||
It is the recommended server for Starlette and **FastAPI**.
|
||||
|
||||
!!! check "**FastAPI** recommends it as"
|
||||
The main web server to run **FastAPI** applications.
|
||||
|
||||
You can combine it with Gunicorn, to have an asynchronous multiprocess server.
|
||||
|
||||
Check more details in the <a href="/deployment/" target="_blank">Deployment</a> section.
|
||||
|
||||
## Benchmarks and speed
|
||||
|
||||
To understand, compare, and see the difference between Uvicorn, Starlette and FastAPI, check the section about [Benchmarks](/benchmarks/).
|
||||
32
docs/benchmarks.md
Normal file
@@ -0,0 +1,32 @@
|
||||
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=a979de55-980d-4721-a46f-77298b3f3923&hw=ph&test=fortune&l=zijzen-7" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
|
||||
|
||||
But when checking benchmarks and comparisons you should have the following in mind.
|
||||
|
||||
## Benchmarks and speed
|
||||
|
||||
When you check the benchmarks, it is common to see several tools of different types compared as equivalent.
|
||||
|
||||
Specifically, to see Uvicorn, Starlette and FastAPI compared together (among many other tools).
|
||||
|
||||
The simplest the problem solved by the tool, the better performance it will get. And most of the benchmarks don't test the additional features provided by the tool.
|
||||
|
||||
The hierarchy is like:
|
||||
|
||||
* **Uvicorn**: an ASGI server
|
||||
* **Starlette**: (uses Uvicorn) a web microframework
|
||||
* **FastAPI**: (uses Starlette) an API microframework with several additional features for building APIs, with data validation, etc.
|
||||
|
||||
* **Uvicorn**:
|
||||
* Will have the best performance, as it doesn't have much extra code apart from the server itself.
|
||||
* You wouldn't write an application in Uvicorn directly. That would mean that your code would have to include more or less, at least, all the code provided by Starlette (or **FastAPI**). And if you did that, your final application would have the same overhead as having used a framework and minimizing your app code and bugs.
|
||||
* If you are comparing Uvicorn, compare it against Daphne, Hypercorn, uWSGI, etc. Application servers.
|
||||
* **Starlette**:
|
||||
* Will have the next best performance, after Uvicorn. In fact, Starlette uses Uvicorn to run. So, it probably can only get "slower" than Uvicorn by having to execute more code.
|
||||
* But it provides you the tools to build simple web applications, with routing based on paths, etc.
|
||||
* If you are comparing Starlette, compare it against Sanic, Flask, Django, etc. Web frameworks (or microframeworks).
|
||||
* **FastAPI**:
|
||||
* The same way that Starlette uses Uvicorn and cannot be faster than it, **FastAPI** uses Starlette, so it cannot be faster than it.
|
||||
* FastAPI provides more features on top of Starlette. Features that you almost always need when building APIs, like data validation and serialization. And by using it, you get automatic documentation for free (the automatic documentation doesn't even add overhead to running applications, it is generated on startup).
|
||||
* If you didn't use FastAPI and used Starlette directly (or another tool, like Sanic, Flask, Responder, etc) you would have to implement all the data validation and serialization yourself. So, your final application would still have the same overhead as if it was built using FastAPI. And in many cases, this data validation and serialization is the biggest amount of code written in applications.
|
||||
* So, by using FastAPI you are saving development time, bugs, lines of code, and you would probably get the same performance (or better) you would if you didn't use it (as you would have to implement it all in your code).
|
||||
* If you are comparing FastAPI, compare it against a web application framework (or set of tools) that provides data validation, serialization and documentation, like Flask-apispec, NestJS, Molten, etc. Frameworks with integrated automatic data validation, serialization and documentation.
|
||||
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 65 KiB |
BIN
docs/img/tutorial/security/image01.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/img/tutorial/security/image02.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
docs/img/tutorial/security/image03.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
docs/img/tutorial/security/image04.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
docs/img/tutorial/security/image05.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
docs/img/tutorial/security/image06.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
docs/img/tutorial/security/image07.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
docs/img/tutorial/security/image08.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
docs/img/tutorial/security/image09.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
docs/img/tutorial/security/image10.png
Normal file
|
After Width: | Height: | Size: 155 KiB |
@@ -28,7 +28,7 @@ FastAPI is a modern, fast (high-performance), web framework for building APIs wi
|
||||
|
||||
The key features are:
|
||||
|
||||
* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic).
|
||||
* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance).
|
||||
|
||||
* **Fast to code**: Increase the speed to develop features by about 200% to 300% *.
|
||||
* **Less bugs**: Reduce about 40% of human (developer) induced errors. *
|
||||
@@ -36,7 +36,7 @@ The key features are:
|
||||
* **Easy**: Designed to be easy to use and learn. Less time reading docs.
|
||||
* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Less bugs.
|
||||
* **Robust**: Get production-ready code. With automatic interactive documentation.
|
||||
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" target="_blank">OpenAPI</a> and <a href="http://json-schema.org/" target="_blank">JSON Schema</a>.
|
||||
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" target="_blank">OpenAPI</a> (previously known as Swagger) and <a href="http://json-schema.org/" target="_blank">JSON Schema</a>.
|
||||
|
||||
<small>* estimation based on tests on an internal development team, building production applications.</small>
|
||||
|
||||
@@ -291,7 +291,7 @@ Coming back to the previous code example, **FastAPI** will:
|
||||
* Check that it has an optional attribute `is_offer`, that should be a `bool`, if present.
|
||||
* All this would also work for deeply nested JSON objects.
|
||||
* Convert from and to JSON automatically.
|
||||
* Document everything as an OpenAPI schema, that can be used by:
|
||||
* Document everything with OpenAPI, that can be used by:
|
||||
* Interactive documentation sytems.
|
||||
* Automatic client code generation systems, for many languages.
|
||||
* Provide 2 interactive documentation web interfaces directly.
|
||||
@@ -321,7 +321,6 @@ Try changing the line with:
|
||||
|
||||
...and see how your editor will auto-complete the attributes and know their types:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
@@ -343,6 +342,11 @@ For a more complete example including more features, see the <a href="https://fa
|
||||
* ...and more.
|
||||
|
||||
|
||||
## Performance
|
||||
|
||||
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=a979de55-980d-4721-a46f-77298b3f3923&hw=ph&test=fortune&l=zijzen-7" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
|
||||
|
||||
To understand more about it, see the section <a href="https://fastapi.tiangolo.com/benchmarks/" target="_blank">Benchmarks</a>.
|
||||
|
||||
## Optional Dependencies
|
||||
|
||||
@@ -367,7 +371,7 @@ Used by FastAPI / Starlette:
|
||||
|
||||
* <a href="http://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - for the server that loads and serves your application.
|
||||
|
||||
You can install all of these with `pip3 install fastapi[full]`.
|
||||
You can install all of these with `pip3 install fastapi[all]`.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
42
docs/project-generation.md
Normal file
@@ -0,0 +1,42 @@
|
||||
There is a project generator that you can use to get started, with a lot of the initial set up, security, database and first API endpoints already done for you.
|
||||
|
||||
## Full-Stack-FastAPI-Couchbase
|
||||
|
||||
GitHub: <a href="https://github.com/tiangolo/full-stack-fastapi-couchbase" target="_blank">https://github.com/tiangolo/full-stack-fastapi-couchbase</a>
|
||||
|
||||
### Features
|
||||
|
||||
* Full **Docker** integration (Docker based).
|
||||
* Docker Swarm Mode deployment.
|
||||
* **Docker Compose** integration and optimization for local development.
|
||||
* **Production ready** Python web server using Uvicorn and Gunicorn.
|
||||
* Python **FastAPI** backend with all its features.
|
||||
* **Celery** worker that can import and use code from the rest of the backend selectively (you don't have to install the complete app in each worker).
|
||||
* **NoSQL Couchbase** database that supports direct synchronization via Couchbase Sync Gateway for offline-first applications.
|
||||
* **Full Text Search** integrated, using Couchbase.
|
||||
* REST backend tests based on Pytest, integrated with Docker, so you can test the full API interaction, independent on the database. As it runs in Docker, it can build a new data store from scratch each time (so you can use ElasticSearch, MongoDB, or whatever you want, and just test that the API works).
|
||||
* Easy Python integration with **Jupyter** Kernels for remote or in-Docker development with extensions like Atom Hydrogen or Visual Studio Code Jupyter.
|
||||
* **Email notifications** for account creation and password recovery, compatible with:
|
||||
* Mailgun
|
||||
* SparkPost
|
||||
* SendGrid
|
||||
* ...any other provider that can generate standard SMTP credentials.
|
||||
* **Vue** frontend:
|
||||
* Generated with Vue CLI.
|
||||
* **JWT Authentication** handling.
|
||||
* Login view.
|
||||
* After login, main dashboard view.
|
||||
* Main dashboard with user creation and edition.
|
||||
* Self user edition.
|
||||
* **Vuex**.
|
||||
* **Vue-router**.
|
||||
* **Vuetify** for beautiful material design components.
|
||||
* **TypeScript**.
|
||||
* Docker server based on **Nginx** (configured to play nicely with Vue-router).
|
||||
* Docker multi-stage building, so you don't need to save or commit compiled code.
|
||||
* Frontend tests ran at build time (can be disabled too).
|
||||
* Made as modular as possible, so it works out of the box, but you can re-generate with Vue CLI or create it as you need, and re-use what you want.
|
||||
* Flower for Celery jobs monitoring.
|
||||
* Load balancing between frontend and backend with **Traefik**, so you can have both under the same domain, separated by path, but served by different containers.
|
||||
* Traefik integration, including Let's Encrypt **HTTPS** certificates automatic generation.
|
||||
* GitLab **CI** (continuous integration), including frontend and backend testing.
|
||||
@@ -1,5 +1,4 @@
|
||||
from fastapi import Depends, FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -7,21 +6,18 @@ app = FastAPI()
|
||||
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
|
||||
|
||||
|
||||
class CommonQueryParams(BaseModel):
|
||||
q: str = None
|
||||
skip: int = None
|
||||
limit: int = None
|
||||
|
||||
|
||||
async def common_parameters(q: str = None, skip: int = 0, limit: int = 100):
|
||||
return CommonQueryParams(q=q, skip=skip, limit=limit)
|
||||
class CommonQueryParams:
|
||||
def __init__(self, q: str = None, skip: int = 0, limit: int = 100):
|
||||
self.q = q
|
||||
self.skip = skip
|
||||
self.limit = limit
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items(commons: CommonQueryParams = Depends(common_parameters)):
|
||||
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
|
||||
response = {}
|
||||
if commons.q:
|
||||
response.update({"q": commons.q})
|
||||
items = fake_items_db[commons.skip : commons.limit]
|
||||
items = fake_items_db[commons.skip : commons.skip + commons.limit]
|
||||
response.update({"items": items})
|
||||
return response
|
||||
|
||||
@@ -1,34 +1,23 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import Cookie, Depends, FastAPI
|
||||
from pydantic import BaseModel
|
||||
from fastapi import Depends, FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class InterestsTracker(BaseModel):
|
||||
track_code: str
|
||||
interests: List[str]
|
||||
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
|
||||
|
||||
|
||||
fake_tracked_users_db = {
|
||||
"Foo": {"track_code": "Foo", "interests": ["sports", "movies"]},
|
||||
"Bar": {"track_code": "Bar", "interests": ["food", "shows"]},
|
||||
"Baz": {"track_code": "Baz", "interests": ["gaming", "virtual reality"]},
|
||||
}
|
||||
class CommonQueryParams:
|
||||
def __init__(self, q: str = None, skip: int = 0, limit: int = 100):
|
||||
self.q = q
|
||||
self.skip = skip
|
||||
self.limit = limit
|
||||
|
||||
|
||||
async def get_tracked_interests(track_code: str = Cookie(None)):
|
||||
if track_code in fake_tracked_users_db:
|
||||
track_dict = fake_tracked_users_db[track_code]
|
||||
track = InterestsTracker(**track_dict)
|
||||
return track
|
||||
return None
|
||||
|
||||
|
||||
@app.get("/interests/")
|
||||
async def read_interests(
|
||||
tracked_interests: InterestsTracker = Depends(get_tracked_interests)
|
||||
):
|
||||
response = {"interests": tracked_interests.interests}
|
||||
@app.get("/items/")
|
||||
async def read_items(commons=Depends(CommonQueryParams)):
|
||||
response = {}
|
||||
if commons.q:
|
||||
response.update({"q": commons.q})
|
||||
items = fake_items_db[commons.skip : commons.skip + commons.limit]
|
||||
response.update({"items": items})
|
||||
return response
|
||||
|
||||
@@ -1,49 +1,23 @@
|
||||
from random import choice
|
||||
from typing import List
|
||||
|
||||
from fastapi import Cookie, Depends, FastAPI
|
||||
from pydantic import BaseModel
|
||||
from fastapi import Depends, FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class InterestsTracker(BaseModel):
|
||||
track_code: str
|
||||
interests: List[str]
|
||||
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
|
||||
|
||||
|
||||
fake_tracked_users_db = {
|
||||
"Foo": {"track_code": "Foo", "interests": ["sports", "movies"]},
|
||||
"Bar": {"track_code": "Bar", "interests": ["food", "shows"]},
|
||||
"Baz": {"track_code": "Baz", "interests": ["gaming", "virtual reality"]},
|
||||
}
|
||||
class CommonQueryParams:
|
||||
def __init__(self, q: str = None, skip: int = 0, limit: int = 100):
|
||||
self.q = q
|
||||
self.skip = skip
|
||||
self.limit = limit
|
||||
|
||||
|
||||
async def get_tracked_interests(track_code: str = Cookie(None)):
|
||||
if track_code in fake_tracked_users_db:
|
||||
track_dict = fake_tracked_users_db[track_code]
|
||||
track = InterestsTracker(**track_dict)
|
||||
return track
|
||||
return None
|
||||
|
||||
|
||||
class ComplexTracker:
|
||||
def __init__(self, tracker: InterestsTracker = Depends(get_tracked_interests)):
|
||||
self.tracker = tracker
|
||||
|
||||
def random_interest(self):
|
||||
"""
|
||||
Get a random interest from the tracked ones for the current user.
|
||||
If the user doesn't have tracked interests, return a random one from the ones available.
|
||||
"""
|
||||
if self.tracker.interests:
|
||||
return choice(self.tracker.interests)
|
||||
return choice(
|
||||
["sports", "movies", "food", "shows", "gaming", "virtual reality"]
|
||||
)
|
||||
|
||||
|
||||
@app.get("/suggested-category")
|
||||
async def read_suggested_category(tracker: ComplexTracker = Depends(None)):
|
||||
response = {"category": tracker.random_interest()}
|
||||
@app.get("/items/")
|
||||
async def read_items(commons: CommonQueryParams = Depends()):
|
||||
response = {}
|
||||
if commons.q:
|
||||
response.update({"q": commons.q})
|
||||
items = fake_items_db[commons.skip : commons.skip + commons.limit]
|
||||
response.update({"items": items})
|
||||
return response
|
||||
|
||||
20
docs/src/dependencies/tutorial005.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from fastapi import Cookie, Depends, FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
def query_extractor(q: str = None):
|
||||
return q
|
||||
|
||||
|
||||
def query_or_cookie_extractor(
|
||||
q: str = Depends(query_extractor), last_query: str = Cookie(None)
|
||||
):
|
||||
if not q:
|
||||
return last_query
|
||||
return q
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_query(query_or_default: str = Depends(query_or_cookie_extractor)):
|
||||
return {"q_or_cookie": query_or_default}
|
||||
21
docs/src/dependencies/tutorial006.py
Normal file
@@ -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}
|
||||
27
docs/src/extra_data_types/tutorial001.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from datetime import datetime, time, timedelta
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Body, FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.put("/items/{item_id}")
|
||||
async def read_items(
|
||||
item_id: UUID,
|
||||
start_datetime: datetime = Body(None),
|
||||
end_datetime: datetime = Body(None),
|
||||
repeat_at: time = Body(None),
|
||||
process_after: timedelta = Body(None),
|
||||
):
|
||||
start_process = start_datetime + process_after
|
||||
duration = end_datetime - start_process
|
||||
return {
|
||||
"item_id": item_id,
|
||||
"start_datetime": start_datetime,
|
||||
"end_datetime": end_datetime,
|
||||
"repeat_at": repeat_at,
|
||||
"process_after": process_after,
|
||||
"start_process": start_process,
|
||||
"duration": duration,
|
||||
}
|
||||
@@ -7,4 +7,4 @@ fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_item(skip: int = 0, limit: int = 100):
|
||||
return fake_items_db[skip:limit]
|
||||
return fake_items_db[skip : skip + limit]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from pydantic import BaseModel
|
||||
@@ -10,8 +8,16 @@ fake_users_db = {
|
||||
"username": "johndoe",
|
||||
"full_name": "John Doe",
|
||||
"email": "johndoe@example.com",
|
||||
"password_hash": "fakehashedsecret",
|
||||
}
|
||||
"hashed_password": "fakehashedsecret",
|
||||
"disabled": False,
|
||||
},
|
||||
"alice": {
|
||||
"username": "alice",
|
||||
"full_name": "Alice Wonderson",
|
||||
"email": "alice@example.com",
|
||||
"hashed_password": "fakehashedsecret2",
|
||||
"disabled": True,
|
||||
},
|
||||
}
|
||||
|
||||
app = FastAPI()
|
||||
@@ -26,9 +32,9 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: Optional[str] = None
|
||||
full_name: Optional[str] = None
|
||||
disabled: Optional[bool] = None
|
||||
email: str = None
|
||||
full_name: str = None
|
||||
disabled: bool = None
|
||||
|
||||
|
||||
class UserInDB(User):
|
||||
@@ -51,26 +57,27 @@ def fake_decode_token(token):
|
||||
async def get_current_user(token: str = Security(oauth2_scheme)):
|
||||
user = fake_decode_token(token)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Invalid authentication credentials"
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
||||
if not current_user.disabled:
|
||||
if current_user.disabled:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
||||
|
||||
@app.post("/token")
|
||||
async def login(form_data: OAuth2PasswordRequestForm):
|
||||
data = form_data.parse()
|
||||
user_dict = fake_users_db[data.username]
|
||||
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
user_dict = fake_users_db.get(form_data.username)
|
||||
if not user_dict:
|
||||
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
||||
user = UserInDB(**user_dict)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Incorrect email or password")
|
||||
hashed_password = fake_hash_password(data.password)
|
||||
hashed_password = fake_hash_password(form_data.password)
|
||||
if not hashed_password == user.hashed_password:
|
||||
raise HTTPException(status_code=400, detail="Incorrect email or password")
|
||||
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
||||
|
||||
return {"access_token": user.username, "token_type": "bearer"}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
import jwt
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
@@ -23,7 +22,8 @@ fake_users_db = {
|
||||
"username": "johndoe",
|
||||
"full_name": "John Doe",
|
||||
"email": "johndoe@example.com",
|
||||
"password_hash": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
|
||||
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
|
||||
"disabled": False,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,9 @@ class TokenPayload(BaseModel):
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: Optional[str] = None
|
||||
full_name: Optional[str] = None
|
||||
disabled: Optional[bool] = None
|
||||
email: str = None
|
||||
full_name: str = None
|
||||
disabled: bool = None
|
||||
|
||||
|
||||
class UserInDB(User):
|
||||
@@ -102,24 +102,21 @@ async def get_current_user(token: str = Security(oauth2_scheme)):
|
||||
|
||||
|
||||
async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
||||
if not current_user.disabled:
|
||||
if current_user.disabled:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
||||
|
||||
@app.post("/token", response_model=Token)
|
||||
async def route_login_access_token(form_data: OAuth2PasswordRequestForm):
|
||||
data = form_data.parse()
|
||||
user = authenticate_user(fake_users_db, data.username, data.password)
|
||||
async def route_login_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Incorrect email or password")
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
return {
|
||||
"access_token": create_access_token(
|
||||
data={"username": data.username}, expires_delta=access_token_expires
|
||||
),
|
||||
"token_type": "bearer",
|
||||
}
|
||||
access_token = create_access_token(
|
||||
data={"username": form_data.username}, expires_delta=access_token_expires
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@app.get("/users/me", response_model=User)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
Coming soon...
|
||||
|
||||
```Python
|
||||
{!./src/application-configuration/tutorial001.py!}
|
||||
{!./src/application_configuration/tutorial001.py!}
|
||||
```
|
||||
|
||||
```Python
|
||||
{!./src/application-configuration/tutorial002.py!}
|
||||
{!./src/application_configuration/tutorial002.py!}
|
||||
```
|
||||
|
||||
```Python
|
||||
{!./src/application-configuration/tutorial003.py!}
|
||||
{!./src/application_configuration/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -116,7 +116,7 @@ Again, doing just that declaration, with **FastAPI** you get:
|
||||
|
||||
Apart from normal singular types like `str`, `int`, `float`, etc. You can use more complex singular types that inherit from `str`.
|
||||
|
||||
To see all the options you have, checkout the docs for <a href="https://pydantic-docs.helpmanual.io/#exotic-types" target="_blank">Pydantic's exotic types</a>.
|
||||
To see all the options you have, checkout the docs for <a href="https://pydantic-docs.helpmanual.io/#exotic-types" target="_blank">Pydantic's exotic types</a>. You will see some examples in the next chapter.
|
||||
|
||||
For example, as in the `Image` model we have a `url` field, we can declare it to be instead of a `str`, a Pydantic's `UrlStr`:
|
||||
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
To declare a request body, you use <a href="https://pydantic-docs.helpmanual.io/" target="_blank">Pydantic</a> models with all their power and benefits.
|
||||
When you need to send data from a client (let's say, a browser) to your API, you send it as a **request body**.
|
||||
|
||||
A **request** body is data sent by the client to your API. A **response** body is the data your API sends to the client.
|
||||
|
||||
Your API almost always has to send a **response** body. But clients don't necessarily need to send **request** bodies all the time.
|
||||
|
||||
To declare a **request** body, you use <a href="https://pydantic-docs.helpmanual.io/" target="_blank">Pydantic</a> models with all their power and benefits.
|
||||
|
||||
!!! info
|
||||
You cannot send a request body using a `GET` operation (HTTP method).
|
||||
|
||||
To send data, you have to use one of: `POST` (the more common), `PUT`, `DELETE` or `PATCH`.
|
||||
|
||||
## Import Pydantic's `BaseModel`
|
||||
|
||||
|
||||
71
docs/tutorial/dependencies/advanced-dependencies.md
Normal file
@@ -0,0 +1,71 @@
|
||||
!!! danger
|
||||
This is, more or less, an "advanced" chapter.
|
||||
|
||||
If you are just starting with **FastAPI** you might want to skip this chapter and come back to it later.
|
||||
|
||||
## Parameterized dependencies
|
||||
|
||||
All the dependencies we have seen are a fixed function or class.
|
||||
|
||||
But there could be cases where you want to be able to set parameters on the dependency, without having to declare many different functions or classes.
|
||||
|
||||
Let's imagine that we want to have a dependency that checks if the query parameter `q` contains some fixed content.
|
||||
|
||||
But we want to be able to parameterize that fixed content.
|
||||
|
||||
## A "callable" instance
|
||||
|
||||
In Python there's a way to make an instance of a class a "callable".
|
||||
|
||||
Not the class itself (which is already a callable), but an instance of that class.
|
||||
|
||||
To do that, we declare a method `__call__`:
|
||||
|
||||
```Python hl_lines="10"
|
||||
{!./src/dependencies/tutorial006.py!}
|
||||
```
|
||||
|
||||
## Parameterize the instance
|
||||
|
||||
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!}
|
||||
```
|
||||
|
||||
In this case, **FastAPI** won't ever touch or care about `__init__`, we will use it directly in our code.
|
||||
|
||||
## Create an instance
|
||||
|
||||
We could create an instance of this class with:
|
||||
|
||||
```Python hl_lines="16"
|
||||
{!./src/dependencies/tutorial006.py!}
|
||||
```
|
||||
|
||||
And that way we are able to "parameterize" our dependency, that now has `"bar"` inside of it, as the attribute `checker.fixed_content`.
|
||||
|
||||
## Use the instance as a dependency
|
||||
|
||||
Then, we could use this `checker` in a `Depends(checker)`, instead of `Depends(FixedContentQueryChecker)`, because the dependency is the instance, `checker`, not the class itself.
|
||||
|
||||
And when solving the dependency, **FastAPI** will call this `checker` like:
|
||||
|
||||
```Python
|
||||
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!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
All this might seem contrived. And it might not be very clear how is it useful yet.
|
||||
|
||||
These examples are intentionally simple, but show how it all works.
|
||||
|
||||
In the chapters about security, you will be using utility functions that are implemented in this same way.
|
||||
|
||||
If you understood all this, you already know how those utility tools for security work underneath.
|
||||
174
docs/tutorial/dependencies/classes-as-dependencies.md
Normal file
@@ -0,0 +1,174 @@
|
||||
Before diving deeper into the **Dependency Injection** system, let's upgrade the previous example.
|
||||
|
||||
## A `dict` from the previous example
|
||||
|
||||
In the previous example, we where returning a `dict` from our dependency ("dependable"):
|
||||
|
||||
```Python hl_lines="7"
|
||||
{!./src/dependencies/tutorial001.py!}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
We can do better...
|
||||
|
||||
## What makes a dependency
|
||||
|
||||
Up to now you have seen dependencies declared as functions.
|
||||
|
||||
But that's not the only way to declare dependencies (although it would probably be the more common).
|
||||
|
||||
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:
|
||||
|
||||
```Python
|
||||
something()
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```Python
|
||||
something(some_argument, some_keyword_argument="foo")
|
||||
```
|
||||
|
||||
then it is a "callable".
|
||||
|
||||
## Classes as dependencies
|
||||
|
||||
You might notice that to create an instance of a Python class, you use that same syntax.
|
||||
|
||||
So, a Python class is also a **callable**.
|
||||
|
||||
Then, in **FastAPI**, you could use a Python class as a dependency.
|
||||
|
||||
What FastAPI actually checks is that it is a "callable" (function, class or anything else) and the parameters defined.
|
||||
|
||||
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.
|
||||
|
||||
Then, we can change the dependency "dependable" `common_parameters` from above to the class `CommonQueryParameters`:
|
||||
|
||||
```Python hl_lines="9 10 11 12 13"
|
||||
{!./src/dependencies/tutorial002.py!}
|
||||
```
|
||||
|
||||
Pay attention to the `__init__` method used to create the instance of the class:
|
||||
|
||||
```Python hl_lines="10"
|
||||
{!./src/dependencies/tutorial002.py!}
|
||||
```
|
||||
|
||||
...it has the same parameters as our previous `common_parameters`:
|
||||
|
||||
```Python hl_lines="6"
|
||||
{!./src/dependencies/tutorial001.py!}
|
||||
```
|
||||
|
||||
Those parameters are what **FastAPI** will use to "solve" the dependency.
|
||||
|
||||
In both cases, it will have:
|
||||
|
||||
* an optional `q` query parameter.
|
||||
* a `skip` query parameter, with a default of `0`.
|
||||
* a `limit` query parameter, with a default of `100`.
|
||||
|
||||
In both cases the data will be converted, validated, documented on the OpenAPI schema, etc.
|
||||
|
||||
## Use it
|
||||
|
||||
Now you can declare your dependency using this class.
|
||||
|
||||
And as when **FastAPI** calls that class the value that will be passed as `commons` to your function will be an "instance" of the class, you can declare that parameter `commons` to be of type of the class, `CommonQueryParams`.
|
||||
|
||||
```Python hl_lines="17"
|
||||
{!./src/dependencies/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Type annotation vs `Depends`
|
||||
|
||||
In the code above, you are declaring `commons` as:
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams = Depends(CommonQueryParams)
|
||||
```
|
||||
|
||||
The last `CommonQueryParams`, in:
|
||||
|
||||
```Python
|
||||
... = Depends(CommonQueryParams)
|
||||
```
|
||||
|
||||
...is what **FastAPI** will actually use to know what is the dependency.
|
||||
|
||||
From it is that FastAPI will extract the declared parameters and that is what FastAPI will actually call.
|
||||
|
||||
---
|
||||
|
||||
In this case, the first `CommonQueryParams`, in:
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams ...
|
||||
```
|
||||
|
||||
...doesn't have any special meaning for **FastAPI**. FastAPI won't use it for data conversion, validation, etc. (as it is using the `= Depends(CommonQueryParams)` for that).
|
||||
|
||||
You could actually write just:
|
||||
|
||||
```Python
|
||||
commons = Depends(CommonQueryParams)
|
||||
```
|
||||
|
||||
..as in:
|
||||
|
||||
```Python hl_lines="17"
|
||||
{!./src/dependencies/tutorial003.py!}
|
||||
```
|
||||
|
||||
But declaring the type is encouraged as that way your editor will know what will be passed as the parameter `commons`, and then it can help you with code completion, type checks, etc:
|
||||
|
||||
<img src="/img/tutorial/dependencies/image02.png">
|
||||
|
||||
## Shortcut
|
||||
|
||||
But you see that we are having some code repetition here, writing `CommonQueryParams` twice:
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams = Depends(CommonQueryParams)
|
||||
```
|
||||
|
||||
**FastAPI** provides a shortcut for these cases, in where the dependency is *specifically* a class that **FastAPI** will "call" to create an instance of the class itself.
|
||||
|
||||
For those specific cases, you can do the following:
|
||||
|
||||
Instead of writing:
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams = Depends(CommonQueryParams)
|
||||
```
|
||||
|
||||
...you write:
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams = Depends()
|
||||
```
|
||||
|
||||
So, you can declare the dependency as the type of the variable, and use `Depends()` as the "default" value, without any parameter, instead of having to write the full class *again* inside of `Depends(CommonQueryParams)`.
|
||||
|
||||
So, the same example would look like:
|
||||
|
||||
```Python hl_lines="17"
|
||||
{!./src/dependencies/tutorial004.py!}
|
||||
```
|
||||
|
||||
...and **FastAPI** will know what to do.
|
||||
|
||||
!!! tip
|
||||
If all that seems more confusing than helpful, disregard it, you don't *need* it.
|
||||
|
||||
It is just a shortcut. Because **FastAPI** cares about helping you minimize code repetition.
|
||||
@@ -22,7 +22,7 @@ That's it.
|
||||
|
||||
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" (the `@app.get("/some-path")`).
|
||||
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.
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
Before diving deeper into the **Dependency Injection** system, let's upgrade the previous example.
|
||||
|
||||
## A `dict` from the previous example
|
||||
|
||||
In the previous example, we where returning a `dict` from our dependency ("dependable"):
|
||||
|
||||
```Python hl_lines="7"
|
||||
{!./src/dependencies/tutorial001.py!}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Create a Pydantic model
|
||||
|
||||
But we are already using Pydantic models in other places and we have already seen all the benefits.
|
||||
|
||||
Let's use them here too.
|
||||
|
||||
Create a model for the common parameters (and don't pay attention to the rest, for now):
|
||||
|
||||
```Python hl_lines="11 12 13 14"
|
||||
{!./src/dependencies/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Return a Pydantic model
|
||||
|
||||
Now we can return a Pydantic model from the dependency ("dependable") with the same data as the dict before:
|
||||
|
||||
```Python hl_lines="18"
|
||||
{!./src/dependencies/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Declare the Pydantic model
|
||||
|
||||
We can now come back to the path operation function and declare the type of the `commons` parameter to be that Pydantic model:
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams = Depends(common_parameters)
|
||||
```
|
||||
|
||||
It won't be interpreted as a JSON request `Body` because we are using `Depends`:
|
||||
|
||||
```Python hl_lines="22"
|
||||
{!./src/dependencies/tutorial002.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
In the case of dependencies with `Depends`, the type of the parameter is only to get editor support.
|
||||
|
||||
Your dependencies won't be enforced to return a specific type of data.
|
||||
|
||||
## Use the Pydantic model
|
||||
|
||||
And now we can use that model in our code, with all the lovable editor support:
|
||||
|
||||
```Python hl_lines="24 25 26"
|
||||
{!./src/dependencies/tutorial002.py!}
|
||||
```
|
||||
|
||||
<img src="/img/tutorial/dependencies/image02.png">
|
||||
|
||||
## Trees of hierarchical dependencies
|
||||
|
||||
With the **Dependency Injection** system you can build arbitrarily deep trees of hierarchical dependencies (also known as dependency graphs) by having dependencies that also have dependencies themselves.
|
||||
|
||||
You will see examples of these dependency trees in the next chapters about security.
|
||||
|
||||
## Recap
|
||||
|
||||
By using Pydantic models in your dependencies too you can keep all the editor support that **FastAPI** is designed to support.
|
||||
60
docs/tutorial/dependencies/sub-dependencies.md
Normal file
@@ -0,0 +1,60 @@
|
||||
You can create dependencies that have sub-dependencies.
|
||||
|
||||
They can be as "deep" as you need them to be.
|
||||
|
||||
**FastAPI** will take care of solving them.
|
||||
|
||||
### First dependency "dependable"
|
||||
|
||||
You could create a first dependency ("dependable") like:
|
||||
|
||||
```Python hl_lines="6 7"
|
||||
{!./src/dependencies/tutorial005.py!}
|
||||
```
|
||||
It declares an optional query parameter `q` as a `str`, and then it just returns it.
|
||||
|
||||
This is quite simple (not very useful), but will help us focus on how the sub-dependencies work.
|
||||
|
||||
### Second dependency, "dependable" and "dependant"
|
||||
|
||||
Then you can create another dependency function (a "dependable") that at the same time declares a dependency of its own (so it is a "dependant" too):
|
||||
|
||||
```Python hl_lines="11"
|
||||
{!./src/dependencies/tutorial005.py!}
|
||||
```
|
||||
|
||||
Let's focus on the parameters declared:
|
||||
|
||||
* Even though this function is a dependency ("dependable") itself, it also declares another dependency (it "depends" on something else).
|
||||
* It depends on the `query_extractor`, and assigns the value returned by it to the parameter `q`.
|
||||
* It also declares an optional `last_query` cookie, as a `str`.
|
||||
* Let's imagine that if the user didn't provide any query `q`, we just use the last query used, that we had saved to a cookie before.
|
||||
|
||||
### Use the dependency
|
||||
|
||||
Then we can use the dependency with:
|
||||
|
||||
```Python hl_lines="19"
|
||||
{!./src/dependencies/tutorial005.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
Notice that we are only declaring one dependency in the path operation function, the `query_or_cookie_extractor`.
|
||||
|
||||
But **FastAPI** will know that it has to solve `query_extractor` first, to pass the results of that to `query_or_cookie_extractor` while calling it.
|
||||
|
||||
|
||||
## Recap
|
||||
|
||||
Apart from all the fancy words used here, the **Dependency Injection** system is quite simple.
|
||||
|
||||
Just functions that look the same as the path operation functions.
|
||||
|
||||
But still, it is very powerful, and allows you to declare arbitrarily deeply nested dependency "graphs" (trees).
|
||||
|
||||
!!! tip
|
||||
All this might not seem as useful with these simple examples.
|
||||
|
||||
But you will see how useful it is in the chapters about **security**.
|
||||
|
||||
And you will also see the amounts of code it will save you.
|
||||
64
docs/tutorial/extra-data-types.md
Normal file
@@ -0,0 +1,64 @@
|
||||
Up to now, you have been using common data types, like:
|
||||
|
||||
* `int`
|
||||
* `float`
|
||||
* `str`
|
||||
* `bool`
|
||||
|
||||
But you can also use more complex data types.
|
||||
|
||||
And you will still have the same features as seen up to now:
|
||||
|
||||
* Great editor support.
|
||||
* Data conversion from incoming requests.
|
||||
* Data conversion for response data.
|
||||
* Data validation.
|
||||
* Automatic annotation and documentation.
|
||||
|
||||
## Other data types
|
||||
|
||||
Here are some of the additional data types you can use:
|
||||
|
||||
* `UUID`:
|
||||
* A standard "Universally Unique Identifier", common as an ID in many databases and systems.
|
||||
* In requests and responses will be represented as a `str`.
|
||||
* `datetime.datetime`:
|
||||
* A Python `datetime.datetime`.
|
||||
* In requests and responses will be represented as a `str` in ISO 8601 format, like: `2008-09-15T15:53:00+05:00`.
|
||||
* `datetime.date`:
|
||||
* Python `datetime.date`.
|
||||
* In requests and responses will be represented as a `str` in ISO 8601 format, like: `2008-09-15`.
|
||||
* `datetime.time`:
|
||||
* A Python `datetime.time`.
|
||||
* In requests and responses will be represented as a `str` in ISO 8601 format, like: `14:23:55.003`.
|
||||
* `datetime.timedelta`:
|
||||
* A Python `datetime.timedelta`.
|
||||
* In requests and responses will be represented as a `float` of total seconds.
|
||||
* Pydantic also allows representing it as a "ISO 8601 time diff encoding", <a href="https://pydantic-docs.helpmanual.io/#json-serialisation" target="_blank">see the docs for more info</a>.
|
||||
* `frozenset`:
|
||||
* In requests and responses, treated the same as a `set`:
|
||||
* In requests, a list will be read, eliminating duplicates and converting it to a `set`.
|
||||
* In responses, the `set` will be converted to a `list`.
|
||||
* The generated schema will specify that the `set` values are unique (using JSON Schema's `uniqueItems`).
|
||||
* `bytes`:
|
||||
* Standard Python `bytes`.
|
||||
* In requests and responses will be treated as `str`.
|
||||
* The generated schema will specify that it's a `str` with `binary` "format".
|
||||
* `Decimal`:
|
||||
* Standard Python `Decimal`.
|
||||
* In requests and responses, handled the same as a `float`.
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
Here's an example path operation with parameters using some of the above types.
|
||||
|
||||
```Python hl_lines="1 2 11 12 13 14 15"
|
||||
{!./src/extra_data_types/tutorial001.py!}
|
||||
```
|
||||
|
||||
Note that the parameters inside the function have their natural data type, and you can, for example, perform normal date manipulations, like:
|
||||
|
||||
```Python hl_lines="17 18"
|
||||
{!./src/extra_data_types/tutorial001.py!}
|
||||
```
|
||||
@@ -7,7 +7,9 @@ This is especially the case for user models, because:
|
||||
* The **database model** would probably need to have a hashed password.
|
||||
|
||||
!!! danger
|
||||
Never store user's plaintext passwords. Always store a secure hash that you can then verify.
|
||||
Never store user's plaintext passwords. Always store a "secure hash" that you can then verify.
|
||||
|
||||
If you don't know, you will learn what a "password hash" is in the <a href="/tutorial/security/simple-oauth2/#password-hashing" target="_blank">security chapters</a>.
|
||||
|
||||
## Multiple models
|
||||
|
||||
@@ -17,6 +19,39 @@ Here's a general idea of how the models could look like with their password fiel
|
||||
{!./src/extra_models/tutorial001.py!}
|
||||
```
|
||||
|
||||
#### About `**user_dict`
|
||||
|
||||
`UserInDB(**user_dict)` means:
|
||||
|
||||
Pass the keys and values of the `user_dict` directly as key-value arguments, equivalent to:
|
||||
|
||||
```Python
|
||||
UserInDB(
|
||||
username = user_dict["username"],
|
||||
password = user_dict["password"],
|
||||
email = user_dict["email"],
|
||||
full_name = user_dict["full_name"],
|
||||
)
|
||||
```
|
||||
|
||||
And then adding the extra `hashed_password=hashed_password`, like in:
|
||||
|
||||
```Python
|
||||
UserInDB(**user_in.dict(), hashed_password=hashed_password)
|
||||
```
|
||||
|
||||
...ends up being like:
|
||||
|
||||
```Python
|
||||
UserInDB(
|
||||
username = user_dict["username"],
|
||||
password = user_dict["password"],
|
||||
email = user_dict["email"],
|
||||
full_name = user_dict["full_name"],
|
||||
hashed_password = hashed_password,
|
||||
)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
The supporting additional functions are just to demo a possible flow of the data, but they of course are not providing any real security.
|
||||
|
||||
|
||||
@@ -57,6 +57,32 @@ You will see the alternative automatic documentation (provided by <a href="https
|
||||
|
||||

|
||||
|
||||
### OpenAPI
|
||||
|
||||
**FastAPI** generates a "schema" with all your API using the **OpenAPI** standard for defining APIs.
|
||||
|
||||
#### "Schema"
|
||||
|
||||
A "schema" is a definition or description of something. Not the code that implements it, but just the abstract description.
|
||||
|
||||
#### API "schema"
|
||||
|
||||
In this case, OpenAPI is a specification that dictates how to define a schema of your API.
|
||||
|
||||
This OpenAPI schema would include your API paths, the posible parameters they take, etc.
|
||||
|
||||
#### Data "schema"
|
||||
|
||||
The term "schema" might also refer to the shape of some data, like a JSON content.
|
||||
|
||||
In that case, it would mean the JSON attributes, and data types they have, etc.
|
||||
|
||||
#### OpenAPI and JSON Schema
|
||||
|
||||
OpenAPI defines an API schema for your API. And that schema includes definitions (or "schemas") of the data sent and received by your API using **JSON Schema**, the standard for JSON data schemas.
|
||||
|
||||
#### Check it
|
||||
|
||||
If you are curious about how the raw OpenAPI schema looks like, it is just an automatically generated JSON with the descriptions of all your API.
|
||||
|
||||
You can see it directly at: <a href="http://127.0.0.1:8000/openapi.json" target="_blank">http://127.0.0.1:8000/openapi.json</a>.
|
||||
@@ -84,6 +110,14 @@ It will show a JSON starting with something like:
|
||||
...
|
||||
```
|
||||
|
||||
#### What for?
|
||||
|
||||
This OpenAPI schema is what powers the 2 interactive documentation systems included.
|
||||
|
||||
And there are dozens of alternatives, all based on OpenAPI. You could easily add any of those alternatives to your application built with **FastAPI**.
|
||||
|
||||
You could also use it to generate code automatically, for clients that communicate with your API. For example, frontend, mobile or IoT applications.
|
||||
|
||||
## Recap, step by step
|
||||
|
||||
### Step 1: import `FastAPI`
|
||||
@@ -148,7 +182,7 @@ https://example.com/items/foo
|
||||
!!! info
|
||||
A "path" is also commonly called an "endpoint" or a "route".
|
||||
|
||||
Building an API, the "path" is the main way to separate "concerns" and functionalities.
|
||||
Building an API, the "path" is the main way to separate "concerns" and "resources".
|
||||
|
||||
#### Operation
|
||||
|
||||
@@ -172,7 +206,7 @@ In the HTTP protocol, you can communicate to each path using one (or more) of th
|
||||
|
||||
---
|
||||
|
||||
When building APIs, you normally use these specific HTTP methods to perform a specific operation.
|
||||
When building APIs, you normally use these specific HTTP methods to perform a specific action.
|
||||
|
||||
Normally you use:
|
||||
|
||||
@@ -183,7 +217,7 @@ Normally you use:
|
||||
|
||||
So, in OpenAPI, each of the HTTP methods is called an "operation".
|
||||
|
||||
We are going to call them "operations" too.
|
||||
We are going to call them "**operations**" too.
|
||||
|
||||
#### Define a path operation function
|
||||
|
||||
@@ -196,6 +230,17 @@ The `@app.get("/")` tells **FastAPI** that the function right below is in charge
|
||||
* the path `/`
|
||||
* using a <abbr title="an HTTP GET method"><code>get</code> operation</abbr>
|
||||
|
||||
!!! info "`@decorator` Info"
|
||||
That `@something` syntax in Python is called a "decorator".
|
||||
|
||||
You put it on top of a function. Like a pretty decorative hat (I guess that's where the term came from).
|
||||
|
||||
A "decorator" takes the function below and does something with it.
|
||||
|
||||
In our case, this decorator tells **FastAPI** that the function below corresponds to the **path** `/` with an **operation** `get`.
|
||||
|
||||
It is the "**path operation decorator**".
|
||||
|
||||
You can also use the other operations:
|
||||
|
||||
* `@app.post()`
|
||||
@@ -216,9 +261,15 @@ And the more exotic ones:
|
||||
|
||||
The information here is presented as a guideline, not a requirement.
|
||||
|
||||
For example, when using GraphQL you normally perform all the operations using only `post`.
|
||||
For example, when using GraphQL you normally perform all the actions using only `post`.
|
||||
|
||||
### Step 4: define the path operation function
|
||||
### Step 4: define the **path operation function**
|
||||
|
||||
This is our "**path operation function**":
|
||||
|
||||
* **path**: is `/`.
|
||||
* **operation**: is `get`.
|
||||
* **function**: is the function below the "decorator" (below `@app.get("/")`).
|
||||
|
||||
```Python hl_lines="7"
|
||||
{!./src/first_steps/tutorial001.py!}
|
||||
@@ -226,7 +277,7 @@ And the more exotic ones:
|
||||
|
||||
This is a Python function.
|
||||
|
||||
It will be called by FastAPI whenever it receives a request to the URL "`/`".
|
||||
It will be called by **FastAPI** whenever it receives a request to the URL "`/`" using `GET`.
|
||||
|
||||
In this case, it is an `async` function.
|
||||
|
||||
@@ -238,7 +289,8 @@ You could also define it as a normal function instead of `async def`:
|
||||
{!./src/first_steps/tutorial003.py!}
|
||||
```
|
||||
|
||||
To know the difference, read the section about [Concurrency and `async` / `await`](/async/).
|
||||
!!! note
|
||||
If you don't know the difference, 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>.
|
||||
|
||||
### Step 5: return the content
|
||||
|
||||
@@ -250,4 +302,13 @@ You can return a `dict`, `list`, singular values as `str`, `int`, etc.
|
||||
|
||||
You can also return Pydantic models (you'll see more about that later).
|
||||
|
||||
There are many other objects and models that will be automatically converted to JSON.
|
||||
There are many other objects and models that will be automatically converted to JSON (including ORMs, etc). Try using your favorite ones, it's highly probable that they are already supported.
|
||||
|
||||
|
||||
## Recap
|
||||
|
||||
* Import `FastAPI`.
|
||||
* Create an `app` instance.
|
||||
* Write a **path operation decorator** (like `@app.get("/")`).
|
||||
* Write a **path operation function** (like `def root(): ...` above).
|
||||
* Run the debugging server (like `uvicorn main:app --debug`).
|
||||
@@ -39,13 +39,13 @@ pip install fastapi[all]
|
||||
|
||||
This is what you would probably do once you want to deploy your application to production:
|
||||
|
||||
```bash
|
||||
```
|
||||
pip install fastapi
|
||||
```
|
||||
|
||||
Also install `uvicorn` to work as the server:
|
||||
|
||||
```bash
|
||||
```
|
||||
pip install uvicorn
|
||||
```
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ You can adapt it to any other NoSQL database like:
|
||||
* **ArangoDB**
|
||||
* **ElasticSearch**, etc.
|
||||
|
||||
!!! tip
|
||||
There is an official project generator with **FastAPI** and **Couchbase**, all based on **Docker**, including a frontend and more tools: <a href="https://github.com/tiangolo/full-stack-fastapi-couchbase" target="_blank">https://github.com/tiangolo/full-stack-fastapi-couchbase</a>
|
||||
|
||||
## Import Couchbase components
|
||||
|
||||
For now, don't pay attention to the rest, only the imports:
|
||||
@@ -49,7 +52,7 @@ This utility function will:
|
||||
* Set defaults for timeouts.
|
||||
* Return it.
|
||||
|
||||
```Python hl_lines="13 14 15 16 17 18 19 20"
|
||||
```Python hl_lines="13 14 15 16 17 18 19 20 21 22"
|
||||
{!./src/nosql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -61,7 +64,7 @@ As **Couchbase** "documents" are actually just "JSON objects", we can model them
|
||||
|
||||
First, let's create a `User` model:
|
||||
|
||||
```Python hl_lines="23 24 25 26 27"
|
||||
```Python hl_lines="25 26 27 28 29"
|
||||
{!./src/nosql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -75,7 +78,7 @@ This will have the data that is actually stored in the database.
|
||||
|
||||
We don't create it as a subclass of Pydantic's `BaseModel` but as a subclass of our own `User`, because it will have all the attributes in `User` plus a couple more:
|
||||
|
||||
```Python hl_lines="30 31 32"
|
||||
```Python hl_lines="32 33 34"
|
||||
{!./src/nosql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -96,7 +99,7 @@ Now create a function that will:
|
||||
|
||||
By creating a function that is only dedicated to getting your user from a `username` (or any other parameter) independent of your path operation function, you can more easily re-use it in multiple parts and also add <abbr title="Automated test, written in code, that checks if another piece of code is working correctly.">unit tests</abbr> for it:
|
||||
|
||||
```Python hl_lines="35 36 37 38 39 40 41"
|
||||
```Python hl_lines="37 38 39 40 41 42 43"
|
||||
{!./src/nosql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -131,7 +134,7 @@ UserInDB(username="johndoe", hashed_password="some_hash")
|
||||
|
||||
### Create the `FastAPI` app
|
||||
|
||||
```Python hl_lines="45"
|
||||
```Python hl_lines="47"
|
||||
{!./src/nosql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -141,7 +144,7 @@ As our code is calling Couchbase and we are not using the <a href="https://docs.
|
||||
|
||||
Also, Couchbase recommends not using a single `Bucket` object in multiple "<abbr title="A sequence of code being executed by the program, while at the same time, or at intervals, there can be others being executed too.">thread</abbr>s", so, we can get just get the bucket directly and pass it to our utility functions:
|
||||
|
||||
```Python hl_lines="48 49 50 51 52"
|
||||
```Python hl_lines="50 51 52 53 54"
|
||||
{!./src/nosql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ In this case, `item_id` is declared to be an `int`.
|
||||
!!! check
|
||||
This will give you editor support inside of your function, with error checks, completion, etc.
|
||||
|
||||
## Data "parsing"
|
||||
## Data <abbr title="also known as: serialization, parsing, marshalling">conversion</abbr>
|
||||
|
||||
If you run this example and open your browser at <a href="http://127.0.0.1:8000/items/3" target="_blank">http://127.0.0.1:8000/items/3</a>, you will see a response of:
|
||||
|
||||
|
||||
@@ -1,5 +1,167 @@
|
||||
Coming soon...
|
||||
Let's imagine that you have your **backend** API in some domain.
|
||||
|
||||
And you have a **frontend** in another domain or in a different path of the same domain (or in a mobile application).
|
||||
|
||||
And you want to have a way for the frontend to authenticate with the backend, using a **username** and **password**.
|
||||
|
||||
We can use **OAuth2** to build that with **FastAPI**.
|
||||
|
||||
But let's save you the time of reading the full long specification just to find those little pieces of information you need.
|
||||
|
||||
Let's use the tools provided by **FastAPI** to handle security.
|
||||
|
||||
## How it looks
|
||||
|
||||
But let's first just use the code and see how it works, and then we'll come back to understand what's happening.
|
||||
|
||||
## Create `main.py`
|
||||
|
||||
Copy the example in a file `main.py`:
|
||||
|
||||
```Python
|
||||
{!./src/security/tutorial002.py!}
|
||||
{!./src/security/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Run it
|
||||
|
||||
Run the example with:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --debug
|
||||
```
|
||||
|
||||
## Check it
|
||||
|
||||
Go to the interactive docs at: <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
|
||||
You will see something like this:
|
||||
|
||||
<img src="/img/tutorial/security/image01.png">
|
||||
|
||||
!!! check "Authorize button!"
|
||||
You already have a shinny new "Authorize" button.
|
||||
|
||||
And your path operation has a little lock in the top-right corner that you can click.
|
||||
|
||||
|
||||
And if you click it, you have a little authorization form to type a `username` and `password` (and other optional fields):
|
||||
|
||||
<img src="/img/tutorial/security/image02.png">
|
||||
|
||||
!!! note
|
||||
It doesn't matter what you type in the form, it won't work yet. But we'll get there.
|
||||
|
||||
This is of course not the frontend for the final users, but it's a great automatic tool to document interactively all your API.
|
||||
|
||||
It can be used by the frontend team (that can also be yourself).
|
||||
|
||||
It can be used by third party applications and systems.
|
||||
|
||||
And it can also be used by yourself, to debug, check and test the same application.
|
||||
|
||||
|
||||
## The `password` flow
|
||||
|
||||
Now let's go back a bit and understand what is all that.
|
||||
|
||||
The `password` "flow" is one of the ways ("flows") defined in OAuth2, to handle security and authentication.
|
||||
|
||||
OAuth2 was designed so that the backend or API could be independent of the server that authenticates the user.
|
||||
|
||||
But in this case, the same **FastAPI** application will handle the API and the authentication.
|
||||
|
||||
So, let's review it from that simplified point of view:
|
||||
|
||||
* The user types his `username` and `password` in the frontend, and hits `Enter`.
|
||||
* The frontend (running in the user's browser) sends that `username` and `password` to a specific URL in our API.
|
||||
* The API checks that `username` and `password`, and responds with a "token".
|
||||
* A "token" is just a string with some content that we can use later to verify this user.
|
||||
* Normally, a token is set to expire after some time.
|
||||
* So, the user will have to login again at some point later.
|
||||
* And if the token is stolen, the risk is less. It is not like a permanent key that will work forever.
|
||||
* The frontend stores that token temporarily somewhere.
|
||||
* The user clicks in the frontend to go to another section of the frontend web app.
|
||||
* The frontend needs to fetch some more data from the API.
|
||||
* But it needs authentication for that specific endpoint.
|
||||
* So, to authenticate with our API, it sends a header `Authorization` with a value of `Bearer ` plus the token.
|
||||
* If the token contains `foobar`, the content of the `Authorization` header would be: `Bearer foobar`.
|
||||
* Note that although the header is case-insensitive (`Authorization` is the same as `authorization`), the value is not. So, `bearer foobar` would not be valid. It has to be `Bearer foobar`.
|
||||
|
||||
## **FastAPI**'s `Security`
|
||||
|
||||
### Import it
|
||||
|
||||
The same way **FastAPI** provides a `Depends`, there is a `Security` that you can import:
|
||||
|
||||
```Python hl_lines="1"
|
||||
{!./src/security/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Use it
|
||||
|
||||
It is actually a subclass of `Depends`, and it has just one extra parameter that we'll see later.
|
||||
|
||||
But by using `Security` instead of `Depends`, **FastAPI** will know that it can use this dependency to define "security schemes" in OpenAPI.
|
||||
|
||||
```Python hl_lines="10"
|
||||
{!./src/security/tutorial001.py!}
|
||||
```
|
||||
|
||||
In this case, we have a `Security` definition (which at the same time is a dependency definition) that will provide a `str` that is assigned to the parameter `token`.
|
||||
|
||||
## **FastAPI**'s `OAuth2PasswordBearer`
|
||||
|
||||
**FastAPI** provides several tools, at different levels of abstraction, to implement these security features.
|
||||
|
||||
In this example we are going to use **OAuth2**, with the **Password** flow, using a **Bearer** token.
|
||||
|
||||
|
||||
!!! info
|
||||
A "bearer" token is not the only option.
|
||||
|
||||
But it's the best one for our use case.
|
||||
|
||||
And it might be the best for most use cases, unless you are an OAuth2 expert and know exactly why there's another option that suits better your needs.
|
||||
|
||||
In that case, **FastAPI** also provides you with the tools to build it.
|
||||
|
||||
`OAuth2PasswordBearer` is a class that we create passing a parameter of the URL in where the client (the frontend running in the user's browser) can use to send the `username` and `password` and get a token.
|
||||
|
||||
```Python hl_lines="6"
|
||||
{!./src/security/tutorial001.py!}
|
||||
```
|
||||
|
||||
It doesn't create that endpoint / path operation, but declares that that URL is the one that the client should use to get the token. That information is used in OpenAPI, and then in the interactive API documentation systems.
|
||||
|
||||
!!! info
|
||||
If you are a very strict "Pythonista" you might dislike the style of the parameter name `tokenUrl` instead of `token_url`.
|
||||
|
||||
That's because it is using the same name as in the OpenAPI spec. So that if you need to investigate more about any of these security schemes you can just copy and paste it to find more information about it.
|
||||
|
||||
The `oauth2_scheme` variable is an instance of `OAuth2PasswordBearer`, but it is also a "callable".
|
||||
|
||||
It could be called as:
|
||||
|
||||
```Python
|
||||
oauth2_scheme(some, parameters)
|
||||
```
|
||||
|
||||
So, it can be used with `Security` (as it could be used with `Depends`).
|
||||
|
||||
## What it does
|
||||
|
||||
It will go and look in the request for that `Authorization` header, check if the value is `Bearer ` plus some token, and will return the token as a `str`.
|
||||
|
||||
If it doesn't see an `Authorization` header, or the value doesn't have a `Bearer ` token, it will respond with a 403 status code error (`FORBIDDEN`) directly.
|
||||
|
||||
You don't even have to check if the token exists to return an error. You can be sure that if your function is executed, it will have a `str` in that token.
|
||||
|
||||
You can try it already in the interactive docs:
|
||||
|
||||
<img src="/img/tutorial/security/image03.png">
|
||||
|
||||
We are not verifying the validity of the token yet, but that's a start already.
|
||||
|
||||
## Recap
|
||||
|
||||
So, in just 3 or 4 extra lines, you already have some primitive form of security.
|
||||
|
||||
120
docs/tutorial/security/get-current-user.md
Normal file
@@ -0,0 +1,120 @@
|
||||
In the previous chapter the security system (which is based on the dependency injection system) was giving the path operation function a `token` as a `str`:
|
||||
|
||||
```Python hl_lines="10"
|
||||
{!./src/security/tutorial001.py!}
|
||||
```
|
||||
|
||||
But that is still not that useful.
|
||||
|
||||
Let's make it give us the current user.
|
||||
|
||||
## Create a user model
|
||||
|
||||
First, let's create a Pydantic user model.
|
||||
|
||||
The same way we use Pydantic to declare bodies, we can use it anywhere else:
|
||||
|
||||
```Python hl_lines="5 12 13 14 15 16"
|
||||
{!./src/security/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Create a `get_current_user` dependency
|
||||
|
||||
Let's create a dependency `get_current_user`.
|
||||
|
||||
Remember that dependencies can have sub-dependencies?
|
||||
|
||||
And remember that `Security` is based on `Depends`?
|
||||
|
||||
So, we can have sub-dependencies using `Security` too.
|
||||
|
||||
`get_current_user` will have a `Security` dependency with the same `oauth2_scheme` we created before.
|
||||
|
||||
The same as we were doing before in the path operation direclty, our new dependency will receive a `token` as a `str` from the `Security` dependency:
|
||||
|
||||
```Python hl_lines="25"
|
||||
{!./src/security/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Get the user
|
||||
|
||||
`get_current_user` will use a (fake) utility function we created, that takes a token as a `str` and returns our Pydantic `User` model:
|
||||
|
||||
```Python hl_lines="19 20 21 22 26 27"
|
||||
{!./src/security/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Inject the current user
|
||||
|
||||
So now we can use the same `Depends` with our `get_current_user` in the path operation:
|
||||
|
||||
```Python hl_lines="31"
|
||||
{!./src/security/tutorial002.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
Here you could actually use `Security` instead of depends too.
|
||||
|
||||
But it is not required.
|
||||
|
||||
The key point where you should use `Security` is when passing an instance of `OAuth2PasswordBearer`.
|
||||
|
||||
Because **FastAPI** will use the fact that you are using `Security` and that you are passing an instance of that class `OAuth2PasswordBearer` (that inherits from `SecurityBase`) to create all the security definitions in OpenAPI.
|
||||
|
||||
Notice that we declare the type of `current_user` as the Pydantic model `User`.
|
||||
|
||||
This will help us inside of the function with all the completion and type checks.
|
||||
|
||||
!!! tip
|
||||
You might remember that request bodies are also declared with Pydantic models.
|
||||
|
||||
Here **FastAPI** won't get confused because you are using `Depends` or `Security`.
|
||||
|
||||
!!! check
|
||||
The way this dependency system is designed allows us to have different dependencies (different "dependables") that all return a `User` model.
|
||||
|
||||
We are not restricted to having only one dependency that can return that type of data.
|
||||
|
||||
|
||||
## Other models
|
||||
|
||||
You can now get the current user directly in the path operation functions and deal with the security mechanisms at the **Dependency Injection** level, using `Security`.
|
||||
|
||||
And you can use any model or data for the security requirements (in this case, a Pydantic model `User`).
|
||||
|
||||
But you are not restricted to using some specific data model, class or type.
|
||||
|
||||
Do you want to have an `id` and `email` and not have any `username` in your model? Sure. You can use these same tools.
|
||||
|
||||
Do you want to just have a `str`? Or just a `dict`? Or a database class model instance directly? It all works the same way.
|
||||
|
||||
|
||||
## Code size
|
||||
|
||||
This example might seem verbose. Have in mind that we are mixing security, data models utility functions and path operations in the same file.
|
||||
|
||||
But here's the key point.
|
||||
|
||||
The security and dependency injection stuff is written once.
|
||||
|
||||
And you can make it as complex as you want. And still, have it written only once, in a single place.
|
||||
|
||||
But you can have thousands of endpoints (path operations) using the same security system.
|
||||
|
||||
And all of them (or any portion of them that you want) can take the advantage of re-using these dependencies or any other dependencies you create.
|
||||
|
||||
And all these thousands of path operations can be as small as 3 lines:
|
||||
|
||||
```Python hl_lines="30 31 32"
|
||||
{!./src/security/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Recap
|
||||
|
||||
You can now get the current user directly in your path operation function.
|
||||
|
||||
We are already halfway there.
|
||||
|
||||
We just need to add a path operation for the user / client to actually send the `username` and `password`.
|
||||
|
||||
That comes next.
|
||||
@@ -1,5 +1,93 @@
|
||||
Coming soon...
|
||||
There are many ways to handle security, authentication and autorization.
|
||||
|
||||
```Python
|
||||
{!./src/security/tutorial001.py!}
|
||||
```
|
||||
And it normally is a complex and "difficult" topic.
|
||||
|
||||
In many frameworks and systems just handling security and authentication takes a big amount of effort and code (in many cases it can be 50% or more of all the code written).
|
||||
|
||||
**FastAPI** provides several tools to help you deal with **Security** easily, rapidly, in a standard way, without having to study and learn all the security specifications.
|
||||
|
||||
But first, let's check some small concepts.
|
||||
|
||||
## In a hurry?
|
||||
|
||||
If you don't care about any of these terms and you just need to add security with authentication based on username and password *right now*, skip to the next chapters.
|
||||
|
||||
## OAuth2
|
||||
|
||||
OAuth2 is a specification that defines several ways to handle authentication and autorization.
|
||||
|
||||
It is quite an extensive especification and covers several complex use cases.
|
||||
|
||||
It includes ways to authenticate using a "third party".
|
||||
|
||||
That's what all the system with "login with Facebook, Google, Twitter, GitHub" use underneath.
|
||||
|
||||
### OAuth 1
|
||||
|
||||
There was an OAuth 1, which is very different from OAuth2, and more complex, as it included directly specifications on how to encrypt the communication.
|
||||
|
||||
It is not very popular or used nowadays.
|
||||
|
||||
OAuth2 doesn't specify how to encrypt the communication, it expects you to have your application served with HTTPS.
|
||||
|
||||
!!! tip
|
||||
In the section about **deployment** you will see how to set up HTTPS for free, using Traefik and Let's Encrypt.
|
||||
|
||||
|
||||
## OpenID Connect
|
||||
|
||||
OpenID Connect is another specification, based on **OAuth2**.
|
||||
|
||||
It just extends OAuth2 specifying some things that are relatively ambiguous in OAuth2, to try to make it more interoperable.
|
||||
|
||||
For example, Google login used OpenID Connect (which underneath uses OAuth2).
|
||||
|
||||
But Facebook login doesn't support OpenID Connect. It has its own flavor of OAuth2.
|
||||
|
||||
### OpenID (not "OpenID Connect")
|
||||
|
||||
There was also an "OpenID" specification. That tried to solve the same thing as **OpenID Connect**, but was not based on OAuth2.
|
||||
|
||||
So, it was a complete additional system.
|
||||
|
||||
It is not very popular or used nowadays.
|
||||
|
||||
## OpenAPI
|
||||
|
||||
OpenAPI (previously known as Swagger) is the open specification for building APIs (now part of the Linux Foundation).
|
||||
|
||||
**FastAPI** is based on **OpenAPI**.
|
||||
|
||||
That's what makes it possible to have multiple automatic interactive documentation interfaces, code generation, etc.
|
||||
|
||||
OpenAPI has a way to define multiple security "schemes".
|
||||
|
||||
By using them, you can take advantage of all these standard-based tools, including these interactive documentation systems.
|
||||
|
||||
OpenAPI defines the following security schemes:
|
||||
|
||||
* `apiKey`: an application specific key that can come from:
|
||||
* A query parameter.
|
||||
* A header.
|
||||
* A cookie.
|
||||
* `http`: standard HTTP authentication systems, including:
|
||||
* `bearer`: a header `Authorization` with a value of `Bearer ` plus a token. This is inherited from OAuth2.
|
||||
* HTTP Basic authentication.
|
||||
* HTTP Digest, etc.
|
||||
* `oauth2`: all the OAuth2 ways to handle security (called "flows").
|
||||
* Several of these flows are appropriate for delegating the authentication to a third party (like Google, Facebook, Twitter, GitHub, etc):
|
||||
* `implicit`
|
||||
* `clientCredentials`
|
||||
* `authorizationCode`
|
||||
* But there is one specific "flow" that can be perfectly used for handling authentication in the same application directly:
|
||||
* `password`: some next chapters will cover examples of this.
|
||||
* `openIdConnect`: has a way to define how to discover OAuth2 authentication data automatically.
|
||||
* This automatic discovery is what is defined in the OpenID Connect specification.
|
||||
|
||||
## **FastAPI** utilities
|
||||
|
||||
FastAPI provides several tools for each of these security schemes in the `fastapi.security` module, to simplify using these security mechanisms.
|
||||
|
||||
In the next chapters you will see how to add security to your API in a very simple way, using the tools provided by **FastAPI**.
|
||||
|
||||
And you will also see how it gets automatically integrated into the interactive documentation system.
|
||||
|
||||
@@ -1,5 +1,207 @@
|
||||
Coming soon...
|
||||
Now that we have all the security flow, let's make the application actually secure, using JWT tokens and secure password hashing.
|
||||
|
||||
```Python
|
||||
This code is something you can actually use in your application, save the password hashes in your database, etc.
|
||||
|
||||
We are going to start from where we left in the previous chapter and increment it.
|
||||
|
||||
## About JWT
|
||||
|
||||
JWT means "JSON Web Tokens".
|
||||
|
||||
It's a standard to codify a JSON object in a long string.
|
||||
|
||||
It is not encrypted, so, anyone could recover the information from the contents.
|
||||
|
||||
But it's signed. So, when you receive a token that you emitted, you can verify that you actually emitted it.
|
||||
|
||||
That way, you can create a token with an expiration of, let's say, 1 week, and then, after a week, when the user comes back with the token, you know he's still signed into your system.
|
||||
|
||||
And after a week, the token will be expired. And if the user (or a third party) tried to modify the token to change the expiration, you would be able to discover it, because the signature would not match.
|
||||
|
||||
If you want to play with JWT tokens and see how they work, check <a href="https://jwt.io/" target="_blank">https://jwt.io</a>.
|
||||
|
||||
## Install `PyJWT`
|
||||
|
||||
We need to install `PyJWT` to generate and verity the JWT tokens in Python:
|
||||
|
||||
```bash
|
||||
pip install pyjwt
|
||||
```
|
||||
|
||||
## Password hashing
|
||||
|
||||
"Hashing" means converting some content (a password in this case) into a sequence of bytes (just a string) that look like gibberish.
|
||||
|
||||
Whenever you pass exactly the same content (exactly the same password) you get exactly the same gibberish.
|
||||
|
||||
But you cannot convert from the gibberish back to the password.
|
||||
|
||||
### What for?
|
||||
|
||||
If your database is stolen, the thief won't have your users' plaintext passwords, only the hashes.
|
||||
|
||||
So, the thief won't be able to try to use that password in another system (as many users use the same password everywhere, this would be dangerous).
|
||||
|
||||
## Install `passlib`
|
||||
|
||||
PassLib is a great Python package to handle password hashes.
|
||||
|
||||
It supports many secure hashing algorithms, and utilities to work with them.
|
||||
|
||||
The recommended algorithm is "Bcrypt".
|
||||
|
||||
So, install PassLib with Bcrypt:
|
||||
|
||||
```bash
|
||||
pip install passlib[bcrypt]
|
||||
```
|
||||
|
||||
!!! tip
|
||||
With `passlib`, you could even configure it to be able to read passwords created by **Django** (among many others).
|
||||
|
||||
So, you would be able to, for example, share the same data from a Django application in a database with a FastAPI application. Or gradually migrate a Django application using the same database.
|
||||
|
||||
|
||||
## Hash and verify the passwords
|
||||
|
||||
Import the tools we need from `passlib`.
|
||||
|
||||
Create a PassLib "context". This is what will be used to hash and verify passwords.
|
||||
|
||||
!!! tip
|
||||
The PassLib context also has functionality to use different hashing algorithms, deprecate old ones, but allow verifying them, etc.
|
||||
|
||||
For example, you could use it to read and verify passwords generated by another system (like Django) but hash any new passwords with a different algorithm like Bcrypt.
|
||||
|
||||
And be compatible with all of them at the same time.
|
||||
|
||||
Create a utility function to hash a password coming from the user.
|
||||
|
||||
And another utility to verify if a received password matches the hash stored.
|
||||
|
||||
And another one to authenticate and return a user.
|
||||
|
||||
```Python hl_lines="7 51 58 59 62 63 72 73 74 75 76 77 78"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
!!! note
|
||||
If you check the new (fake) database `fake_users_db`, you will see how the hashed password looks like now: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`.
|
||||
|
||||
## Handle JWT tokens
|
||||
|
||||
Import the modules installed.
|
||||
|
||||
Create a random secret key that will be used to sign the JWT tokens.
|
||||
|
||||
To generate a secure random secret, key use the command:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
And copy the output to the variable `SECRET_KEY` (don't use the one in the example).
|
||||
|
||||
Create a variable `ALGORITHM` with the algorithm used to sign the JWT token and set it to `"HS256"`.
|
||||
|
||||
And another one for the `TOKEN_SUBJECT`, and set it to, for example, `"access"`.
|
||||
|
||||
Create a variable for the expiration of the token.
|
||||
|
||||
Define a Pydantic Model that will be used in the token endpoint for the response.
|
||||
|
||||
Create a utility function to generate a new access token.
|
||||
|
||||
```Python hl_lines="3 6 14 15 16 17 31 32 33 81 82 83 84 85 86 87 88 89"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
## Update the dependencies
|
||||
|
||||
Update `get_current_user` to receive the same token as before, but this time, using JWT tokens.
|
||||
|
||||
Decode the received token, verify it, and return the current user.
|
||||
|
||||
If the token is invalid, return an HTTP error right away.
|
||||
|
||||
```Python hl_lines="92 93 94 95 96 97 98 99 100 101"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
## Update the `/token` path operation
|
||||
|
||||
Create a `timedelta` with the expiration time of the token.
|
||||
|
||||
Create a real JWT access token and return it.
|
||||
|
||||
```Python hl_lines="115 116 117 118 119"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
## Check it
|
||||
|
||||
Run the server and go to the docs: <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
|
||||
You'll see the user interface like:
|
||||
|
||||
<img src="/img/tutorial/security/image07.png">
|
||||
|
||||
Authorize the application the same way as before.
|
||||
|
||||
Using the credentials:
|
||||
|
||||
Username: `johndoe`
|
||||
Password: `secret`
|
||||
|
||||
!!! check
|
||||
Notice that nowhere in the code is the plaintext password "`secret`", we only have the hashed version.
|
||||
|
||||
<img src="/img/tutorial/security/image08.png">
|
||||
|
||||
Call the endpoint `/users/me`, you will get the response as:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"username": "johndoe",
|
||||
"email": "johndoe@example.com",
|
||||
"full_name": "John Doe",
|
||||
"disabled": false
|
||||
}
|
||||
```
|
||||
|
||||
<img src="/img/tutorial/security/image09.png">
|
||||
|
||||
If you open the developer tools, you could see how the data sent and received is just the token, the password is only sent in the first request to authenticate the user:
|
||||
|
||||
<img src="/img/tutorial/security/image10.png">
|
||||
|
||||
!!! note
|
||||
Notice the header `Authorization`, with a value that starts with `Bearer `.
|
||||
|
||||
## Advanced usage with `scopes`
|
||||
|
||||
We didn't use it in this example, but `Security` can receive a parameter `scopes`, as a list of strings.
|
||||
|
||||
It would describe the scopes required for a specific path operation, as different path operations might require different security scopes, even while using the same `OAuth2PasswordBearer` (or any of the other tools).
|
||||
|
||||
This only applies to OAuth2, and it might be, more or less, an advanced feature, but it is there, if you need to use it.
|
||||
|
||||
## Recap
|
||||
|
||||
This concludes our tour for the security features of **FastAPI**.
|
||||
|
||||
In almost any framework handling the security becomes a rather complex subject quite quickly.
|
||||
|
||||
Many packages that simplify it a lot have to make many compromises with the data model, database, and available features. And some of these packages that simplify things too much actually have security flaws underneath.
|
||||
|
||||
---
|
||||
|
||||
**FastAPI** doesn't make any compromise with any database, data model or tool.
|
||||
|
||||
It gives you all the flexibility to chose the ones that fit your project the best.
|
||||
|
||||
And you can use directly many well maintained and widely used packages like `passlib` and `pyjwt`, because **FastAPI** doesn't require any complex mechanisms to integrate external packages.
|
||||
|
||||
But it provides you the tools to simplify the process as much as possible without compromising flexibility, robustness or security.
|
||||
|
||||
And you can use secure, standard protocols like OAuth2 in a relatively simple way.
|
||||
|
||||
@@ -1,5 +1,238 @@
|
||||
Coming soon...
|
||||
Now let's build from the previous chapter and add the missing parts to have a complete security flow.
|
||||
|
||||
```Python
|
||||
## Get the `username` and `password`
|
||||
|
||||
We are going to use **FastAPI** security utilities to get the `username` and `password`.
|
||||
|
||||
OAuth2 specifies that when using the "password flow" (that we are using) the client / user must send a `username` and `password` fields as form data.
|
||||
|
||||
And the spec says that the fields have to be named like that. So `user-name` or `email` wouldn't work.
|
||||
|
||||
But don't worry, you can show it as you wish to your final users in the frontend.
|
||||
|
||||
And your database models can use any other names you want.
|
||||
|
||||
But for the login path operation, we need to use these names to be compatible with the spec (and be able to, for example, use the integrated API documentation system).
|
||||
|
||||
The spec also states that the `username` and `password` must be sent as form data (so, no JSON here).
|
||||
|
||||
### `scope`
|
||||
|
||||
The spec also says that the client can send another form field "`scope`".
|
||||
|
||||
The form field name is `scope` (in singular), but it is actually a long string with "scopes" separated by spaces.
|
||||
|
||||
Each "scope" is just a string (without spaces).
|
||||
|
||||
They are normally used to declare specific security permissions, for exampe:
|
||||
|
||||
* `"users:read"` or `"users:write"` are common examples.
|
||||
* `instagram_basic` is used by Facebook / Instagram.
|
||||
* `https://www.googleapis.com/auth/drive` is used by Google.
|
||||
|
||||
!!! info
|
||||
In OAuth2 a "scope" is just a string that declares a specific permision required.
|
||||
|
||||
It doesn't matter if it has other characters like `:`, or if it is a URL.
|
||||
|
||||
Those details are implementation specific.
|
||||
|
||||
For OAuth2 they are just strings.
|
||||
|
||||
|
||||
## Code to get the `username` and `password`
|
||||
|
||||
Now let's use the utilities provided by **FastAPI** to handle this.
|
||||
|
||||
### `OAuth2PasswordRequestForm`
|
||||
|
||||
First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depends` for the path `/token`:
|
||||
|
||||
```Python hl_lines="2 73"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
`OAuth2PasswordRequestForm` is a class dependency that declares a form body with:
|
||||
|
||||
* The `username`.
|
||||
* The `password`.
|
||||
* An optional `scope` field as a big string, composed of strings separated by spaces.
|
||||
* An optional `grant_type`.
|
||||
|
||||
!!! tip
|
||||
The OAuth2 spec actually *requires* a field `grant_type` with a fixed value of `password`, but `OAuth2PasswordRequestForm` doesn't enforce it.
|
||||
|
||||
If you need to enforce it, use `OAuth2PasswordRequestFormStrict` instead of `OAuth2PasswordRequestForm`.
|
||||
|
||||
* An optional `client_id` (we don't need it for our example).
|
||||
* An optional `client_secret` (we don't need it for our example).
|
||||
|
||||
### Use the form data
|
||||
|
||||
!!! tip
|
||||
The instance of the dependency class `OAuth2PasswordRequestForm` won't have an attribute `scope` with the long string separated by spaces, instead, it will have a `scopes` attribute with the actual list of strings for each scope sent.
|
||||
|
||||
We are not using `scopes` in this example, but the functionality is there if you need it.
|
||||
|
||||
Now, get the user data from the (fake) database, using the `username` from the form field.
|
||||
|
||||
If there is no such user, we return an error saying "incorrect username or password".
|
||||
|
||||
For the error, we use the exception `HTTPException` provided by Starlette directly:
|
||||
|
||||
```Python hl_lines="4 74 75 76"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
### Check the password
|
||||
|
||||
At this point we have a the user data from our database, but we haven't checked the password.
|
||||
|
||||
Let's put that data in the Pydantic `UserInDB` model first.
|
||||
|
||||
You should never save plaintext passwords, so, we'll use the (fake) password hashing system.
|
||||
|
||||
If the passwords don't match, we return the same error.
|
||||
|
||||
#### Password hashing
|
||||
|
||||
"Hashing" means: converting some content (a password in this case) into a sequence of bytes (just a string) that look like gibberish.
|
||||
|
||||
Whenever you pass exactly the same content (exactly the same password) you get exactly the same gibberish.
|
||||
|
||||
But you cannot convert from the gibberish back to the password.
|
||||
|
||||
##### What for?
|
||||
|
||||
If your database is stolen, the thief won't have your users' plaintext passwords, only the hashes.
|
||||
|
||||
So, the thief won't be able to try to use that password in another system (as many users use the same password everywhere, this would be dangerous).
|
||||
|
||||
```Python hl_lines="77 78 79 80"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
#### About `**user_dict`
|
||||
|
||||
`UserInDB(**user_dict)` means:
|
||||
|
||||
Pass the keys and values of the `user_dict` directly as key-value arguments, equivalent to:
|
||||
|
||||
```Python
|
||||
UserInDB(
|
||||
username = user_dict["username"],
|
||||
email = user_dict["email"],
|
||||
full_name = user_dict["full_name"],
|
||||
disabled = user_dict["disabled"],
|
||||
hashed_password = user_dict["hashed_password"],
|
||||
)
|
||||
```
|
||||
|
||||
## Return the token
|
||||
|
||||
The response of the `token` endpoint must be a JSON object.
|
||||
|
||||
It should have a `token_type`. In our case, as we are using "Bearer" tokens, the token type should be "`bearer`".
|
||||
|
||||
And it should have an `access_token`, with a string containing our access token.
|
||||
|
||||
For this simple example, we are going to just be completely insecure and return the same `username` as the token.
|
||||
|
||||
!!! tip
|
||||
In the next chapter, you will see a real secure implementation, with password hashing and JWT tokens.
|
||||
|
||||
But for now, let's focus on the specific details we need.
|
||||
|
||||
```Python hl_lines="82"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
## Update the dependencies
|
||||
|
||||
Now we are going to update our dependencies.
|
||||
|
||||
We want to get the `current_user` *only* if this user is active.
|
||||
|
||||
So, we create an additional dependency `get_current_active_user` that in turn uses `get_current_user` as a dependency.
|
||||
|
||||
Both of these dependencies will just return an HTTP error if the user doesn't exists, or if is inactive.
|
||||
|
||||
So, in our endpoint, we will only get a user if the user exists, was correctly authenticated, and is active:
|
||||
|
||||
```Python hl_lines="57 58 59 60 61 62 63 66 67 68 69 86"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
## See it in action
|
||||
|
||||
Open the interactive docs: <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
|
||||
### Authenticate
|
||||
|
||||
Click the "Authorize" button.
|
||||
|
||||
Use the credentials:
|
||||
|
||||
User: `johndoe`
|
||||
|
||||
Password: `secret`
|
||||
|
||||
<img src="/img/tutorial/security/image04.png">
|
||||
|
||||
After authenticating in the system, you will see it like:
|
||||
|
||||
<img src="/img/tutorial/security/image05.png">
|
||||
|
||||
### Get your own user data
|
||||
|
||||
Now use the operation `GET` with the path `/users/me`.
|
||||
|
||||
You will get your user's data, like:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"username": "johndoe",
|
||||
"email": "johndoe@example.com",
|
||||
"full_name": "John Doe",
|
||||
"disabled": false,
|
||||
"hashed_password": "fakehashedsecret"
|
||||
}
|
||||
```
|
||||
|
||||
<img src="/img/tutorial/security/image06.png">
|
||||
|
||||
If you click the lock icon and logout, and then try the same operation again, you will get an HTTP 403 error of:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"detail": "Not authenticated"
|
||||
}
|
||||
```
|
||||
|
||||
### Inactive user
|
||||
|
||||
Now try with an inactive user, authenticate with:
|
||||
|
||||
User: `alice`
|
||||
|
||||
Password: `secret2`
|
||||
|
||||
And try to use the operation `GET` with the path `/users/me`.
|
||||
|
||||
You will get an "inactive user" error, like:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"detail": "Inactive user"
|
||||
}
|
||||
```
|
||||
|
||||
## Recap
|
||||
|
||||
You now have the tools to implement a complete security system based on `username` and `password` for your API.
|
||||
|
||||
Using these tools, you can make the security system compatible with any database and with any user or data model.
|
||||
|
||||
The only detail missing is that it is not actually "secure" yet.
|
||||
|
||||
In the next chapter you'll see how to use a secure password hashing library and JWT tokens.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
**FastAPI** doesn't require you to use a SQL (relational) database.
|
||||
|
||||
But you can use relational database that you want.
|
||||
But you can use any relational database that you want.
|
||||
|
||||
Here we'll see an example using <a href="https://www.sqlalchemy.org/" target="_blank">SQLAlchemy</a>.
|
||||
|
||||
@@ -69,13 +69,13 @@ That way you don't have to declare them explicitly.
|
||||
|
||||
So, your models will behave very similarly to, for example, Flask-SQLAlchemy.
|
||||
|
||||
```Python hl_lines="15 16 17 18 19"
|
||||
```Python hl_lines="16 17 18 19 20"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Create the SQLAlchemy `Base` model
|
||||
|
||||
```Python hl_lines="22"
|
||||
```Python hl_lines="23"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -85,7 +85,7 @@ Now this is finally code specific to your app.
|
||||
|
||||
Here's a user model that will be a table in the database:
|
||||
|
||||
```Python hl_lines="25 26 27 28 29"
|
||||
```Python hl_lines="26 27 28 29 30"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -93,7 +93,7 @@ Here's a user model that will be a table in the database:
|
||||
|
||||
By creating a function that is only dedicated to getting your user from a `username` (or any other parameter) independent of your path operation function, you can more easily re-use it in multiple parts and also add <abbr title="Automated test, written in code, that checks if another piece of code is working correctly.">unit tests</abbr> for it:
|
||||
|
||||
```Python hl_lines="32 33"
|
||||
```Python hl_lines="33 34"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -103,7 +103,7 @@ Now, finally, here's the standard **FastAPI** code.
|
||||
|
||||
Create your app and path operation function:
|
||||
|
||||
```Python hl_lines="37 40 41 42 43"
|
||||
```Python hl_lines="38 41 42 43 44"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -131,7 +131,7 @@ user = get_user(username, db_session)
|
||||
|
||||
Then we should declare the path operation without `async def`, just with a normal `def`:
|
||||
|
||||
```Python hl_lines="41"
|
||||
```Python hl_lines="42"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.1.12"
|
||||
__version__ = "0.1.15"
|
||||
|
||||
from .applications import FastAPI
|
||||
from .routing import APIRouter
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import asyncio
|
||||
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 uuid import UUID
|
||||
|
||||
from fastapi import params
|
||||
from fastapi.dependencies.models import Dependant, SecurityRequirement
|
||||
@@ -16,7 +19,18 @@ from pydantic.utils import lenient_issubclass
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.requests import Request
|
||||
|
||||
param_supported_types = (str, int, float, bool)
|
||||
param_supported_types = (
|
||||
str,
|
||||
int,
|
||||
float,
|
||||
bool,
|
||||
UUID,
|
||||
date,
|
||||
datetime,
|
||||
time,
|
||||
timedelta,
|
||||
Decimal,
|
||||
)
|
||||
|
||||
|
||||
def get_sub_dependant(*, param: inspect.Parameter, path: str) -> Dependant:
|
||||
@@ -44,8 +58,6 @@ def get_flat_dependant(dependant: Dependant) -> Dependant:
|
||||
security_schemes=dependant.security_requirements.copy(),
|
||||
)
|
||||
for sub_dependant in dependant.dependencies:
|
||||
if sub_dependant is dependant:
|
||||
raise ValueError("recursion", dependant.dependencies)
|
||||
flat_sub = get_flat_dependant(sub_dependant)
|
||||
flat_dependant.path_params.extend(flat_sub.path_params)
|
||||
flat_dependant.query_params.extend(flat_sub.query_params)
|
||||
@@ -74,7 +86,7 @@ def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant:
|
||||
assert (
|
||||
lenient_issubclass(param.annotation, param_supported_types)
|
||||
or param.annotation == param.empty
|
||||
), f"Path params must be of type str, int, float or boot: {param}"
|
||||
), f"Path params must be of one of the supported types"
|
||||
param = signature_params[param_name]
|
||||
add_param_to_fields(
|
||||
param=param,
|
||||
@@ -85,7 +97,7 @@ def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant:
|
||||
elif (
|
||||
param.default == param.empty
|
||||
or param.default is None
|
||||
or type(param.default) in param_supported_types
|
||||
or isinstance(param.default, param_supported_types)
|
||||
) and (
|
||||
param.annotation == param.empty
|
||||
or lenient_issubclass(param.annotation, param_supported_types)
|
||||
@@ -183,16 +195,12 @@ def add_param_to_body_fields(*, param: inspect.Parameter, dependant: Dependant)
|
||||
dependant.body_params.append(field)
|
||||
|
||||
|
||||
def is_coroutine_callable(call: Callable = None) -> bool:
|
||||
if not call:
|
||||
return False
|
||||
def is_coroutine_callable(call: Callable) -> bool:
|
||||
if inspect.isfunction(call):
|
||||
return asyncio.iscoroutinefunction(call)
|
||||
if inspect.isclass(call):
|
||||
return False
|
||||
call = getattr(call, "__call__", None)
|
||||
if not call:
|
||||
return False
|
||||
return asyncio.iscoroutinefunction(call)
|
||||
|
||||
|
||||
@@ -206,7 +214,8 @@ async def solve_dependencies(
|
||||
request=request, dependant=sub_dependant, body=body
|
||||
)
|
||||
if sub_errors:
|
||||
return {}, errors
|
||||
errors.extend(sub_errors)
|
||||
continue
|
||||
assert sub_dependant.call is not None, "sub_dependant.call must be a function"
|
||||
if is_coroutine_callable(sub_dependant.call):
|
||||
solved = await sub_dependant.call(**sub_values)
|
||||
@@ -230,7 +239,7 @@ async def solve_dependencies(
|
||||
values.update(query_values)
|
||||
values.update(header_values)
|
||||
values.update(cookie_values)
|
||||
errors = path_errors + query_errors + header_errors + cookie_errors
|
||||
errors += path_errors + query_errors + header_errors + cookie_errors
|
||||
if dependant.body_params:
|
||||
body_values, body_errors = await request_body_to_args( # type: ignore # body_params checked above
|
||||
dependant.body_params, body
|
||||
@@ -287,7 +296,7 @@ async def request_body_to_args(
|
||||
received_body = {}
|
||||
for field in required_params:
|
||||
value = received_body.get(field.alias)
|
||||
if value is None:
|
||||
if value is None or (isinstance(field.schema, params.Form) and value == ""):
|
||||
if field.required:
|
||||
errors.append(
|
||||
ErrorWrapper(
|
||||
|
||||
@@ -3,7 +3,7 @@ from types import GeneratorType
|
||||
from typing import Any, Set
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic.json import pydantic_encoder
|
||||
from pydantic.json import ENCODERS_BY_TYPE
|
||||
|
||||
|
||||
def jsonable_encoder(
|
||||
@@ -41,4 +41,19 @@ def jsonable_encoder(
|
||||
)
|
||||
for item in obj
|
||||
]
|
||||
return pydantic_encoder(obj)
|
||||
errors = []
|
||||
try:
|
||||
encoder = ENCODERS_BY_TYPE[type(obj)]
|
||||
return encoder(obj)
|
||||
except KeyError as e:
|
||||
errors.append(e)
|
||||
try:
|
||||
data = dict(obj)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
try:
|
||||
data = vars(obj)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
raise ValueError(errors)
|
||||
return jsonable_encoder(data, by_alias=by_alias, include_none=include_none)
|
||||
|
||||
@@ -147,61 +147,65 @@ def get_openapi_path(
|
||||
security_schemes: Dict[str, Any] = {}
|
||||
definitions: Dict[str, Any] = {}
|
||||
assert route.methods is not None, "Methods must be a list"
|
||||
for method in route.methods:
|
||||
operation = get_openapi_operation_metadata(route=route, method=method)
|
||||
parameters: List[Dict] = []
|
||||
flat_dependant = get_flat_dependant(route.dependant)
|
||||
security_definitions, operation_security = get_openapi_security_definitions(
|
||||
flat_dependant=flat_dependant
|
||||
)
|
||||
if operation_security:
|
||||
operation.setdefault("security", []).extend(operation_security)
|
||||
if security_definitions:
|
||||
security_schemes.update(security_definitions)
|
||||
all_route_params = get_openapi_params(route.dependant)
|
||||
validation_definitions, operation_parameters = get_openapi_operation_parameters(
|
||||
all_route_params=all_route_params
|
||||
)
|
||||
definitions.update(validation_definitions)
|
||||
parameters.extend(operation_parameters)
|
||||
if parameters:
|
||||
operation["parameters"] = parameters
|
||||
if method in METHODS_WITH_BODY:
|
||||
request_body_oai = get_openapi_operation_request_body(
|
||||
body_field=route.body_field, model_name_map=model_name_map
|
||||
if route.include_in_schema:
|
||||
for method in route.methods:
|
||||
operation = get_openapi_operation_metadata(route=route, method=method)
|
||||
parameters: List[Dict] = []
|
||||
flat_dependant = get_flat_dependant(route.dependant)
|
||||
security_definitions, operation_security = get_openapi_security_definitions(
|
||||
flat_dependant=flat_dependant
|
||||
)
|
||||
if request_body_oai:
|
||||
operation["requestBody"] = request_body_oai
|
||||
if "ValidationError" not in definitions:
|
||||
definitions["ValidationError"] = validation_error_definition
|
||||
definitions[
|
||||
"HTTPValidationError"
|
||||
] = validation_error_response_definition
|
||||
status_code = str(route.status_code)
|
||||
response_schema = {"type": "string"}
|
||||
if lenient_issubclass(route.content_type, JSONResponse):
|
||||
if route.response_field:
|
||||
response_schema, _ = field_schema(
|
||||
route.response_field,
|
||||
model_name_map=model_name_map,
|
||||
ref_prefix=REF_PREFIX,
|
||||
if operation_security:
|
||||
operation.setdefault("security", []).extend(operation_security)
|
||||
if security_definitions:
|
||||
security_schemes.update(security_definitions)
|
||||
all_route_params = get_openapi_params(route.dependant)
|
||||
validation_definitions, operation_parameters = get_openapi_operation_parameters(
|
||||
all_route_params=all_route_params
|
||||
)
|
||||
definitions.update(validation_definitions)
|
||||
parameters.extend(operation_parameters)
|
||||
if parameters:
|
||||
operation["parameters"] = parameters
|
||||
if method in METHODS_WITH_BODY:
|
||||
request_body_oai = get_openapi_operation_request_body(
|
||||
body_field=route.body_field, model_name_map=model_name_map
|
||||
)
|
||||
else:
|
||||
response_schema = {}
|
||||
content = {route.content_type.media_type: {"schema": response_schema}}
|
||||
operation["responses"] = {
|
||||
status_code: {"description": route.response_description, "content": content}
|
||||
}
|
||||
if all_route_params or route.body_field:
|
||||
operation["responses"][str(HTTP_422_UNPROCESSABLE_ENTITY)] = {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": REF_PREFIX + "HTTPValidationError"}
|
||||
}
|
||||
},
|
||||
if request_body_oai:
|
||||
operation["requestBody"] = request_body_oai
|
||||
if "ValidationError" not in definitions:
|
||||
definitions["ValidationError"] = validation_error_definition
|
||||
definitions[
|
||||
"HTTPValidationError"
|
||||
] = validation_error_response_definition
|
||||
status_code = str(route.status_code)
|
||||
response_schema = {"type": "string"}
|
||||
if lenient_issubclass(route.content_type, JSONResponse):
|
||||
if route.response_field:
|
||||
response_schema, _ = field_schema(
|
||||
route.response_field,
|
||||
model_name_map=model_name_map,
|
||||
ref_prefix=REF_PREFIX,
|
||||
)
|
||||
else:
|
||||
response_schema = {}
|
||||
content = {route.content_type.media_type: {"schema": response_schema}}
|
||||
operation["responses"] = {
|
||||
status_code: {
|
||||
"description": route.response_description,
|
||||
"content": content,
|
||||
}
|
||||
}
|
||||
path[method.lower()] = operation
|
||||
if all_route_params or route.body_field:
|
||||
operation["responses"][str(HTTP_422_UNPROCESSABLE_ENTITY)] = {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": REF_PREFIX + "HTTPValidationError"}
|
||||
}
|
||||
},
|
||||
}
|
||||
path[method.lower()] = operation
|
||||
return path, security_schemes, definitions
|
||||
|
||||
|
||||
|
||||
@@ -241,7 +241,6 @@ class Form(Body):
|
||||
self,
|
||||
default: Any,
|
||||
*,
|
||||
sub_key: bool = False,
|
||||
media_type: str = "application/x-www-form-urlencoded",
|
||||
alias: str = None,
|
||||
title: str = None,
|
||||
@@ -257,7 +256,7 @@ class Form(Body):
|
||||
):
|
||||
super().__init__(
|
||||
default,
|
||||
embed=sub_key,
|
||||
embed=True,
|
||||
media_type=media_type,
|
||||
alias=alias,
|
||||
title=title,
|
||||
@@ -278,7 +277,6 @@ class File(Form):
|
||||
self,
|
||||
default: Any,
|
||||
*,
|
||||
sub_key: bool = False,
|
||||
media_type: str = "multipart/form-data",
|
||||
alias: str = None,
|
||||
title: str = None,
|
||||
@@ -294,7 +292,6 @@ class File(Form):
|
||||
):
|
||||
super().__init__(
|
||||
default,
|
||||
embed=sub_key,
|
||||
media_type=media_type,
|
||||
alias=alias,
|
||||
title=title,
|
||||
|
||||
@@ -22,9 +22,10 @@ from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
|
||||
def serialize_response(*, field: Field = None, response: Response) -> Any:
|
||||
encoded = jsonable_encoder(response)
|
||||
if field:
|
||||
errors = []
|
||||
value, errors_ = field.validate(response, {}, loc=("response",))
|
||||
value, errors_ = field.validate(encoded, {}, loc=("response",))
|
||||
if isinstance(errors_, ErrorWrapper):
|
||||
errors.append(errors_)
|
||||
elif isinstance(errors_, list):
|
||||
@@ -33,7 +34,7 @@ def serialize_response(*, field: Field = None, response: Response) -> Any:
|
||||
raise ValidationError(errors)
|
||||
return jsonable_encoder(value)
|
||||
else:
|
||||
return jsonable_encoder(response)
|
||||
return encoded
|
||||
|
||||
|
||||
def get_app(
|
||||
@@ -51,17 +52,20 @@ def get_app(
|
||||
try:
|
||||
body = None
|
||||
if body_field:
|
||||
body_bytes = await request.body()
|
||||
if body_bytes and is_body_form:
|
||||
if is_body_form:
|
||||
raw_body = await request.form()
|
||||
body = {}
|
||||
form_fields = {}
|
||||
for field, value in raw_body.items():
|
||||
if isinstance(value, UploadFile):
|
||||
body[field] = await value.read()
|
||||
form_fields[field] = await value.read()
|
||||
else:
|
||||
body[field] = value
|
||||
elif body_bytes:
|
||||
body = await request.json()
|
||||
form_fields[field] = value
|
||||
if form_fields:
|
||||
body = form_fields
|
||||
else:
|
||||
body_bytes = await request.body()
|
||||
if body_bytes:
|
||||
body = await request.json()
|
||||
except Exception as e:
|
||||
logging.error("Error getting request body", e)
|
||||
raise HTTPException(
|
||||
@@ -83,40 +87,10 @@ def get_app(
|
||||
raw_response = await run_in_threadpool(dependant.call, **values)
|
||||
if isinstance(raw_response, Response):
|
||||
return raw_response
|
||||
if isinstance(raw_response, BaseModel):
|
||||
return content_type(
|
||||
content=serialize_response(
|
||||
field=response_field, response=raw_response
|
||||
),
|
||||
status_code=status_code,
|
||||
)
|
||||
errors = []
|
||||
try:
|
||||
return content_type(
|
||||
content=serialize_response(
|
||||
field=response_field, response=raw_response
|
||||
),
|
||||
status_code=status_code,
|
||||
)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
try:
|
||||
response = dict(raw_response)
|
||||
return content_type(
|
||||
content=serialize_response(field=response_field, response=response),
|
||||
status_code=status_code,
|
||||
)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
try:
|
||||
response = vars(raw_response)
|
||||
return content_type(
|
||||
content=serialize_response(field=response_field, response=response),
|
||||
status_code=status_code,
|
||||
)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
raise ValueError(errors)
|
||||
response_data = serialize_response(
|
||||
field=response_field, response=raw_response
|
||||
)
|
||||
return content_type(content=response_data, status_code=status_code)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from fastapi.openapi.models import APIKey, APIKeyIn
|
||||
from fastapi.security.base import SecurityBase
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
class APIKeyBase(SecurityBase):
|
||||
@@ -9,26 +11,41 @@ class APIKeyBase(SecurityBase):
|
||||
|
||||
class APIKeyQuery(APIKeyBase):
|
||||
def __init__(self, *, name: str, scheme_name: str = None):
|
||||
self.model = APIKey(in_=APIKeyIn.query, name=name)
|
||||
self.model = APIKey(**{"in": APIKeyIn.query}, name=name)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, requests: Request) -> str:
|
||||
return requests.query_params.get(self.model.name)
|
||||
async def __call__(self, request: Request) -> str:
|
||||
api_key: str = request.query_params.get(self.model.name)
|
||||
if not api_key:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
return api_key
|
||||
|
||||
|
||||
class APIKeyHeader(APIKeyBase):
|
||||
def __init__(self, *, name: str, scheme_name: str = None):
|
||||
self.model = APIKey(in_=APIKeyIn.header, name=name)
|
||||
self.model = APIKey(**{"in": APIKeyIn.header}, name=name)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, requests: Request) -> str:
|
||||
return requests.headers.get(self.model.name)
|
||||
async def __call__(self, request: Request) -> str:
|
||||
api_key: str = request.headers.get(self.model.name)
|
||||
if not api_key:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
return api_key
|
||||
|
||||
|
||||
class APIKeyCookie(APIKeyBase):
|
||||
def __init__(self, *, name: str, scheme_name: str = None):
|
||||
self.model = APIKey(in_=APIKeyIn.cookie, name=name)
|
||||
self.model = APIKey(**{"in": APIKeyIn.cookie}, name=name)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, requests: Request) -> str:
|
||||
return requests.cookies.get(self.model.name)
|
||||
async def __call__(self, request: Request) -> str:
|
||||
api_key: str = request.cookies.get(self.model.name)
|
||||
if not api_key:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
return api_key
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import binascii
|
||||
from base64 import b64decode
|
||||
|
||||
from fastapi.openapi.models import (
|
||||
HTTPBase as HTTPBaseModel,
|
||||
HTTPBearer as HTTPBearerModel,
|
||||
)
|
||||
from fastapi.security.base import SecurityBase
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from pydantic import BaseModel
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
class HTTPBasicCredentials(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class HTTPAuthorizationCredentials(BaseModel):
|
||||
scheme: str
|
||||
credentials: str
|
||||
|
||||
|
||||
class HTTPBase(SecurityBase):
|
||||
@@ -12,16 +29,41 @@ class HTTPBase(SecurityBase):
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
return request.headers.get("Authorization")
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||
if not (authorization and scheme and credentials):
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
|
||||
|
||||
class HTTPBasic(HTTPBase):
|
||||
def __init__(self, *, scheme_name: str = None):
|
||||
def __init__(self, *, scheme_name: str = None, realm: str = None):
|
||||
self.model = HTTPBaseModel(scheme="basic")
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.realm = realm
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
return request.headers.get("Authorization")
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
# before implementing headers with 401 errors, wait for: https://github.com/encode/starlette/issues/295
|
||||
# unauthorized_headers = {"WWW-Authenticate": "Basic"}
|
||||
invalid_user_credentials_exc = HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Invalid authentication credentials"
|
||||
)
|
||||
if not authorization or scheme.lower() != "basic":
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
try:
|
||||
data = b64decode(param).decode("ascii")
|
||||
except (ValueError, UnicodeDecodeError, binascii.Error):
|
||||
raise invalid_user_credentials_exc
|
||||
username, separator, password = data.partition(":")
|
||||
if not (separator):
|
||||
raise invalid_user_credentials_exc
|
||||
return HTTPBasicCredentials(username=username, password=password)
|
||||
|
||||
|
||||
class HTTPBearer(HTTPBase):
|
||||
@@ -30,7 +72,13 @@ class HTTPBearer(HTTPBase):
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
return request.headers.get("Authorization")
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||
if not (authorization and scheme and credentials):
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
|
||||
|
||||
class HTTPDigest(HTTPBase):
|
||||
@@ -39,4 +87,10 @@ class HTTPDigest(HTTPBase):
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
return request.headers.get("Authorization")
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||
if not (authorization and scheme and credentials):
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.openapi.models import OAuth2 as OAuth2Model, OAuthFlows as OAuthFlowsModel
|
||||
from fastapi.params import Form
|
||||
from fastapi.security.base import SecurityBase
|
||||
from pydantic import BaseModel, Schema
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
class OAuth2PasswordRequestData(BaseModel):
|
||||
grant_type: str = "password"
|
||||
username: str
|
||||
password: str
|
||||
scope: Optional[List[str]] = None
|
||||
# Client ID and secret might come from headers
|
||||
client_id: Optional[str] = None
|
||||
client_secret: Optional[str] = None
|
||||
|
||||
|
||||
class OAuth2PasswordRequestForm(BaseModel):
|
||||
class OAuth2PasswordRequestForm:
|
||||
"""
|
||||
This is not a "Security" model. Use it as request Body. As in:
|
||||
This is a dependency class, use it like:
|
||||
|
||||
@app.post("/login")
|
||||
def login(form_data: Oauth2PasswordRequestForm):
|
||||
def login(form_data: Oauth2PasswordRequestForm = Depends()):
|
||||
data = form_data.parse()
|
||||
print(data.username)
|
||||
print(data.password)
|
||||
for scope in data.scope:
|
||||
for scope in data.scopes:
|
||||
print(scope)
|
||||
if data.client_id:
|
||||
print(data.client_id)
|
||||
@@ -39,8 +30,8 @@ class OAuth2PasswordRequestForm(BaseModel):
|
||||
It creates the following Form request parameters in your endpoint:
|
||||
|
||||
grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password".
|
||||
Nevertheless, this model is permissive and allows not passing it. If you want to enforce it,
|
||||
use instead the OAuth2PasswordRequestFormStrict model.
|
||||
Nevertheless, this dependency class is permissive and allows not passing it. If you want to enforce it,
|
||||
use instead the OAuth2PasswordRequestFormStrict dependency.
|
||||
username: username string. The OAuth2 spec requires the exact field name "username".
|
||||
password: password string. The OAuth2 spec requires the exact field name "password".
|
||||
scope: Optional string. Several scopes (each one a string) separated by spaces. E.g.
|
||||
@@ -49,33 +40,75 @@ class OAuth2PasswordRequestForm(BaseModel):
|
||||
using HTTP Basic auth, as: client_id:client_secret
|
||||
client_secret: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
|
||||
using HTTP Basic auth, as: client_id:client_secret
|
||||
|
||||
|
||||
It has the method parse() that returns a model with all the same data and the scopes extracted as a list of strings.
|
||||
"""
|
||||
|
||||
grant_type: str = Schema(None, regex="password")
|
||||
username: str
|
||||
password: str
|
||||
scope: str = ""
|
||||
# Client ID and secret might come from headers
|
||||
client_id: Optional[str] = None
|
||||
client_secret: Optional[str] = None
|
||||
|
||||
def parse(self) -> OAuth2PasswordRequestData:
|
||||
return OAuth2PasswordRequestData(
|
||||
grant_type=self.grant_type,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
scope=self.scope.split(),
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
)
|
||||
def __init__(
|
||||
self,
|
||||
grant_type: str = Form(None, regex="password"),
|
||||
username: str = Form(...),
|
||||
password: str = Form(...),
|
||||
scope: str = Form(""),
|
||||
client_id: Optional[str] = Form(None),
|
||||
client_secret: Optional[str] = Form(None),
|
||||
):
|
||||
self.grant_type = grant_type
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.scopes = scope.split()
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
|
||||
|
||||
class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm):
|
||||
# The OAuth2 spec says it MUST have the value "password"
|
||||
grant_type: str = Schema(..., regex="password")
|
||||
"""
|
||||
This is a dependency class, use it like:
|
||||
|
||||
@app.post("/login")
|
||||
def login(form_data: Oauth2PasswordRequestFormStrict = Depends()):
|
||||
data = form_data.parse()
|
||||
print(data.username)
|
||||
print(data.password)
|
||||
for scope in data.scopes:
|
||||
print(scope)
|
||||
if data.client_id:
|
||||
print(data.client_id)
|
||||
if data.client_secret:
|
||||
print(data.client_secret)
|
||||
return data
|
||||
|
||||
|
||||
It creates the following Form request parameters in your endpoint:
|
||||
|
||||
grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password".
|
||||
This dependency is strict about it. If you want to be permissive, use instead the
|
||||
OAuth2PasswordRequestFormStrict dependency class.
|
||||
username: username string. The OAuth2 spec requires the exact field name "username".
|
||||
password: password string. The OAuth2 spec requires the exact field name "password".
|
||||
scope: Optional string. Several scopes (each one a string) separated by spaces. E.g.
|
||||
"items:read items:write users:read profile openid"
|
||||
client_id: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
|
||||
using HTTP Basic auth, as: client_id:client_secret
|
||||
client_secret: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
|
||||
using HTTP Basic auth, as: client_id:client_secret
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
grant_type: str = Form(..., regex="password"),
|
||||
username: str = Form(...),
|
||||
password: str = Form(...),
|
||||
scope: str = Form(""),
|
||||
client_id: Optional[str] = Form(None),
|
||||
client_secret: Optional[str] = Form(None),
|
||||
):
|
||||
super().__init__(
|
||||
grant_type=grant_type,
|
||||
username=username,
|
||||
password=password,
|
||||
scope=scope,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
)
|
||||
|
||||
|
||||
class OAuth2(SecurityBase):
|
||||
@@ -86,7 +119,12 @@ class OAuth2(SecurityBase):
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
return request.headers.get("Authorization")
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
if not authorization:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
return authorization
|
||||
|
||||
|
||||
class OAuth2PasswordBearer(OAuth2):
|
||||
@@ -98,9 +136,9 @@ class OAuth2PasswordBearer(OAuth2):
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
if not authorization or "Bearer " not in authorization:
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "bearer":
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
token = authorization.replace("Bearer ", "")
|
||||
return token
|
||||
return param
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel
|
||||
from fastapi.security.base import SecurityBase
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
class OpenIdConnect(SecurityBase):
|
||||
@@ -9,4 +11,9 @@ class OpenIdConnect(SecurityBase):
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
return request.headers.get("Authorization")
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
if not authorization:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
return authorization
|
||||
|
||||
8
fastapi/security/utils.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
def get_authorization_scheme_param(authorization_header_value: str) -> Tuple[str, str]:
|
||||
if not authorization_header_value:
|
||||
return "", ""
|
||||
scheme, _, param = authorization_header_value.partition(" ")
|
||||
return scheme, param
|
||||
15
mkdocs.yml
@@ -28,6 +28,7 @@ nav:
|
||||
- Body - Multiple Parameters: 'tutorial/body-multiple-params.md'
|
||||
- Body - Schema: 'tutorial/body-schema.md'
|
||||
- Body - Nested Models: 'tutorial/body-nested-models.md'
|
||||
- Extra data types: 'tutorial/extra-data-types.md'
|
||||
- Cookie Parameters: 'tutorial/cookie-params.md'
|
||||
- Header Parameters: 'tutorial/header-params.md'
|
||||
- Response Model: 'tutorial/response-model.md'
|
||||
@@ -40,20 +41,26 @@ nav:
|
||||
- Custom Response: 'tutorial/custom-response.md'
|
||||
- Dependencies:
|
||||
- Dependencies Intro: 'tutorial/dependencies/intro.md'
|
||||
- First Steps: 'tutorial/dependencies/first-steps.md'
|
||||
- Second Steps: 'tutorial/dependencies/second-steps.md'
|
||||
- SQL (Relational) Databases: 'tutorial/sql-databases.md'
|
||||
- NoSQL (Distributed / Big Data) Databases: 'tutorial/nosql-databases.md'
|
||||
- First Steps - Functions: 'tutorial/dependencies/first-steps-functions.md'
|
||||
- Classes as Dependencies: 'tutorial/dependencies/classes-as-dependencies.md'
|
||||
- Sub-dependencies: 'tutorial/dependencies/sub-dependencies.md'
|
||||
- Advanced Dependencies: 'tutorial/dependencies/advanced-dependencies.md'
|
||||
- Security:
|
||||
- Security Intro: 'tutorial/security/intro.md'
|
||||
- First Steps: 'tutorial/security/first-steps.md'
|
||||
- Get Current User: 'tutorial/security/get-current-user.md'
|
||||
- Simple OAuth2 with Password and Bearer: 'tutorial/security/simple-oauth2.md'
|
||||
- OAuth2 with Password (and hashing), Bearer with JWT tokens: 'tutorial/security/oauth2-jwt.md'
|
||||
- SQL (Relational) Databases: 'tutorial/sql-databases.md'
|
||||
- NoSQL (Distributed / Big Data) Databases: 'tutorial/nosql-databases.md'
|
||||
- Bigger Applications - Multiple Files: 'tutorial/bigger-applications.md'
|
||||
- Application Configuration: 'tutorial/application-configuration.md'
|
||||
- Extra Starlette options: 'tutorial/extra-starlette.md'
|
||||
- Concurrency and async / await: 'async.md'
|
||||
- Deployment: 'deployment.md'
|
||||
- Project Generation - Template: 'project-generation.md'
|
||||
- Alternatives, Inspiration and Comparisons: 'alternatives.md'
|
||||
- Benchmarks: 'benchmarks.md'
|
||||
|
||||
markdown_extensions:
|
||||
- markdown.extensions.codehilite:
|
||||
|
||||
118
pending_tests/main.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from fastapi import (
|
||||
Body,
|
||||
Cookie,
|
||||
Depends,
|
||||
FastAPI,
|
||||
File,
|
||||
Form,
|
||||
Header,
|
||||
Path,
|
||||
Query,
|
||||
Security,
|
||||
)
|
||||
from fastapi.security import (
|
||||
HTTPBasic,
|
||||
OAuth2,
|
||||
OAuth2PasswordBearer,
|
||||
OAuth2PasswordRequestForm,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
||||
from starlette.status import HTTP_202_ACCEPTED
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/security")
|
||||
def get_security(sec=Security(HTTPBasic())):
|
||||
return sec
|
||||
|
||||
|
||||
reusable_oauth2 = OAuth2(
|
||||
flows={
|
||||
"password": {
|
||||
"tokenUrl": "/token",
|
||||
"scopes": {"read:user": "Read a User", "write:user": "Create a user"},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/security/oauth2")
|
||||
def get_security_oauth2(sec=Security(reusable_oauth2, scopes=["read:user"])):
|
||||
return sec
|
||||
|
||||
|
||||
@app.post("/token")
|
||||
def post_token(request_data: OAuth2PasswordRequestForm = Form(...)):
|
||||
data = request_data.parse()
|
||||
access_token = data.username + ":" + data.password
|
||||
return {"access_token": access_token}
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
is_offer: bool
|
||||
|
||||
|
||||
class FakeDB:
|
||||
def __init__(self):
|
||||
self.data = {
|
||||
"johndoe": {
|
||||
"username": "johndoe",
|
||||
"password": "shouldbehashed",
|
||||
"fist_name": "John",
|
||||
"last_name": "Doe",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DBConnectionManager:
|
||||
def __init__(self):
|
||||
self.db = FakeDB()
|
||||
|
||||
def __call__(self):
|
||||
return self.db
|
||||
|
||||
|
||||
connection_manager = DBConnectionManager()
|
||||
|
||||
|
||||
class TokenUserData(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class UserInDB(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
fist_name: str
|
||||
last_name: str
|
||||
|
||||
|
||||
def require_token(
|
||||
token: str = Security(reusable_oauth2, scopes=["read:user", "write:user"])
|
||||
):
|
||||
raw_token = token.replace("Bearer ", "")
|
||||
# Never do this plaintext password usage in production
|
||||
username, password = raw_token.split(":")
|
||||
return TokenUserData(username=username, password=password)
|
||||
|
||||
|
||||
def require_user(
|
||||
db: FakeDB = Depends(connection_manager),
|
||||
user_data: TokenUserData = Depends(require_token),
|
||||
):
|
||||
return db.data[user_data.username]
|
||||
|
||||
|
||||
class UserOut(BaseModel):
|
||||
username: str
|
||||
fist_name: str
|
||||
last_name: str
|
||||
|
||||
|
||||
@app.get("/dependency", response_model=UserOut)
|
||||
def get_dependency(user: UserInDB = Depends(require_user)):
|
||||
return user
|
||||
@@ -20,7 +20,7 @@ classifiers = [
|
||||
]
|
||||
requires = [
|
||||
"starlette >=0.9.7",
|
||||
"pydantic >=0.16"
|
||||
"pydantic >=0.17"
|
||||
]
|
||||
description-file = "README.md"
|
||||
requires-python = ">=3.6"
|
||||
@@ -35,7 +35,8 @@ test = [
|
||||
"mypy",
|
||||
"black",
|
||||
"isort",
|
||||
"requests"
|
||||
"requests",
|
||||
"email_validator"
|
||||
]
|
||||
doc = [
|
||||
"mkdocs",
|
||||
@@ -43,8 +44,8 @@ doc = [
|
||||
"markdown-include"
|
||||
]
|
||||
dev = [
|
||||
"prospector",
|
||||
"rope"
|
||||
"pyjwt",
|
||||
"passlib[bcrypt]"
|
||||
]
|
||||
all = [
|
||||
"requests",
|
||||
|
||||
6
scripts/test-cov-html.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
bash scripts/test.sh --cov-report=html
|
||||
@@ -7,8 +7,7 @@ export VERSION_SCRIPT="import sys; print('%s.%s' % sys.version_info[0:2])"
|
||||
export PYTHON_VERSION=`python -c "$VERSION_SCRIPT"`
|
||||
|
||||
export PYTHONPATH=./docs/src
|
||||
# PYTHONPATH=. pytest --cov=fastapi --cov=tests --cov-fail-under=100 --cov-report=term-missing ${@} --cov-report=html
|
||||
pytest --cov=fastapi --cov=tests --cov=docs/src --cov-report=term-missing ${@} --cov-report=html
|
||||
pytest --cov=fastapi --cov=tests --cov=docs/src --cov-report=term-missing ${@}
|
||||
mypy fastapi --disallow-untyped-defs
|
||||
if [ "${PYTHON_VERSION}" = '3.7' ]; then
|
||||
echo "Skipping 'black' on 3.7. See issue https://github.com/ambv/black/issues/494"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/dog")
|
||||
def get_a_dog():
|
||||
return "Woof"
|
||||
|
||||
|
||||
@router.get("/cat")
|
||||
def get_a_cat():
|
||||
return "Meow"
|
||||
@@ -1,13 +0,0 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/dog")
|
||||
def get_b_dog():
|
||||
return "B Woof"
|
||||
|
||||
|
||||
@router.get("/cat")
|
||||
def get_b_cat():
|
||||
return "B Meow"
|
||||
@@ -1,6 +1,4 @@
|
||||
from fastapi import Depends, FastAPI, Path, Query, Security
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from pydantic import BaseModel
|
||||
from fastapi import FastAPI, Path, Query
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -144,8 +142,6 @@ def get_path_param_le_ge_int(item_id: int = Path(..., le=3, ge=1)):
|
||||
|
||||
@app.get("/query")
|
||||
def get_query(query):
|
||||
if query is None:
|
||||
return "foo bar"
|
||||
return f"foo bar {query}"
|
||||
|
||||
|
||||
@@ -158,8 +154,6 @@ def get_query_optional(query=None):
|
||||
|
||||
@app.get("/query/int")
|
||||
def get_query_type(query: int):
|
||||
if query is None:
|
||||
return "foo bar"
|
||||
return f"foo bar {query}"
|
||||
|
||||
|
||||
@@ -184,30 +178,9 @@ def get_query_param(query=Query(None)):
|
||||
|
||||
@app.get("/query/param-required")
|
||||
def get_query_param_required(query=Query(...)):
|
||||
if query is None:
|
||||
return "foo bar"
|
||||
return f"foo bar {query}"
|
||||
|
||||
|
||||
@app.get("/query/param-required/int")
|
||||
def get_query_param_required_type(query: int = Query(...)):
|
||||
if query is None:
|
||||
return "foo bar"
|
||||
return f"foo bar {query}"
|
||||
|
||||
|
||||
reusable_oauth2b = OAuth2PasswordBearer(tokenUrl="/token")
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: str = Security(reusable_oauth2b)):
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/security/oauth2b")
|
||||
def read_current_user(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
from fastapi import (
|
||||
Body,
|
||||
Cookie,
|
||||
Depends,
|
||||
FastAPI,
|
||||
File,
|
||||
Form,
|
||||
Header,
|
||||
Path,
|
||||
Query,
|
||||
Security,
|
||||
)
|
||||
from fastapi.security import (
|
||||
HTTPBasic,
|
||||
OAuth2,
|
||||
OAuth2PasswordBearer,
|
||||
OAuth2PasswordRequestForm,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
||||
from starlette.status import HTTP_202_ACCEPTED
|
||||
|
||||
from .endpoints.a import router as router_a
|
||||
from .endpoints.b import router as router_b
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
app.include_router(router_a)
|
||||
app.include_router(router_b, prefix="/b")
|
||||
|
||||
|
||||
@app.get("/cookie")
|
||||
def get_cookie(coo=Cookie(None)):
|
||||
return coo
|
||||
|
||||
|
||||
@app.get("/header")
|
||||
def get_header(head_name=Header(None)):
|
||||
return head_name
|
||||
|
||||
|
||||
@app.get("/header_under")
|
||||
def get_header(head_name=Header(None, convert_underscores=False)):
|
||||
return head_name
|
||||
|
||||
|
||||
@app.get("/security")
|
||||
def get_security(sec=Security(HTTPBasic())):
|
||||
return sec
|
||||
|
||||
|
||||
reusable_oauth2 = OAuth2(
|
||||
flows={
|
||||
"password": {
|
||||
"tokenUrl": "/token",
|
||||
"scopes": {"read:user": "Read a User", "write:user": "Create a user"},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/security/oauth2")
|
||||
def get_security_oauth2(sec=Security(reusable_oauth2, scopes=["read:user"])):
|
||||
return sec
|
||||
|
||||
|
||||
@app.post("/token")
|
||||
def post_token(request_data: OAuth2PasswordRequestForm = Form(...)):
|
||||
data = request_data.parse()
|
||||
access_token = data.username + ":" + data.password
|
||||
return {"access_token": access_token}
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
is_offer: bool
|
||||
|
||||
|
||||
@app.put("/items/{item_id}")
|
||||
def put_item(item_id: str, item: Item):
|
||||
return item
|
||||
|
||||
|
||||
@app.post("/items/")
|
||||
def post_item(item: Item):
|
||||
return item
|
||||
|
||||
|
||||
@app.post("/items-all-params/{item_id}")
|
||||
def post_items_all_params(
|
||||
item_id: str = Path(...),
|
||||
body: Item = Body(...),
|
||||
query_a: int = Query(None),
|
||||
query_b=Query(None),
|
||||
coo: str = Cookie(None),
|
||||
x_head: int = Header(None),
|
||||
x_under: str = Header(None, convert_underscores=False),
|
||||
):
|
||||
return {
|
||||
"item_id": item_id,
|
||||
"body": body,
|
||||
"query_a": query_a,
|
||||
"query_b": query_b,
|
||||
"coo": coo,
|
||||
"x_head": x_head,
|
||||
"x_under": x_under,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/items-all-params-defaults/{item_id}")
|
||||
def post_items_all_params_default(
|
||||
item_id: str,
|
||||
body_item_a: Item,
|
||||
body_item_b: Item,
|
||||
query_a: int,
|
||||
query_b: int,
|
||||
coo: str = Cookie(None),
|
||||
x_head: int = Header(None),
|
||||
x_under: str = Header(None, convert_underscores=False),
|
||||
):
|
||||
return {
|
||||
"item_id": item_id,
|
||||
"body_item_a": body_item_a,
|
||||
"body_item_b": body_item_b,
|
||||
"query_a": query_a,
|
||||
"query_b": query_b,
|
||||
"coo": coo,
|
||||
"x_head": x_head,
|
||||
"x_under": x_under,
|
||||
}
|
||||
|
||||
|
||||
@app.delete("/items/{item_id}")
|
||||
def delete_item(item_id: str):
|
||||
return item_id
|
||||
|
||||
|
||||
@app.options("/options/")
|
||||
def options():
|
||||
return JSONResponse(headers={"x-fastapi": "fast"})
|
||||
|
||||
|
||||
@app.head("/head/")
|
||||
def head():
|
||||
return {"not sent": "nope"}
|
||||
|
||||
|
||||
@app.patch("/patch/{user_id}")
|
||||
def patch(user_id: str, increment: float):
|
||||
return {"user_id": user_id, "total": 5 + increment}
|
||||
|
||||
|
||||
@app.trace("/trace/")
|
||||
def trace():
|
||||
return PlainTextResponse(media_type="message/http")
|
||||
|
||||
|
||||
@app.get("/model", response_model=Item, status_code=HTTP_202_ACCEPTED)
|
||||
def model():
|
||||
return {"name": "Foo", "price": "5.0", "password": "not sent"}
|
||||
|
||||
|
||||
@app.get(
|
||||
"/metadata",
|
||||
tags=["tag1", "tag2"],
|
||||
summary="The summary",
|
||||
description="The description",
|
||||
response_description="Response description",
|
||||
deprecated=True,
|
||||
operation_id="a_very_long_and_strange_operation_id",
|
||||
)
|
||||
def get_meta():
|
||||
return "Foo"
|
||||
|
||||
|
||||
@app.get("/html", content_type=HTMLResponse)
|
||||
def get_html():
|
||||
return """
|
||||
<html>
|
||||
<body>
|
||||
<h1>
|
||||
Some text inside
|
||||
</h1>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
class FakeDB:
|
||||
def __init__(self):
|
||||
self.data = {
|
||||
"johndoe": {
|
||||
"username": "johndoe",
|
||||
"password": "shouldbehashed",
|
||||
"fist_name": "John",
|
||||
"last_name": "Doe",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DBConnectionManager:
|
||||
def __init__(self):
|
||||
self.db = FakeDB()
|
||||
|
||||
def __call__(self):
|
||||
return self.db
|
||||
|
||||
|
||||
connection_manager = DBConnectionManager()
|
||||
|
||||
|
||||
class TokenUserData(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class UserInDB(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
fist_name: str
|
||||
last_name: str
|
||||
|
||||
|
||||
def require_token(
|
||||
token: str = Security(reusable_oauth2, scopes=["read:user", "write:user"])
|
||||
):
|
||||
raw_token = token.replace("Bearer ", "")
|
||||
# Never do this plaintext password usage in production
|
||||
username, password = raw_token.split(":")
|
||||
return TokenUserData(username=username, password=password)
|
||||
|
||||
|
||||
def require_user(
|
||||
db: FakeDB = Depends(connection_manager),
|
||||
user_data: TokenUserData = Depends(require_token),
|
||||
):
|
||||
return db.data[user_data.username]
|
||||
|
||||
|
||||
class UserOut(BaseModel):
|
||||
username: str
|
||||
fist_name: str
|
||||
last_name: str
|
||||
|
||||
|
||||
@app.get("/dependency", response_model=UserOut)
|
||||
def get_dependency(user: UserInDB = Depends(require_user)):
|
||||
return user
|
||||
@@ -1078,19 +1078,6 @@ openapi_schema = {
|
||||
],
|
||||
}
|
||||
},
|
||||
"/security/oauth2b": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"operationId": "read_current_user_security_oauth2b_get",
|
||||
"security": [{"OAuth2PasswordBearer": []}],
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
@@ -1119,13 +1106,7 @@ openapi_schema = {
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"securitySchemes": {
|
||||
"OAuth2PasswordBearer": {
|
||||
"type": "oauth2",
|
||||
"flows": {"password": {"scopes": {}, "tokenUrl": "/token"}},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1134,6 +1115,7 @@ openapi_schema = {
|
||||
"path,expected_status,expected_response",
|
||||
[
|
||||
("/api_route", 200, {"message": "Hello World"}),
|
||||
("/non_decorated_route", 200, {"message": "Hello World"}),
|
||||
("/nonexistent", 404, {"detail": "Not Found"}),
|
||||
("/openapi.json", 200, openapi_schema),
|
||||
],
|
||||
@@ -1147,10 +1129,12 @@ def test_get_path(path, expected_status, expected_response):
|
||||
def test_swagger_ui():
|
||||
response = client.get("/docs")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"] == "text/html; charset=utf-8"
|
||||
assert "swagger-ui-dist" in response.text
|
||||
|
||||
|
||||
def test_redoc():
|
||||
response = client.get("/redoc")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"] == "text/html; charset=utf-8"
|
||||
assert "redoc@next" in response.text
|
||||
|
||||
360
tests/test_extra_routes.py
Normal file
@@ -0,0 +1,360 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float = None
|
||||
|
||||
|
||||
@app.api_route("/items/{item_id}", methods=["GET"])
|
||||
def get_items(item_id: str):
|
||||
return {"item_id": item_id}
|
||||
|
||||
|
||||
def get_not_decorated(item_id: str):
|
||||
return {"item_id": item_id}
|
||||
|
||||
|
||||
app.add_api_route("/items-not-decorated/{item_id}", get_not_decorated)
|
||||
|
||||
|
||||
@app.delete("/items/{item_id}")
|
||||
def delete_item(item_id: str, item: Item):
|
||||
return {"item_id": item_id, "item": item}
|
||||
|
||||
|
||||
@app.head("/items/{item_id}")
|
||||
def head_item(item_id: str):
|
||||
return JSONResponse(headers={"x-fastapi-item-id": item_id})
|
||||
|
||||
|
||||
@app.options("/items/{item_id}")
|
||||
def options_item(item_id: str):
|
||||
return JSONResponse(headers={"x-fastapi-item-id": item_id})
|
||||
|
||||
|
||||
@app.patch("/items/{item_id}")
|
||||
def patch_item(item_id: str, item: Item):
|
||||
return {"item_id": item_id, "item": item}
|
||||
|
||||
|
||||
@app.trace("/items/{item_id}")
|
||||
def trace_item(item_id: str):
|
||||
return JSONResponse(media_type="message/http")
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/{item_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Items Get",
|
||||
"operationId": "get_items_items__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
},
|
||||
"delete": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Delete Item Delete",
|
||||
"operationId": "delete_item_items__item_id__delete",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
"options": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Options Item Options",
|
||||
"operationId": "options_item_items__item_id__options",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
},
|
||||
"head": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Head Item Head",
|
||||
"operationId": "head_item_items__item_id__head",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
},
|
||||
"patch": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Patch Item Patch",
|
||||
"operationId": "patch_item_items__item_id__patch",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
"trace": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Trace Item Trace",
|
||||
"operationId": "trace_item_items__item_id__trace",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
"/items-not-decorated/{item_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Not Decorated Get",
|
||||
"operationId": "get_not_decorated_items-not-decorated__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Item": {
|
||||
"title": "Item",
|
||||
"required": ["name"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
},
|
||||
},
|
||||
"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_api_route():
|
||||
response = client.get("/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "foo"}
|
||||
|
||||
|
||||
def test_get_api_route_not_decorated():
|
||||
response = client.get("/items-not-decorated/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "foo"}
|
||||
|
||||
|
||||
def test_delete():
|
||||
response = client.delete("/items/foo", json={"name": "Foo"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "foo", "item": {"name": "Foo", "price": None}}
|
||||
|
||||
|
||||
def test_head():
|
||||
response = client.head("/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["x-fastapi-item-id"] == "foo"
|
||||
|
||||
|
||||
def test_options():
|
||||
response = client.options("/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["x-fastapi-item-id"] == "foo"
|
||||
|
||||
|
||||
def test_patch():
|
||||
response = client.patch("/items/foo", json={"name": "Foo"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "foo", "item": {"name": "Foo", "price": None}}
|
||||
|
||||
|
||||
def test_trace():
|
||||
response = client.request("trace", "/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"] == "message/http"
|
||||
23
tests/test_include_route.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.route("/items/")
|
||||
def read_items(request: Request):
|
||||
return JSONResponse({"hello": "world"})
|
||||
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_sub_router():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"hello": "world"}
|
||||
50
tests/test_jsonable_encoder.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import pytest
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
|
||||
class Person:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
|
||||
class Pet:
|
||||
def __init__(self, owner: Person, name: str):
|
||||
self.owner = owner
|
||||
self.name = name
|
||||
|
||||
|
||||
class DictablePerson(Person):
|
||||
def __iter__(self):
|
||||
return ((k, v) for k, v in self.__dict__.items())
|
||||
|
||||
|
||||
class DictablePet(Pet):
|
||||
def __iter__(self):
|
||||
return ((k, v) for k, v in self.__dict__.items())
|
||||
|
||||
|
||||
class Unserializable:
|
||||
def __iter__(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def __dict__(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def test_encode_class():
|
||||
person = Person(name="Foo")
|
||||
pet = Pet(owner=person, name="Firulais")
|
||||
assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}}
|
||||
|
||||
|
||||
def test_encode_dictable():
|
||||
person = DictablePerson(name="Foo")
|
||||
pet = DictablePet(owner=person, name="Firulais")
|
||||
assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}}
|
||||
|
||||
|
||||
def test_encode_unsupported():
|
||||
unserializable = Unserializable()
|
||||
with pytest.raises(ValueError):
|
||||
jsonable_encoder(unserializable)
|
||||
25
tests/test_param_class.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.params import Param
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
def read_items(q: str = Param(None)):
|
||||
return {"q": q}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_default_param_query_none():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"q": None}
|
||||
|
||||
|
||||
def test_default_param_query():
|
||||
response = client.get("/items/?q=foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"q": "foo"}
|
||||
@@ -40,9 +40,19 @@ response_not_valid_int = {
|
||||
("/query/int?query=42.5", 422, response_not_valid_int),
|
||||
("/query/int?query=baz", 422, response_not_valid_int),
|
||||
("/query/int?not_declared=baz", 422, response_missing),
|
||||
("/query/int/optional", 200, "foo bar"),
|
||||
("/query/int/optional?query=50", 200, "foo bar 50"),
|
||||
("/query/int/optional?query=foo", 422, response_not_valid_int),
|
||||
("/query/int/default", 200, "foo bar 10"),
|
||||
("/query/int/default?query=50", 200, "foo bar 50"),
|
||||
("/query/int/default?query=foo", 422, response_not_valid_int),
|
||||
("/query/param", 200, "foo bar"),
|
||||
("/query/param?query=50", 200, "foo bar 50"),
|
||||
("/query/param-required", 422, response_missing),
|
||||
("/query/param-required?query=50", 200, "foo bar 50"),
|
||||
("/query/param-required/int", 422, response_missing),
|
||||
("/query/param-required/int?query=50", 200, "foo bar 50"),
|
||||
("/query/param-required/int?query=foo", 422, response_not_valid_int),
|
||||
],
|
||||
)
|
||||
def test_get_path(path, expected_status, expected_response):
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from .main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_security_oauth2_password_bearer():
|
||||
response = client.get(
|
||||
"/security/oauth2b", headers={"Authorization": "Bearer footokenbar"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "footokenbar"}
|
||||
|
||||
|
||||
def test_security_oauth2_password_bearer_wrong_header():
|
||||
response = client.get("/security/oauth2b", headers={"Authorization": "footokenbar"})
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_security_oauth2_password_bearer_no_header():
|
||||
response = client.get("/security/oauth2b")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
68
tests/test_security_api_key_cookie.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import APIKeyCookie
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
api_key = APIKeyCookie(name="key")
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: str = Security(api_key)):
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
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": [{"APIKeyCookie": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_api_key():
|
||||
response = client.get("/users/me", cookies={"key": "secret"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "secret"}
|
||||
|
||||
|
||||
def test_security_api_key_no_key():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
68
tests/test_security_api_key_header.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import APIKeyHeader
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
api_key = APIKeyHeader(name="key")
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: str = Security(api_key)):
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
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": [{"APIKeyHeader": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"APIKeyHeader": {"type": "apiKey", "name": "key", "in": "header"}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_api_key():
|
||||
response = client.get("/users/me", headers={"key": "secret"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "secret"}
|
||||
|
||||
|
||||
def test_security_api_key_no_key():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
68
tests/test_security_api_key_query.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import APIKeyQuery
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
api_key = APIKeyQuery(name="key")
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: str = Security(api_key)):
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
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": [{"APIKeyQuery": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_api_key():
|
||||
response = client.get("/users/me?key=secret")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "secret"}
|
||||
|
||||
|
||||
def test_security_api_key_no_key():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
247
tests/test_security_oauth2.py
Normal file
@@ -0,0 +1,247 @@
|
||||
import pytest
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import OAuth2
|
||||
from fastapi.security.oauth2 import OAuth2PasswordRequestFormStrict
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
reusable_oauth2 = OAuth2(
|
||||
flows={
|
||||
"password": {
|
||||
"tokenUrl": "/token",
|
||||
"scopes": {"read:users": "Read the users", "write:users": "Create users"},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: str = Security(reusable_oauth2)):
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.post("/login")
|
||||
def read_current_user(form_data: OAuth2PasswordRequestFormStrict = Depends()):
|
||||
return form_data
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/login": {
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read Current User Post",
|
||||
"operationId": "read_current_user_login_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_read_current_user"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
},
|
||||
"/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": [{"OAuth2": []}],
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_read_current_user": {
|
||||
"title": "Body_read_current_user",
|
||||
"required": ["grant_type", "username", "password"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"grant_type": {
|
||||
"title": "Grant_Type",
|
||||
"pattern": "password",
|
||||
"type": "string",
|
||||
},
|
||||
"username": {"title": "Username", "type": "string"},
|
||||
"password": {"title": "Password", "type": "string"},
|
||||
"scope": {"title": "Scope", "type": "string", "default": ""},
|
||||
"client_id": {"title": "Client_Id", "type": "string"},
|
||||
"client_secret": {"title": "Client_Secret", "type": "string"},
|
||||
},
|
||||
},
|
||||
"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"},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"securitySchemes": {
|
||||
"OAuth2": {
|
||||
"type": "oauth2",
|
||||
"flows": {
|
||||
"password": {
|
||||
"scopes": {
|
||||
"read:users": "Read the users",
|
||||
"write:users": "Create users",
|
||||
},
|
||||
"tokenUrl": "/token",
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_oauth2():
|
||||
response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "Bearer footokenbar"}
|
||||
|
||||
|
||||
def test_security_oauth2_password_other_header():
|
||||
response = client.get("/users/me", headers={"Authorization": "Other footokenbar"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "Other footokenbar"}
|
||||
|
||||
|
||||
def test_security_oauth2_password_bearer_no_header():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
required_params = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_incorrect = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": 'string does not match regex "password"',
|
||||
"type": "value_error.str.regex",
|
||||
"ctx": {"pattern": "password"},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data,expected_status,expected_response",
|
||||
[
|
||||
(None, 422, required_params),
|
||||
({"username": "johndoe", "password": "secret"}, 422, grant_type_required),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
|
||||
422,
|
||||
grant_type_incorrect,
|
||||
),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "password"},
|
||||
200,
|
||||
{
|
||||
"grant_type": "password",
|
||||
"username": "johndoe",
|
||||
"password": "secret",
|
||||
"scopes": [],
|
||||
"client_id": None,
|
||||
"client_secret": None,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_strict_login(data, expected_status, expected_response):
|
||||
response = client.post("/login", data=data)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
74
tests/test_security_openid_connect.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security.open_id_connect_url import OpenIdConnect
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
oid = OpenIdConnect(openIdConnectUrl="/openid")
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: str = Security(oid)):
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
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": [{"OpenIdConnect": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"OpenIdConnect": {"type": "openIdConnect", "openIdConnectUrl": "/openid"}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_oauth2():
|
||||
response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "Bearer footokenbar"}
|
||||
|
||||
|
||||
def test_security_oauth2_password_other_header():
|
||||
response = client.get("/users/me", headers={"Authorization": "Other footokenbar"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "Other footokenbar"}
|
||||
|
||||
|
||||
def test_security_oauth2_password_bearer_no_header():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
51
tests/test_serialize_response.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float = None
|
||||
owner_ids: List[int] = None
|
||||
|
||||
|
||||
@app.get("/items/invalid", response_model=Item)
|
||||
def get_invalid():
|
||||
return {"name": "invalid", "price": "foo"}
|
||||
|
||||
|
||||
@app.get("/items/innerinvalid", response_model=Item)
|
||||
def get_innerinvalid():
|
||||
return {"name": "double invalid", "price": "foo", "owner_ids": ["foo", "bar"]}
|
||||
|
||||
|
||||
@app.get("/items/invalidlist", response_model=List[Item])
|
||||
def get_invalidlist():
|
||||
return [
|
||||
{"name": "foo"},
|
||||
{"name": "bar", "price": "bar"},
|
||||
{"name": "baz", "price": "baz"},
|
||||
]
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_invalid():
|
||||
with pytest.raises(ValidationError):
|
||||
client.get("/items/invalid")
|
||||
|
||||
|
||||
def test_double_invalid():
|
||||
with pytest.raises(ValidationError):
|
||||
client.get("/items/innerinvalid")
|
||||
|
||||
|
||||
def test_invalid_list():
|
||||
with pytest.raises(ValidationError):
|
||||
client.get("/items/invalidlist")
|
||||
@@ -0,0 +1,40 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from application_configuration.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {
|
||||
"title": "My Super Project",
|
||||
"version": "2.5.0",
|
||||
"description": "This is a very fancy project, with auto docs for the API and everything",
|
||||
},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Items Get",
|
||||
"operationId": "read_items_items__get",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_items():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"name": "Foo"}]
|
||||
@@ -83,7 +83,7 @@ openapi_schema = {
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_scheme():
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
@@ -172,3 +172,9 @@ def test_post_body(path, body, expected_status, expected_response):
|
||||
response = client.post(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
|
||||
|
||||
def test_post_broken_body():
|
||||
response = client.post("/items/", data={"name": "Foo", "price": 50.5})
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "There was an error parsing the body"}
|
||||
|
||||
@@ -101,7 +101,7 @@ openapi_schema = {
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_scheme():
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
@@ -143,6 +143,5 @@ item_id_not_int = {
|
||||
)
|
||||
def test_post_body(path, body, expected_status, expected_response):
|
||||
response = client.put(path, json=body)
|
||||
print(response.text)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
@@ -8,8 +6,6 @@ from body_schema.tutorial001 import app
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
print(sys.path)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
@@ -111,8 +107,8 @@ openapi_schema = {
|
||||
}
|
||||
|
||||
|
||||
def openapi_schema():
|
||||
response = client.put("/openapi.json")
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
36
tests/test_tutorial/test_custom_response/test_tutorial001.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from custom_response.tutorial001 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": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Items Get",
|
||||
"operationId": "read_items_items__get",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_get_custom_response():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"item_id": "Foo"}]
|
||||
47
tests/test_tutorial/test_custom_response/test_tutorial004.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from custom_response.tutorial004 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": {"text/html": {"schema": {"type": "string"}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Items Get",
|
||||
"operationId": "read_items_items__get",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
html_contents = """
|
||||
<html>
|
||||
<head>
|
||||
<title>Some HTML in here</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Look ma! HTML!</h1>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_get_custom_response():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.text == html_contents
|
||||
@@ -84,6 +84,12 @@ openapi_schema = {
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_status,expected_response",
|
||||
[
|
||||
|
||||
144
tests/test_tutorial/test_dependencies/test_tutorial004.py
Normal file
@@ -0,0 +1,144 @@
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from dependencies.tutorial004 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 Get",
|
||||
"operationId": "read_items_items__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": {
|
||||
"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
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items",
|
||||
200,
|
||||
{
|
||||
"items": [
|
||||
{"item_name": "Foo"},
|
||||
{"item_name": "Bar"},
|
||||
{"item_name": "Baz"},
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items?q=foo",
|
||||
200,
|
||||
{
|
||||
"items": [
|
||||
{"item_name": "Foo"},
|
||||
{"item_name": "Bar"},
|
||||
{"item_name": "Baz"},
|
||||
],
|
||||
"q": "foo",
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items?q=foo&skip=1",
|
||||
200,
|
||||
{"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"},
|
||||
),
|
||||
(
|
||||
"/items?q=bar&limit=2",
|
||||
200,
|
||||
{"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"},
|
||||
),
|
||||
(
|
||||
"/items?q=bar&skip=1&limit=1",
|
||||
200,
|
||||
{"items": [{"item_name": "Bar"}], "q": "bar"},
|
||||
),
|
||||
(
|
||||
"/items?limit=1&q=bar&skip=1",
|
||||
200,
|
||||
{"items": [{"item_name": "Bar"}], "q": "bar"},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_get(path, expected_status, expected_response):
|
||||
response = client.get(path)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
136
tests/test_tutorial/test_extra_data_types/test_tutorial001.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from extra_data_types.tutorial001 import app
|
||||
|
||||
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": "Read Items Put",
|
||||
"operationId": "read_items_items__item_id__put",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "Item_Id",
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Body_read_items"}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_read_items": {
|
||||
"title": "Body_read_items",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start_datetime": {
|
||||
"title": "Start_Datetime",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
},
|
||||
"end_datetime": {
|
||||
"title": "End_Datetime",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
},
|
||||
"repeat_at": {
|
||||
"title": "Repeat_At",
|
||||
"type": "string",
|
||||
"format": "time",
|
||||
},
|
||||
"process_after": {
|
||||
"title": "Process_After",
|
||||
"type": "number",
|
||||
"format": "time-delta",
|
||||
},
|
||||
},
|
||||
},
|
||||
"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_extra_types():
|
||||
item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e"
|
||||
data = {
|
||||
"start_datetime": "2018-12-22T14:00:00+00:00",
|
||||
"end_datetime": "2018-12-24T15:00:00+00:00",
|
||||
"repeat_at": "15:30:00",
|
||||
"process_after": 300,
|
||||
}
|
||||
expected_response = data.copy()
|
||||
expected_response.update(
|
||||
{
|
||||
"start_process": "2018-12-22T14:05:00+00:00",
|
||||
"duration": 176_100,
|
||||
"item_id": item_id,
|
||||
}
|
||||
)
|
||||
response = client.put(f"/items/{item_id}", json=data)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == expected_response
|
||||
@@ -1,5 +1,3 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
@@ -8,8 +6,6 @@ from header_params.tutorial001 import app
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
print(sys.path)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from path_operation_advanced_configuration.tutorial001 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": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Items Get",
|
||||
"operationId": "some_specific_id_you_define",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_get():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"item_id": "Foo"}]
|
||||
@@ -0,0 +1,23 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from path_operation_advanced_configuration.tutorial002 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_get():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"item_id": "Foo"}]
|
||||