Compare commits

...

42 Commits

Author SHA1 Message Date
Sebastián Ramírez
5278314f2f 🔖 Bump version, new security features and bug fixes 2018-12-28 20:40:40 +04:00
Sebastián Ramírez
4a0316bcfe 🎨 Add missing type definition 2018-12-28 20:39:04 +04:00
Sebastián Ramírez
0393a093d3 Improve security utilities and add tests 2018-12-28 20:35:48 +04:00
Sebastián Ramírez
27f530a7ff 📝 Update docs, clarify what's a schema 2018-12-28 16:32:03 +04:00
Sebastián Ramírez
c3e5e65093 🎨 Fix missing format 2018-12-28 16:11:45 +04:00
Sebastián Ramírez
804ec460fc ⬆️ Add tests, fix issues and update Pydantic 2018-12-28 16:10:29 +04:00
Sebastián Ramírez
0125ea4f83 📝 Update tutorials 2018-12-28 16:03:54 +04:00
Sebastián Ramírez
216770118a ✏️ Fix typos 2018-12-27 17:25:39 +04:00
Sebastián Ramírez
a935d66b10 📝 Update docs about alternatives, inspiration and benchmarks 2018-12-27 17:14:46 +04:00
Sebastián Ramírez
dd2541bc97 📝 Improve explanation of request bodies 2018-12-26 19:01:15 +04:00
Sebastián Ramírez
098e629344 🔖 Bump version, after changes in OAuth2 utils 2018-12-24 20:21:28 +04:00
Sebastián Ramírez
bbe5f28b77 📝 Add docs for OAuth2 security 2018-12-24 20:20:48 +04:00
Sebastián Ramírez
4a0922ebab ♻️ Update OAuth2 class utilities to be dependencies 2018-12-24 20:20:21 +04:00
Sebastián Ramírez
8f16868c6a Add passlib and pyjwt to development dependencies 2018-12-24 20:19:05 +04:00
Sebastián Ramírez
bc3e7f2bbc 🔖 Version bump, fixing several issues, lots of docs and tests 2018-12-24 09:35:20 +04:00
Sebastián Ramírez
58848be2de Add pending tests to temporal dir 2018-12-24 09:35:02 +04:00
Sebastián Ramírez
cfb65d0e15 🐛 Fix utility OAuth2PasswordRequestForm to use forms
and be used as a dependency
2018-12-24 09:34:28 +04:00
Sebastián Ramírez
855daa2e53 📝 Add tutorial for complete OAuth2 password flow 2018-12-24 09:33:48 +04:00
Sebastián Ramírez
de54e85152 📝 Add Security tutorial: Get current user 2018-12-24 08:03:59 +04:00
Sebastián Ramírez
b8d3070daf 📝 Add first Security tutorials 2018-12-23 23:25:57 +04:00
Sebastián Ramírez
471c9cfc2d 📝 Add example screenshot for dependencies 2018-12-23 21:29:59 +04:00
Sebastián Ramírez
b79c13baed 📝 Update and add docs for dependencies 2018-12-23 21:21:37 +04:00
Sebastián Ramírez
332ee4aee1 📝 Update and clarify first-steps tutorial 2018-12-23 18:42:29 +04:00
Sebastián Ramírez
ad40f4a457 📝 Fix double editor screenshot 2018-12-22 20:23:24 +04:00
Sebastián Ramírez
6b9931f882 Add tests for metadata 2018-12-22 18:47:05 +04:00
Sebastián Ramírez
4c51bb6714 Test extra routes, with parameters directly 2018-12-22 18:30:34 +04:00
Sebastián Ramírez
57ff677027 Add tests for validation errors in response 2018-12-22 18:20:01 +04:00
Sebastián Ramírez
613c3f3e95 Test all HTTP methods 2018-12-22 18:18:19 +04:00
Sebastián Ramírez
bf6d923ca8 Add ujson for local development 2018-12-22 17:23:51 +04:00
Sebastián Ramírez
252188c686 Update tests for HTML content and remove unneeded tests 2018-12-22 17:23:04 +04:00
Sebastián Ramírez
510fec9bee ♻️ Refactor jsonable_encoder and test it
with nested arbitrary classes
2018-12-22 17:15:04 +04:00
Sebastián Ramírez
a73709507c Add docs, tests and fixes for extra data types
including refactor of jsonable_encoder to allow other object and model types
2018-12-22 14:35:48 +04:00
Sebastián Ramírez
75407b9295 🚨 Fix mypy type errors 2018-12-22 09:05:13 +04:00
Sebastián Ramírez
3180f35bdd Fix OpenAPI test for body schema 2018-12-22 09:00:58 +04:00
Sebastián Ramírez
d498b7feb3 Add tests for response_model 2018-12-22 08:54:52 +04:00
Sebastián Ramírez
3269e6a95c Test custom responses 2018-12-22 08:47:44 +04:00
Sebastián Ramírez
f1808de18e Add tests for form and files 2018-12-22 08:39:26 +04:00
Sebastián Ramírez
748dc375db 🐛 Fix Form and File params must always be embeded
and add tests for forms and files
2018-12-22 08:24:48 +04:00
Sebastián Ramírez
b38fb937b0 🔇 Remove debugging prints 2018-12-22 08:21:02 +04:00
Sebastián Ramírez
23ef570bf6 Add test-cov-html script for local coverage
analysis and debugging
2018-12-22 07:42:24 +04:00
Sebastián Ramírez
c25a71e352 🐛 Re-implement check for body as a workaround
while encode/starlette#287 is merged
2018-12-22 07:40:56 +04:00
Sebastián Ramírez
0c5e684ff9 📝 Add Project Generation section 2018-12-21 20:27:03 +04:00
114 changed files with 4898 additions and 781 deletions

View File

@@ -21,6 +21,7 @@ mkdocs-material = "*"
markdown-include = "*"
autoflake = "*"
email-validator = "*"
ujson = "*"
[packages]
starlette = "*"

13
Pipfile.lock generated
View File

@@ -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",

View File

@@ -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:
![editor support](img/vscode-completion.png)
![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png)
@@ -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
View 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
View 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.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

View File

@@ -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:
![editor support](img/vscode-completion.png)
![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png)
@@ -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

View 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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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}

View 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}

View 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,
}

View File

@@ -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]

View File

@@ -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"}

View File

@@ -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)

View File

@@ -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!}
```

View File

@@ -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`:

View File

@@ -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`

View 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.

View 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.

View File

@@ -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.

View File

@@ -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.

View 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.

View 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!}
```

View File

@@ -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.

View File

@@ -57,6 +57,32 @@ You will see the alternative automatic documentation (provided by <a href="https
![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png)
### 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`).

View File

@@ -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
```

View File

@@ -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!}
```

View File

@@ -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:

View File

@@ -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.

View 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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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!}
```

View File

@@ -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

View File

@@ -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(

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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
View 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

View File

@@ -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
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -e
set -x
bash scripts/test.sh --cov-report=html

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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"

View 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"}

View 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
View 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"}

View File

@@ -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):

View File

@@ -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"}

View 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"}

View 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"}

View 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"}

View 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

View 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"}

View 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")

View File

@@ -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"}]

View File

@@ -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"}

View File

@@ -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

View File

@@ -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

View File

View 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"}]

View 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

View File

@@ -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",
[

View 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

View File

View 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

View File

@@ -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"},

View File

@@ -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"}]

View File

@@ -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"}]

Some files were not shown because too many files have changed in this diff Show More