Compare commits

...

62 Commits

Author SHA1 Message Date
Sebastián Ramírez
88b31e6a4d 🔖 Release 0.5.1: docs 2019-02-18 22:52:03 +04:00
Sebastián Ramírez
2c3b826810 📝 Add contributing/development docs (#42) 2019-02-18 22:40:31 +04:00
Sebastián Ramírez
aa64eecda6 Update error handling docs, including Starlette's utils (#41)
📝 Update error handling docs, including Starlette's utils
2019-02-18 21:58:21 +04:00
Sebastián Ramírez
712b18a58a 📝 Update docs 2019-02-16 19:36:09 +04:00
Sebastián Ramírez
a809da5567 📝 Add note about path declaration order 2019-02-16 19:23:42 +04:00
Sebastián Ramírez
80b68cd97d 📝 Add section about help/getting help 2019-02-16 18:10:15 +04:00
Sebastián Ramírez
894e131e03 🔖 Release 0.5.0 with new HTTPException 2019-02-16 17:06:31 +04:00
Sebastián Ramírez
8772e2f2ee Add HTTPException with custom headers (#35)
* 📝 Update Release Notes with issue templates

*  Add HTTPException with support for headers

Including docs and tests

* 📝 Update Security docs to use new HTTPException
2019-02-16 17:01:29 +04:00
Sebastián Ramírez
7edbd9345b Update issue templates (#34)
Update issue templates
2019-02-16 14:09:20 +04:00
Sebastián Ramírez
56819fdd89 📝 Update Release Notes 2019-02-16 13:47:05 +04:00
euri10
febf8e7341 📝 Add docs for using the Starlette Request directly (#25)
Add docs for using the Starlette Request directly
2019-02-16 12:44:56 +04:00
Sebastián Ramírez
293ebd7cc2 📝 Update Release Notes 2019-02-15 23:19:19 +04:00
Sebastián Ramírez
54e3949f74 📝 Update SQLAlchemy docs, with current workaround 2019-02-15 22:05:18 +04:00
Sebastián Ramírez
acbcbba94f 🔖 Release 0.4.0 with openapi_prefix, #26 2019-02-14 23:04:55 +04:00
Sebastián Ramírez
f7b7a099c3 📝 Update Release Notes and openapi_prefix docs 2019-02-14 23:02:47 +04:00
Kabir Khan
0ea0d0e82a Add Open API prefix route - correct docs behind reverse proxy (#26)
Add Open API prefix route - correct docs behind reverse proxy.
2019-02-14 22:57:49 +04:00
Sebastián Ramírez
890f1f7899 📝 Add note about DB Browser for SQLite in SQL docs 2019-02-12 23:31:18 +04:00
Sebastián Ramírez
783816a7e3 📝 Update Release Notes 2019-02-12 23:07:54 +04:00
Sebastián Ramírez
7863490c8c 🔖 Release after SQLAlchemy fix: 0.3.0 2019-02-12 23:06:05 +04:00
Sebastián Ramírez
955e9fcb31 Update fix SQLAlchemy support with ORM (#30)
 SQLAlchemy ORM support

Improved jsonable_encoder with SQLAlchemy support, tests running with SQLite, improved and updated SQL docs

*  Add SQLAlchemy to development dependencies (not required for using FastAPI)

*  Add sqlalchemy to testing dependencies (not required to use FastAPI)
2019-02-12 23:02:21 +04:00
Sebastián Ramírez
9484f939ed 🔖 Bump version, after fix, release 2019-02-12 21:46:35 +04:00
Sebastián Ramírez
9745a5d1ae 🐛 Fix jsonable_encoder for models with Config (#29)
but without json_encoders
2019-02-12 21:43:34 +04:00
Sebastián Ramírez
92c825be6a 🔖 Release 0.2.0 2019-02-08 16:09:48 +04:00
euri10
32438c85f6 Using pydantic custom encoders (#21)
Add support for Pydantic custom JSON encoders.
2019-02-08 16:06:19 +04:00
Sebastián Ramírez
02e53fde90 📝 Update release notes 2019-02-08 15:43:00 +04:00
Ken Kinder
902cdaf010 Fix typos (#24)
Fix typos in security section.
2019-02-08 15:41:13 +04:00
Sebastián Ramírez
04d77bb1c4 ✏️ Fix typos in index and alternatives 2019-02-08 15:39:26 +04:00
Sebastián Ramírez
6d9fc08a7e 🚀 Bump version and add Release Notes 2019-02-01 14:23:20 +04:00
euri10
5c9c088a2a Upgrade Starlette version (#17)
Upgrade Starlette version
2019-02-01 14:14:23 +04:00
Sebastián Ramírez
014c7df142 📝 Add Requests to inspiration 2019-01-24 22:31:33 +04:00
Sebastián Ramírez
9259dc228a 📈 Add Analytics to understand docs usage and improvements 2019-01-24 21:58:27 +04:00
Sebastián Ramírez
de431d948d Merge pull request #11 from tiangolo/fix-10
Pin versions of dependencies and bump version
2019-01-23 16:12:09 +01:00
Sebastián Ramírez
3d2c0993c1 📌 Pin versions of dependencies and bump version 2019-01-23 18:57:48 +04:00
Sebastián Ramírez
37bc3614fd 📝 Fix docs clarification about dict unwrapping
in extra-models and simple-oauth2 #7
2019-01-14 23:01:34 +04:00
Sebastián Ramírez
188da34529 📝 Clarify docs, alternatives, about APISpec OAI versions 2019-01-14 21:26:29 +04:00
Sebastián Ramírez
d692c28f52 📝 Add docs for bigger applications and APIRouter
and update tests to match docs
2019-01-14 19:23:38 +04:00
Sebastián Ramírez
8568862a19 📝 Add docs for response status codes 2019-01-14 17:30:55 +04:00
Sebastián Ramírez
dfa067b061 📝 Add screenshot to body-schema tutorial 2019-01-10 20:52:06 +04:00
Sebastián Ramírez
0d1b97fb94 📝 Add docs for deployment, with Docker, HTTPS, etc 2019-01-05 20:24:33 +04:00
Sebastián Ramírez
df1e754380 🔧 Update development environment dependencies 2019-01-05 18:49:50 +04:00
Sebastián Ramírez
e5b341c7dd 🔖 Bump version after fix for constrained bytes 2019-01-05 17:38:59 +04:00
Sebastián Ramírez
577c5a84db 🐛 Fix constrained bytes, from defaults in Pydantic
#2
2019-01-05 17:30:27 +04:00
Sebastián Ramírez
a5cfee434d 📝 Update docs for dependencies 2019-01-05 17:19:41 +04:00
Sebastián Ramírez
9a8349bf96 📝 Improve explanation of dependencies 2019-01-01 19:27:02 +04:00
Sebastián Ramírez
a59408f68c ✏️ Fix typo in dependencies docs 2018-12-30 22:18:45 +04:00
Sebastián Ramírez
3c08b05ea6 🔖 Bump version, after query and header as lists
and bug fixes for Python 3.7
2018-12-30 21:46:49 +04:00
Sebastián Ramírez
60599bad99 🐛 Fix Python 3.7 specific list query handling 2018-12-30 21:43:34 +04:00
Sebastián Ramírez
ccf30b5c2e 📝 Update docs, 100% coverage 2018-12-30 00:17:22 +04:00
Sebastián Ramírez
ca0652aebf 🐛 Fix type checks for Python 3.7 2018-12-30 00:14:39 +04:00
Sebastián Ramírez
be957e7c99 Allow lists of query or header params
and add tests for them
2018-12-30 00:07:31 +04:00
Sebastián Ramírez
90af868146 Add security checks for HTTP utils
and tests for them
2018-12-29 23:04:54 +04:00
Sebastián Ramírez
660f917d79 ✏️ Fix typos and docs notes 2018-12-29 18:43:58 +04:00
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
140 changed files with 5274 additions and 750 deletions

42
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,42 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Create a file with '...'
2. Add a path operation function with '....'
3. Open the browser and call it with a payload of '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment:**
- OS: [e.g. Linux / Windows / macOS]
- FastAPI Version [e.g. 0.3.0], get it with:
```Python
import fastapi
print(fastapi.__version__)
```
- Python version, get it with:
```bash
python --version
```
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE]"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I want to be able to [...] but I can't because [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

17
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,17 @@
---
name: Question
about: Ask a question
title: "[QUESTION]"
labels: question
assignees: ''
---
**Description**
How can I [...]?
Is it possible to [...]?
**Additional context**
Add any other context or screenshots about the feature request here.

10
Pipfile
View File

@@ -5,10 +5,7 @@ verify_ssl = true
[dev-packages]
mypy = "*"
jedi = "*"
black = "*"
prospector = "*"
rope = "*"
jupyter = "*"
better-exceptions = "*"
pytest = "*"
@@ -22,10 +19,13 @@ markdown-include = "*"
autoflake = "*"
email-validator = "*"
ujson = "*"
flake8 = "*"
python-multipart = "*"
sqlalchemy = "*"
[packages]
starlette = "*"
pydantic = "*"
starlette = "==0.10.1"
pydantic = "==0.18.2"
[requires]
python_version = "3.6"

335
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "a0f966a95cb84845ca4aad02c44fc0e7c5e2047fc44dcf19a95a4abaa02d0197"
"sha256": "37b34bb892b6b4dc0f7c941434d0e08199aa7a7ca83efb6294b89ace44168bba"
},
"pipfile-spec": 6,
"requires": {
@@ -26,18 +26,18 @@
},
"pydantic": {
"hashes": [
"sha256:51f879ca4b1d114c9f892737a0d65233251fb00fcd2b6da2be0d277b8ba7d28d",
"sha256:c90c9e5ae2a6a3f59efdcb1505ddfb18be6dc5648b536bf33782269460954cc2"
"sha256:9f023811b6cefd203c5fd8fd15a4152f04e79e531b8f676ab1244dfe06ce8024",
"sha256:edbb08b561feda505374c0f25e4b54466a0a0c702ed6b2efaabdc3890d1a82e7"
],
"index": "pypi",
"version": "==0.16.1"
"version": "==0.18.2"
},
"starlette": {
"hashes": [
"sha256:01f04283b49a8cb0c8921baa90dbafe47e953f0a265f6ebb38176038e4bd9bf8"
"sha256:7cc05c33d00db3b2ddfd7516a737544ed0a34c9dd0ced94076f29b581ce4f532"
],
"index": "pypi",
"version": "==0.9.9"
"version": "==0.10.1"
}
},
"develop": {
@@ -48,19 +48,12 @@
],
"version": "==1.4.3"
},
"astroid": {
"hashes": [
"sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be",
"sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d"
],
"version": "==2.0.4"
},
"atomicwrites": {
"hashes": [
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
],
"version": "==1.2.1"
"version": "==1.3.0"
},
"attrs": {
"hashes": [
@@ -85,10 +78,11 @@
},
"better-exceptions": {
"hashes": [
"sha256:0a73efef96b48f867ea980227ac3b00d36a92754e6d316ad2ee472f136014580"
"sha256:bf79c87659bc849989d726bf0e4a2100edefe7eded112d201f54fe08467fdf63",
"sha256:c196cad849de615abb9f6eb67ca1b83f33b938818f0e2fe8fa157b22aeb7b992"
],
"index": "pypi",
"version": "==0.2.1"
"version": "==0.2.2"
},
"black": {
"hashes": [
@@ -100,10 +94,10 @@
},
"bleach": {
"hashes": [
"sha256:48d39675b80a75f6d1c3bdbffec791cf0bbbab665cf01e20da701c77de278718",
"sha256:73d26f018af5d5adcdabf5c1c974add4361a9c76af215fe32fdec8a6fc5fb9b9"
"sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16",
"sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa"
],
"version": "==3.0.2"
"version": "==3.1.0"
},
"certifi": {
"hashes": [
@@ -162,10 +156,10 @@
},
"decorator": {
"hashes": [
"sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82",
"sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c"
"sha256:33cd704aea07b4c28b3eb2c97d288a06918275dac0ecebdaf1bc8a48d98adb9e",
"sha256:cabb249f4710888a2fc0e13e9a16c343d932033718ff62e1e9bc93a9d3a9122b"
],
"version": "==4.3.0"
"version": "==4.3.2"
},
"defusedxml": {
"hashes": [
@@ -189,12 +183,6 @@
],
"version": "==0.14"
},
"dodgy": {
"hashes": [
"sha256:65e13cf878d7aff129f1461c13cb5fd1bb6dfe66bb5327e09379c3877763280c"
],
"version": "==0.1.9"
},
"email-validator": {
"hashes": [
"sha256:ddc4b5b59fa699bb10127adcf7ad4de78fde4ec539a072b104b8bb16da666ae5"
@@ -204,18 +192,26 @@
},
"entrypoints": {
"hashes": [
"sha256:10ad569bb245e7e2ba425285b9fa3e8178a0dc92fc53b1e1c553805e15a8825b",
"sha256:d2d587dde06f99545fb13a383d2cd336a8ff1f359c5839ce3a64c917d10c029f"
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
],
"version": "==0.2.3"
"version": "==0.3"
},
"flake8": {
"hashes": [
"sha256:c3ba1e130c813191db95c431a18cb4d20a468e98af7a77e2181b68574481ad36",
"sha256:fd9ddf503110bf3d8b1d270e8c673aab29ccb3dd6abf29bae1f54e5116ab4a91"
],
"index": "pypi",
"version": "==3.7.5"
},
"flit": {
"hashes": [
"sha256:6aefa6ff89a993af7a7af40d3df3d0387d6663df99797981ec41b1431ec6d1e1",
"sha256:9969db9708305b64fd8acf20043fcff144f910222397a221fd29871f02ed4a6f"
"sha256:1d93f7a833ed8a6e120ddc40db5c4763bc39bccc75c05081ec8285ece718aefb",
"sha256:6f6f0fb83c51ffa3a150fa41b5ac118df9ea4a87c2c06dff4ebf9adbe7b52b36"
],
"index": "pypi",
"version": "==1.2.1"
"version": "==1.3"
},
"idna": {
"hashes": [
@@ -267,7 +263,6 @@
"sha256:571702b5bd167911fe9036e5039ba67f820d6502832285cde8c881ab2b2149fd",
"sha256:c8481b5e59d34a5c7c42e98f6625e633f6ef59353abea6437472c7ec2093f191"
],
"index": "pypi",
"version": "==0.13.2"
},
"jinja2": {
@@ -279,10 +274,10 @@
},
"jsonschema": {
"hashes": [
"sha256:3ae8afd6f4ca6417f14bf43ef61341311598f14234cdb4174fe43d42b236a3c8",
"sha256:dfd8426040892c8d0ef6da574085f282569f189cb24b70091a66c21c12d6705e"
"sha256:683fe7ed58763ea0be572de5aad47cd3cc1297640916f9a8ccd222b287da7d2f",
"sha256:b42d7a292addb57370e6260bcbadb77e00a899fe6ec998c453f45893c41c658b"
],
"version": "==3.0.0a3"
"version": "==3.0.0b3"
},
"jupyter": {
"hashes": [
@@ -314,40 +309,6 @@
],
"version": "==4.4.0"
},
"lazy-object-proxy": {
"hashes": [
"sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33",
"sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39",
"sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019",
"sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088",
"sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b",
"sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e",
"sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6",
"sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b",
"sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5",
"sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff",
"sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd",
"sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7",
"sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff",
"sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d",
"sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2",
"sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35",
"sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4",
"sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514",
"sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252",
"sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109",
"sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f",
"sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c",
"sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92",
"sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577",
"sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d",
"sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d",
"sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f",
"sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a",
"sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b"
],
"version": "==1.3.1"
},
"livereload": {
"hashes": [
"sha256:29cadfabcedd12eed792e0131991235b9d4764d4474bed75cf525f57109ec0a2",
@@ -426,27 +387,26 @@
},
"mkdocs-material": {
"hashes": [
"sha256:037712dd7e2128a9b596943bcd92ebc9ad28800906dcee447e2fc008dd9dbbff",
"sha256:52522c8553a6d6da8fca2afe43297e8f88acdcf8ccf752a118148f1328f761e2"
"sha256:4b4af83c704d2bab41be3a5228e800a5e1157003368fbf548d95073ce19e0f61",
"sha256:86c0042c803586985bf79c99962ebd4644c3f0ff095d5df541f09fa48f5b62cc"
],
"index": "pypi",
"version": "==3.1.0"
"version": "==3.3.0"
},
"more-itertools": {
"hashes": [
"sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
"sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
"sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
"sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40",
"sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"
],
"version": "==4.3.0"
"version": "==6.0.0"
},
"mypy": {
"hashes": [
"sha256:12d965c9c4e8a625673aec493162cf390e66de12ef176b1f4821ac00d55f3ab3",
"sha256:38d5b5f835a81817dcc0af8d155bce4e9aefa03794fe32ed154d6612e83feafa"
"sha256:308c274eb8482fbf16006f549137ddc0d69e5a589465e37b99c4564414363ca7",
"sha256:e80fd6af34614a0e898a57f14296d0dacb584648f0339c2e000ddbf0f4cc2f8d"
],
"index": "pypi",
"version": "==0.650"
"version": "==0.670"
},
"mypy-extensions": {
"hashes": [
@@ -457,10 +417,10 @@
},
"nbconvert": {
"hashes": [
"sha256:08d21cf4203fabafd0d09bbd63f06131b411db8ebeede34b0fd4be4548351779",
"sha256:a8a2749f972592aa9250db975304af6b7337f32337e523a2c995cc9e12c07807"
"sha256:302554a2e219bc0fc84f3edd3e79953f3767b46ab67626fdec16e38ba3f7efe4",
"sha256:5de8fb2284422272a1d45abc77c07b888127550a6d602ce619592a2b08a474ff"
],
"version": "==5.4.0"
"version": "==5.4.1"
},
"nbformat": {
"hashes": [
@@ -484,17 +444,10 @@
},
"parso": {
"hashes": [
"sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2",
"sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24"
"sha256:6ecf7244be8e7283ec9009c72d074830e7e0e611c974f813d76db0390a4e0dd6",
"sha256:8162be7570ffb34ec0b8d215d7f3b6c5fab24f51eb3886d6dee362de96b6db94"
],
"version": "==0.3.1"
},
"pep8-naming": {
"hashes": [
"sha256:1b419fa45b68b61cd8c5daf4e0c96d28915ad14d3d5f35fcc1e7e95324a33a2e",
"sha256:4eedfd4c4b05e48796f74f5d8628c068ff788b9c2b08471ad408007fc6450e5a"
],
"version": "==0.4.1"
"version": "==0.3.3"
},
"pexpect": {
"hashes": [
@@ -513,10 +466,10 @@
},
"pluggy": {
"hashes": [
"sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095",
"sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f"
"sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616",
"sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"
],
"version": "==0.8.0"
"version": "==0.8.1"
},
"prometheus-client": {
"hashes": [
@@ -526,18 +479,11 @@
},
"prompt-toolkit": {
"hashes": [
"sha256:c1d6aff5252ab2ef391c2fe498ed8c088066f66bc64a8d5c095bbf795d9fec34",
"sha256:d4c47f79b635a0e70b84fdb97ebd9a274203706b1ee5ed44c10da62755cf3ec9",
"sha256:fd17048d8335c1e6d5ee403c3569953ba3eb8555d710bfc548faf0712666ea39"
"sha256:88002cc618cacfda8760c4539e76c3b3f148ecdb7035a3d422c7ecdc90c2a3ba",
"sha256:c6655a12e9b08edb8cf5aeab4815fd1e1bdea4ad73d3bbf269cf2e0c4eb75d5e",
"sha256:df5835fb8f417aa55e5cafadbaeb0cf630a1e824aad16989f9f0493e679ec010"
],
"version": "==2.0.7"
},
"prospector": {
"hashes": [
"sha256:877d8d361a5c0e04c8587718c22c5d671afcf814945c96b3e592836d772943fd"
],
"index": "pypi",
"version": "==1.1.6.2"
"version": "==2.0.8"
},
"ptyprocess": {
"hashes": [
@@ -556,25 +502,17 @@
},
"pycodestyle": {
"hashes": [
"sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83",
"sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a"
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
],
"version": "==2.4.0"
},
"pydocstyle": {
"hashes": [
"sha256:2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8",
"sha256:5741c85e408f9e0ddf873611085e819b809fca90b619f5fd7f34bd4959da3dd4",
"sha256:ed79d4ec5e92655eccc21eb0c6cf512e69512b4a97d215ace46d17e4990f2039"
],
"version": "==3.0.0"
"version": "==2.5.0"
},
"pyflakes": {
"hashes": [
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
"sha256:5e8c00e30c464c99e0b501dc160b13a14af7f27d4dffb529c556e30a159e231d",
"sha256:f277f9ca3e55de669fba45b7393a1449009cff5a37d1af10ebb76c52765269cd"
],
"version": "==1.6.0"
"version": "==2.1.0"
},
"pygments": {
"hashes": [
@@ -583,38 +521,6 @@
],
"version": "==2.3.1"
},
"pylint": {
"hashes": [
"sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec",
"sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb"
],
"version": "==2.1.1"
},
"pylint-celery": {
"hashes": [
"sha256:41e32094e7408d15c044178ea828dd524beedbdbe6f83f712c5e35bde1de4beb"
],
"version": "==0.3"
},
"pylint-django": {
"hashes": [
"sha256:5dc5f85caef2c5f9e61622b9cbd89d94edd3dcf546939b2974d18de4fa90d676",
"sha256:bf313f10b68ed915a34f0f475cc9ff8c7f574a95302beb48b79c5993f7efd84c"
],
"version": "==2.0.2"
},
"pylint-flask": {
"hashes": [
"sha256:8fcdbb7cbf13d8c2ac1f2230b2aa1c1b83bb3ca2bd8b76f95561cb8757a305ec"
],
"version": "==0.5"
},
"pylint-plugin-utils": {
"hashes": [
"sha256:8ad25a82bcce390d1d6b7c006c123e0cb18051839c9df7b8bdb7823c53fe676e"
],
"version": "==0.4"
},
"pymdown-extensions": {
"hashes": [
"sha256:25b0a7967fa697b5035e23340a48594e3e93acb10b06d74574218ace3347d1df",
@@ -624,32 +530,39 @@
},
"pyrsistent": {
"hashes": [
"sha256:59880cc33ac293515892b2969aa8f4ed2cec592cbd0be4c4e20f2410468bbc62"
"sha256:07f7ae71291af8b0dbad8c2ab630d8223e4a8c4e10fc37badda158c02e753acf"
],
"version": "==0.14.8"
"version": "==0.14.10"
},
"pytest": {
"hashes": [
"sha256:f689bf2fc18c4585403348dd56f47d87780bf217c53ed9ae7a3e2d7faa45f8e9",
"sha256:f812ea39a0153566be53d88f8de94839db1e8a05352ed8a49525d7d7f37861e9"
"sha256:65aeaa77ae87c7fc95de56285282546cfa9c886dc8e5dc78313db1c25e21bc07",
"sha256:6ac6d467d9f053e95aaacd79f831dbecfe730f419c6c7022cb316b365cd9199d"
],
"index": "pypi",
"version": "==4.0.2"
"version": "==4.2.0"
},
"pytest-cov": {
"hashes": [
"sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7",
"sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762"
"sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33",
"sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f"
],
"index": "pypi",
"version": "==2.6.0"
"version": "==2.6.1"
},
"python-dateutil": {
"hashes": [
"sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93",
"sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02"
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
"sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
],
"version": "==2.7.5"
"version": "==2.8.0"
},
"python-multipart": {
"hashes": [
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
],
"index": "pypi",
"version": "==0.0.5"
},
"pytoml": {
"hashes": [
@@ -712,19 +625,6 @@
"index": "pypi",
"version": "==2.21.0"
},
"requirements-detector": {
"hashes": [
"sha256:9fbc4b24e8b7c3663aff32e3eba34596848c6b91bd425079b386973bd8d08931"
],
"version": "==0.6"
},
"rope": {
"hashes": [
"sha256:a108c445e1cd897fe19272ab7877d172e7faf3d4148c80e7d20faba42ea8f7b2"
],
"index": "pypi",
"version": "==0.11.0"
},
"send2trash": {
"hashes": [
"sha256:60001cc07d707fe247c94f74ca6ac0d3255aabcb930529690897ca2a39db28b2",
@@ -732,12 +632,6 @@
],
"version": "==1.5.0"
},
"setoptconf": {
"hashes": [
"sha256:5b0b5d8e0077713f5d5152d4f63be6f048d9a1bb66be15d089a11c898c3cf49c"
],
"version": "==0.2.0"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
@@ -745,12 +639,12 @@
],
"version": "==1.12.0"
},
"snowballstemmer": {
"sqlalchemy": {
"hashes": [
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
"sha256:7dede29f121071da9873e7b8c98091874617858e790dc364ffaab4b09d81216c"
],
"version": "==1.2.1"
"index": "pypi",
"version": "==1.3.0b3"
},
"terminado": {
"hashes": [
@@ -775,15 +669,9 @@
},
"tornado": {
"hashes": [
"sha256:0662d28b1ca9f67108c7e3b77afabfb9c7e87bde174fbda78186ecedc2499a9d",
"sha256:4e5158d97583502a7e2739951553cbd88a72076f152b4b11b64b9a10c4c49409",
"sha256:732e836008c708de2e89a31cb2fa6c0e5a70cb60492bee6f1ea1047500feaf7f",
"sha256:8154ec22c450df4e06b35f131adc4f2f3a12ec85981a203301d310abf580500f",
"sha256:8e9d728c4579682e837c92fdd98036bd5cdefa1da2aaf6acf26947e6dd0c01c5",
"sha256:d4b3e5329f572f055b587efc57d29bd051589fb5a43ec8898c77a47ec2fa2bbb",
"sha256:e5f2585afccbff22390cddac29849df463b252b711aa2ce7c5f3f342a5b3b444"
"sha256:d3b719a0cb7094e2b1ca94b31f4b601639fa7ad01a548a1a2ccdd6cbdfd56671"
],
"version": "==5.1.1"
"version": "==6.0b1"
},
"traitlets": {
"hashes": [
@@ -794,30 +682,27 @@
},
"typed-ast": {
"hashes": [
"sha256:0555eca1671ebe09eb5f2176723826f6f44cca5060502fea259de9b0e893ab53",
"sha256:0ca96128ea66163aea13911c9b4b661cb345eb729a20be15c034271360fc7474",
"sha256:16ccd06d614cf81b96de42a37679af12526ea25a208bce3da2d9226f44563868",
"sha256:1e21ae7b49a3f744958ffad1737dfbdb43e1137503ccc59f4e32c4ac33b0bd1c",
"sha256:37670c6fd857b5eb68aa5d193e14098354783b5138de482afa401cc2644f5a7f",
"sha256:46d84c8e3806619ece595aaf4f37743083f9454c9ea68a517f1daa05126daf1d",
"sha256:5b972bbb3819ece283a67358103cc6671da3646397b06e7acea558444daf54b2",
"sha256:6306ffa64922a7b58ee2e8d6f207813460ca5a90213b4a400c2e730375049246",
"sha256:6cb25dc95078931ecbd6cbcc4178d1b8ae8f2b513ae9c3bd0b7f81c2191db4c6",
"sha256:7e19d439fee23620dea6468d85bfe529b873dace39b7e5b0c82c7099681f8a22",
"sha256:7f5cd83af6b3ca9757e1127d852f497d11c7b09b4716c355acfbebf783d028da",
"sha256:81e885a713e06faeef37223a5b1167615db87f947ecc73f815b9d1bbd6b585be",
"sha256:94af325c9fe354019a29f9016277c547ad5d8a2d98a02806f27a7436b2da6735",
"sha256:b1e5445c6075f509d5764b84ce641a1535748801253b97f3b7ea9d948a22853a",
"sha256:cb061a959fec9a514d243831c514b51ccb940b58a5ce572a4e209810f2507dcf",
"sha256:cc8d0b703d573cbabe0d51c9d68ab68df42a81409e4ed6af45a04a95484b96a5",
"sha256:da0afa955865920edb146926455ec49da20965389982f91e926389666f5cf86a",
"sha256:dc76738331d61818ce0b90647aedde17bbba3d3f9e969d83c1d9087b4f978862",
"sha256:e7ec9a1445d27dbd0446568035f7106fa899a36f55e52ade28020f7b3845180d",
"sha256:f741ba03feb480061ab91a465d1a3ed2d40b52822ada5b4017770dfcb88f839f",
"sha256:fe800a58547dd424cd286b7270b967b5b3316b993d86453ede184a17b5a6b17d"
"sha256:035a54ede6ce1380599b2ce57844c6554666522e376bd111eb940fbc7c3dad23",
"sha256:037c35f2741ce3a9ac0d55abfcd119133cbd821fffa4461397718287092d9d15",
"sha256:049feae7e9f180b64efacbdc36b3af64a00393a47be22fa9cb6794e68d4e73d3",
"sha256:19228f7940beafc1ba21a6e8e070e0b0bfd1457902a3a81709762b8b9039b88d",
"sha256:2ea681e91e3550a30c2265d2916f40a5f5d89b59469a20f3bad7d07adee0f7a6",
"sha256:3a6b0a78af298d82323660df5497bcea0f0a4a25a0b003afd0ce5af049bd1f60",
"sha256:5385da8f3b801014504df0852bf83524599df890387a3c2b17b7caa3d78b1773",
"sha256:606d8afa07eef77280c2bf84335e24390055b478392e1975f96286d99d0cb424",
"sha256:69245b5b23bbf7fb242c9f8f08493e9ecd7711f063259aefffaeb90595d62287",
"sha256:6f6d839ab09830d59b7fa8fb6917023d8cb5498ee1f1dbd82d37db78eb76bc99",
"sha256:730888475f5ac0e37c1de4bd05eeb799fdb742697867f524dc8a4cd74bcecc23",
"sha256:9819b5162ffc121b9e334923c685b0d0826154e41dfe70b2ede2ce29034c71d8",
"sha256:9e60ef9426efab601dd9aa120e4ff560f4461cf8442e9c0a2b92548d52800699",
"sha256:af5fbdde0690c7da68e841d7fc2632345d570768ea7406a9434446d7b33b0ee1",
"sha256:b64efdbdf3bbb1377562c179f167f3bf301251411eb5ac77dec6b7d32bcda463",
"sha256:bac5f444c118aeb456fac1b0b5d14c6a71ea2a42069b09c176f75e9bd4c186f6",
"sha256:bda9068aafb73859491e13b99b682bd299c1b5fd50644d697533775828a28ee0",
"sha256:d659517ca116e6750101a1326107d3479028c5191f0ecee3c7203c50f5b915b0",
"sha256:eddd3fb1f3e0f82e5915a899285a39ee34ce18fd25d89582bc89fc9fb16cd2c6"
],
"markers": "python_version < '3.7' and implementation_name == 'cpython'",
"version": "==1.1.1"
"version": "==1.3.1"
},
"ujson": {
"hashes": [
@@ -853,12 +738,6 @@
"sha256:fa618be8435447a017fd1bf2c7ae922d0428056cfc7449f7a8641edf76b48265"
],
"version": "==3.4.2"
},
"wrapt": {
"hashes": [
"sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6"
],
"version": "==1.10.11"
}
}
}

View File

@@ -24,11 +24,11 @@
---
FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+.
FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
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>
@@ -166,7 +166,7 @@ You will see the alternative automatic documentation (provided by <a href="https
## Example upgrade
Now modify the file `main.py` to recive a body from a `PUT` request.
Now modify the file `main.py` to receive a body from a `PUT` request.
Declare the body using standard Python types, thanks to Pydantic.
@@ -257,7 +257,7 @@ item: Item
* Validation of data:
* Automatic and clear errors when the data is invalid.
* Validation even for deeply nested JSON objects.
* <abbr title="also known as: serialization, parsing, marshalling">Conversion</abbr> of input data: coming from the network, to Python data and types. Reading from:
* <abbr title="also known as: serialization, parsing, marshalling">Conversion</abbr> of input data: coming from the network to Python data and types. Reading from:
* JSON.
* Path parameters.
* Query parameters.
@@ -291,8 +291,8 @@ 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:
* Interactive documentation sytems.
* Document everything with OpenAPI, that can be used by:
* Interactive documentation systems.
* Automatic client code generation systems, for many languages.
* Provide 2 interactive documentation web interfaces directly.
@@ -329,7 +329,7 @@ For a more complete example including more features, see the <a href="https://fa
**Spoiler alert**: the tutorial - user guide includes:
* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**.
* How to set **validation constrains** as `maximum_length` or `regex`.
* How to set **validation constraints** as `maximum_length` or `regex`.
* A very powerful and easy to use **<abbr title="also known as components, resources, providers, services, injectables">Dependency Injection</abbr>** system.
* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth.
* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic).
@@ -342,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
@@ -366,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

374
docs/alternatives.md Normal file
View File

@@ -0,0 +1,374 @@
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="http://docs.python-requests.org" target="_blank">Requests</a>
**FastAPI** is not actually an alternative to **Requests**. Their scope is very different.
It would actually be common to use Requests *inside* of a FastAPI application.
But still, FastAPI got quite some inspiration from Requests.
**Requests** is a library to *interact* with APIs (as a client), while **FastAPI** is a library to *build* APIs (as a server).
They are, more or less, at opposite ends, complementing each other.
Requests has a very simple and intuitive design, it's very easy to use, with sensible defaults. But at the same time, it's very powerful and customizable.
That's why, as said in the official website:
> Requests is one of the most downloaded Python packages of all time
The way you use it is very simple. For example, to do a `GET` request, you would write:
```Python
response = requests.get("http://example.com/some/url")
```
The FastAPI counterpart API path operation could look like:
```Python hl_lines="1"
@app.get("/some/url")
def read_url():
return {"message": "Hello World"}
```
See the similarities in `requests.get(...)` and `@app.get(...)`.
!!! check "Inspired **FastAPI** to"
* Have a simple and intuitive API.
* Use HTTP method names (operations) directly, in a straightforward and intuitive way.
* Have sensible defaults, but powerful customizations.
### <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 OpenAPI schemas.
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 OpenAPI 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.

123
docs/contributing.md Normal file
View File

@@ -0,0 +1,123 @@
First, you might want to see the basic ways to <a href="https://fastapi.tiangolo.com/help-fastapi/" target="_blank">help FastAPI and get help</a>.
## Developing
If you already cloned the repository and you know that you need to deep dive in the code, here are some guidelines to set up your environment.
### Pipenv
If you are using <a href="https://pipenv.readthedocs.io/en/latest/" target="_blank">Pipenv</a>, you can create a virtual environment and install the packages with:
```bash
pipenv install --dev
```
Then you can activate that virtual environment with:
```bash
pipenv shell
```
### No Pipenv
If you are not using Pipenv, you can create a virtual environment with your preferred tool, and install the packages listed in the file `Pipfile`.
### Flit
**FastAPI** uses <a href="https://flit.readthedocs.io/en/latest/index.html" target="_blank">Flit</a> to build, package and publish the project.
If you installed the development dependencies with one of the methods above, you already have the `flit` command.
To install your local version of FastAPI as a package in your local environment, run:
```bash
flit install --symlink
```
It will install your local FastAPI in your local environment.
#### Using your local FastAPI
If you create a Python file that imports and uses FastAPI, and run it with the Python from your local environment, it will use your local FastAPI source code.
And if you update that local FastAPI source code, as it is installed with `--symlink`, when you run that Python file again, it will use the fresh version of FastAPI you just edited.
That way, you don't have to "install" your local version to be able to test every change.
### Format
There is a script that you can run that will format and clean all your code:
```bash
bash scripts/lint.sh
```
It will also auto-sort all your imports.
For it to sort them correctly, you need to have FastAPI installed locally in your environment, with the command in the section above:
```bash
flit install --symlink
```
### Docs
The documentation uses <a href="https://www.mkdocs.org/" target="_blank">MkDocs</a>.
All the documentation is in Markdown format in the directory `./docs`.
Many of the tutorials have blocks of code.
In most of the cases, these blocks of code are actual complete applicactions that can be run as is.
In fact, those blocks of code are not written inside the Markdown, they are Python files in the `./docs/src/` directory.
And those Python files are included/injected in the documentation when generating the site.
#### Docs for tests
Most of the tests actually run against the example source files in the documentation.
This helps making sure that:
* The documentation is up to date.
* The documentation examples can be run as is.
* Most of the features are covered by the documentation, ensured by the coverage tests.
During local development, there is a script that builds the site and checks for any changes, live-reloading:
```bash
bash scripts/docs-live.sh
```
It will serve the documentation on `http://0.0.0.0:8008`.
That way, you can edit the documentation/source files and see the changes live.
#### Apps and docs at the same time
And if you run the examples with, e.g.:
```bash
uvicorn tutorial001:app --debug
```
as Uvicorn by default will use the port `8000`, the documentation on port `8008` won't clash.
### Tests
There is a script that you can run locally to test all the code and generate coverage reports in HTML:
```bash
bash scripts/test-cov-html.sh
```
This command generates a directory `./htmlcov/`, if you open the file `./htmlcov/index.html` in your browser, you can explore interactively the regions of code that are covered by the tests, and notice if there is any region missing.

View File

@@ -1 +1,219 @@
Coming soon...
It is recommended to use <a href="https://www.docker.com/" target="_blank">**Docker**</a> for security, replicability, development simplicity, etc.
In this section you'll see instructions and links to guides to know how to:
* Make your **FastAPI** application a Docker image/container with maximum performance. In about **5 min**.
* (Optionally) understand what you, as a developer, need to know about HTTPS.
* Set up a Docker Swarm mode cluster with automatic HTTPS, even on a simple $5 USD/month server. In about **20 min**.
* Generate and deploy a full **FastAPI** application, using your Docker Swarm cluster, with HTTPS, etc. In about **10 min**.
---
You can also easily use **FastAPI** in a standard server directly too (without Docker).
## Docker
If you are using Docker, you can use the official Docker image:
### <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>
This image has an "auto-tuning" mechanism included, so that you can just add your code and get very high performance automatically. And without making sacrifices.
But you can still change and update all the configurations with environment variables or configuration files.
!!! tip
To see all the configurations and options, go to the Docker image page: <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>.
### Build your Image
* Go to your project directory.
* Create a `Dockerfile` with:
```Dockerfile
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
COPY ./app /app
```
* Create an `app` directory and enter in it.
* Create a `main.py` file with:
```Python
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
return {"item_id": item_id, "q": q}
```
* You should now have a directory structure like:
```
.
├── app
│ └── main.py
└── Dockerfile
```
* Go to the project directory (in where your `Dockerfile` is, containing your `app` directory).
* Build your FastAPI image:
```bash
docker build -t myimage .
```
* Run a container based on your image:
```bash
docker run -d --name mycontainer -p 80:80 myimage
```
Now you have an optimized FastAPI server in a Docker container. Auto-tuned for your current server (and number of CPU cores).
### Check it
You should be able to check it in your Docker container's URL, for example: <a href="http://192.168.99.100/items/5?q=somequery" target="_blank">http://192.168.99.100/items/5?q=somequery</a> or <a href="http://127.0.0.1/items/5?q=somequery" target="_blank">http://127.0.0.1/items/5?q=somequery</a> (or equivalent, using your Docker host).
You will see something like:
```JSON
{"item_id": 5, "q": "somequery"}
```
### Interactive API docs
Now you can go to <a href="http://192.168.99.100/docs" target="_blank">http://192.168.99.100/docs</a> or <a href="http://127.0.0.1/docs" target="_blank">http://127.0.0.1/docs</a> (or equivalent, using your Docker host).
You will see the automatic interactive API documentation (provided by <a href="https://github.com/swagger-api/swagger-ui" target="_blank">Swagger UI</a>):
![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png)
### Alternative API docs
And you can also go to <a href="http://192.168.99.100/redoc" target="_blank">http://192.168.99.100/redoc</a> or <a href="http://127.0.0.1/redoc" target="_blank">http://127.0.0.1/redoc</a> (or equivalent, using your Docker host).
You will see the alternative automatic documentation (provided by <a href="https://github.com/Rebilly/ReDoc" target="_blank">ReDoc</a>):
![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png)
## HTTPS
### About HTTPS
It is easy to assume that HTTPS is something that is just "enabled" or not.
But it is way more complex than that.
!!! tip
If you are in a hurry or don't care, continue with the next section for step by step instructions to set everything up.
To learn the basics of HTTPS, from a consumer perspective, check <a href="https://howhttps.works/" target="_blank">https://howhttps.works/</a>.
Now, from a developer's perspective, here are several things to have in mind while thinking about HTTPS:
* For HTTPS, the server needs to have "certificates" generated by a third party.
* Those certificates are actually acquired from the third-party, not "generated".
* Certificates have a lifetime.
* They expire.
* And then they need to be renewed, acquired again from the third party.
* The encryption of the connection happens at the TCP level.
* That's one layer below HTTP.
* So, the certificate and encryption handling is done before HTTP.
* TCP doesn't know about "domains". Only about IP addresses.
* The information about the specific domain requested goes in the HTTP data.
* The HTTPS certificates "certificate" a certain domain, but the protocol and encryption happen at the TCP level, before knowing which domain is being dealt with.
* By default, that would mean that you can only have one HTTPS certificate per IP address.
* No matter how big is your server and how small each application you have there might be. But...
* There's an extension to the TLS protocol (the one handling the encryption at the TCP level, before HTTP) called <a href="https://en.wikipedia.org/wiki/Server_Name_Indication" target="_blank"><abbr title="Server Name Indication">SNI</abbr></a>.
* This SNI extension allows one single server (with a single IP address) to have several HTTPS certificates and server multiple HTTPS domains/applications.
* For this to work, a single component (program) running in the server, listening in the public IP address, must have all the HTTPS certificates in the server.
* After having a secure connection, the communication protocol is the same HTTP.
* It goes encrypted, but the encrypted contents are the same HTTP protocol.
It is a common practice to have one program/HTTP server runing in the server (the machine, host, etc) and managing all the HTTPS parts, sending the decrypted HTTP requests to the actual HTTP application running in the same server (the **FastAPI** application, in this case), take the HTTP response from the application, encrypt it using the appropriate certificate and sending it back to the client using HTTPS. This server is ofter called a <a href="https://en.wikipedia.org/wiki/TLS_termination_proxy" target="_blank">TLS Termination Proxy</a>.
### Let's Encrypt
Up to some years ago, these HTTPS certificates were sold by trusted third-parties.
The process to acquire one of these certificates used to be cumbersome, require quite some paperwork and the certificates were quite expensive.
But then <a href="https://letsencrypt.org/" target="_blank">Let's Encrypt</a> was created.
It is a project from the Linux Foundation. It provides HTTPS certificates for free. In an automated way. These certificates use all the standard cryptographic security, and are short lived (about 3 months), so, the security is actually increased, by reducing their lifespan.
The domain's are securely verified and the certificates are generated automatically. This also allows automatizing the renewal of these certificates.
The idea is to automatize the acquisition and renewal of these certificates, so that you can have secure HTTPS, free, forever.
### Traefik
<a href="https://traefik.io/" target="_blank">Traefik</a> is a high performance reverse proxy / load balancer. It can do the "TLS Termination Proxy" job (apart from other features).
It has integration with Let's Encrypt. So, it can handle all the HTTPS parts, including certificate acquisition and renewal.
It also has integrations with Docker. So, you can declare your domains in each application configurations and have it read those configurations, generate the HTTPS certificates and serve HTTPS to your application, all automatically. Without requiring any change in its configuration.
---
With this information and tools, continue with the next section to combine everything.
## Docker Swarm mode cluster with Traefik and HTTPS
You can have a Docker Swarm mode cluster set up in minutes (about 20 min) with a main Traefik handling HTTPS (including certificate acquisition and renewal).
By using Docker Swarm mode, you can start with a "cluster" of a single machine (it can even be a $5 USD / month server) and then you can grow as much as you need adding more servers.
To set up a Docker Swarm Mode cluster with Traefik and HTTPS handling, follow this guide:
### <a href="https://medium.com/@tiangolo/docker-swarm-mode-and-traefik-for-a-https-cluster-20328dba6232" target="_blank">Docker Swarm Mode and Traefik for an HTTPS cluster</a>.
### Deploy a FastAPI application
The easiest way to set everything up, would be using the <a href="/project-generation/" target="_blank">FastAPI project generator</a>.
It is designed to be integrated with this Docker Swarm cluster with Traefik and HTTPS described above.
You can generate a project in about 2 min.
The generated project has instructions to deploy it, doing it takes other 2 min.
## Alternatively, deploy **FastAPI** without Docker
You can deploy **FastAPI** directly without Docker too.
You just need to install <a href="https://www.uvicorn.org/" target="_blank">Uvicorn</a> (or any other ASGI server).
And run your application the same way you have done in the tutorials, but without the `--debug` option, e.g.:
```bash
uvicorn main:app --host 0.0.0.0 --port 80
```
You might want to set up some tooling to make sure it is restarted automatically if it stops.
You might also want to install <a href="https://gunicorn.org/" target="_blank">Gunicorn</a> and <a href="https://www.uvicorn.org/#running-with-gunicorn" target="_blank">use it as a manager for Uvicorn</a>.
Making sure to fine-tune the number of workers, etc.
But if you are doing all that, you might just use the Docker image that does it automatically.

View File

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

100
docs/help-fastapi.md Normal file
View File

@@ -0,0 +1,100 @@
Are you liking **FastAPI**?
Would you like to help FastAPI, other users, and the author?
Or would you like to get help with **FastAPI**?
There are very simple ways to help (several involve just one or two clicks).
And there are several ways to get help too.
## Star **FastAPI** in GitHub
You can "star" FastAPI in GitHub (clicking the star button at the top right): <a href="https://github.com/tiangolo/fastapi" target="_blank">https://github.com/tiangolo/fastapi</a>.
By adding a star, other users will be able to find it more easily and see that it has been already useful for others.
## Watch the GitHub repository for releases
You can "watch" FastAPI in GitHub (clicking the "watch" button at the top right): <a href="https://github.com/tiangolo/fastapi" target="_blank">https://github.com/tiangolo/fastapi</a>.
There you can select "Releases only".
Doing it, you will receive notifications (in your email) whenever there's a new release (a new version) of **FastAPI** with bug fixes and new features.
## Connect with the author
You can connect with me (Sebastián Ramírez / `tiangolo`), the author.
You can:
* <a href="https://github.com/tiangolo" target="_blank">Follow me on **GitHub**</a>.
* See other Open Source projects I have created that could help you.
* Follow me to see when I create a new Open Source project.
* <a href="https://twitter.com/tiangolo" target="_blank">Follow me on **Twitter**</a>.
* Tell me how you use FastAPI (I love to hear that).
* Ask questions.
* <a href="https://www.linkedin.com/in/tiangolo/" target="_blank">Connect with me on **Linkedin**</a>.
* Talk to me.
* Endorse me or recommend me :)
* <a href="https://medium.com/@tiangolo" target="_blank">Read what I write (or follow me) on **Medium**</a>.
* Read other ideas, articles and tools I have created.
* Follow me to see when I publish something new.
## Tweet about **FastAPI**
<a href="http://twitter.com/home/?status=I'm loving FastAPI because... https://github.com/tiangolo/fastapi cc @tiangolo" target="_blank">Tweet about **FastAPI**</a> and let me and others why you like it.
## Let me know how are you using **FastAPI**
I love to hear about how **FastAPI** is being used, what have you liked in it, in which project/company are you using it, etc.
You can let me know:
* <a href="http://twitter.com/home/?status=Hey @tiangolo, I'm using FastAPI at..." target="_blank">On **Twitter**</a>.
* <a href="https://www.linkedin.com/in/tiangolo/" target="_blank">On **Linkedin**</a>.
* <a href="https://medium.com/@tiangolo" target="_blank">On **Medium**</a>.
## Vote for FastAPI
You can vote to include FastAPI in several "awesome lists":
* <a href="https://github.com/vinta/awesome-python/pull/1209" target="_blank">Vote to include **FastAPI** in `awesome-python`</a>.
* <a href="https://github.com/timofurrer/awesome-asyncio/pull/43" target="_blank">Vote to include **FastAPI** in `awesome-asyncio`</a>.
## Help others with issues in GitHub
You can see <a href="https://github.com/tiangolo/fastapi/issues" target="_blank">existing issues</a> and try and help others.
## Watch the GitHub repository
You can "watch" FastAPI in GitHub (clicking the "watch" button at the top right): <a href="https://github.com/tiangolo/fastapi" target="_blank">https://github.com/tiangolo/fastapi</a>.
If you select "Watching" instead of "Releases only", you will receive notifications when someone creates a new issue.
Then you can try and help them solving those issues.
## Create issues
You can <a href="https://github.com/tiangolo/fastapi/issues/new/choose" target="_blank">create a new issue</a> in the GitHub repository, for example to:
* Report a bug/issue.
* Suggest a new feature.
* Ask a question.
## Create a Pull Request
You can <a href="https://github.com/tiangolo/fastapi" target="_blank">create a Pull Request</a>, for example:
* To fix a typo you found on the documentation.
* To propose new documentation sections.
* To fix an existing issue/bug.
* To add a new feature.
---
Thanks!

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -24,11 +24,11 @@
---
FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+.
FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
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. *
@@ -166,7 +166,7 @@ You will see the alternative automatic documentation (provided by <a href="https
## Example upgrade
Now modify the file `main.py` to recive a body from a `PUT` request.
Now modify the file `main.py` to receive a body from a `PUT` request.
Declare the body using standard Python types, thanks to Pydantic.
@@ -257,7 +257,7 @@ item: Item
* Validation of data:
* Automatic and clear errors when the data is invalid.
* Validation even for deeply nested JSON objects.
* <abbr title="also known as: serialization, parsing, marshalling">Conversion</abbr> of input data: coming from the network, to Python data and types. Reading from:
* <abbr title="also known as: serialization, parsing, marshalling">Conversion</abbr> of input data: coming from the network to Python data and types. Reading from:
* JSON.
* Path parameters.
* Query parameters.
@@ -291,8 +291,8 @@ 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:
* Interactive documentation sytems.
* Document everything with OpenAPI, that can be used by:
* Interactive documentation systems.
* Automatic client code generation systems, for many languages.
* Provide 2 interactive documentation web interfaces directly.
@@ -329,7 +329,7 @@ For a more complete example including more features, see the <a href="https://fa
**Spoiler alert**: the tutorial - user guide includes:
* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**.
* How to set **validation constrains** as `maximum_length` or `regex`.
* How to set **validation constraints** as `maximum_length` or `regex`.
* A very powerful and easy to use **<abbr title="also known as components, resources, providers, services, injectables">Dependency Injection</abbr>** system.
* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth.
* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic).
@@ -342,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
@@ -366,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

45
docs/release-notes.md Normal file
View File

@@ -0,0 +1,45 @@
## Next
## 0.5.1
* Add section about <a href="https://fastapi.tiangolo.com/help-fastapi/" target="_blank">helping and getting help with **FastAPI**</a>.
* Add note about <a href="https://fastapi.tiangolo.com/tutorial/path-params/#order-matters" target="_blank">path operations order in docs</a>.
* Update <a href="https://fastapi.tiangolo.com/tutorial/handling-errors/" target="_blank">section about error handling</a> with more information and make relation with Starlette error handling utilities more explicit. PR <a href="https://github.com/tiangolo/fastapi/pull/41" target="_blank">#41</a>.
* Add <a href="" target="_blank">Development - Contributing section to the docs</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/42" target="_blank">#42</a>.
## 0.5.0
* Add new `HTTPException` with support for custom headers. With new documentation for handling errors at: <a href="https://fastapi.tiangolo.com/tutorial/handling-errors/" target="_blank">https://fastapi.tiangolo.com/tutorial/handling-errors/</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/35" target="_blank">#35</a>.
* Add <a href="https://fastapi.tiangolo.com/tutorial/using-request-directly/" target="_blank">documentation to use Starlette `Request` object</a> directly. Check <a href="https://github.com/tiangolo/fastapi/pull/25" target="_blank">#25</a> by <a href="https://github.com/euri10" target="_blank">@euri10</a>.
* Add issue templates to simplify reporting bugs, getting help, etc: <a href="https://github.com/tiangolo/fastapi/pull/34" target="_blank">#34</a>.
* Update example for the SQLAlchemy tutorial at <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">https://fastapi.tiangolo.com/tutorial/sql-databases/</a> using middleware and database session attached to request.
## 0.4.0
* Add `openapi_prefix`, support for reverse proxy and mounting sub-applicaitons. See the docs at <a href="https://fastapi.tiangolo.com/tutorial/sub-applications-proxy/" target="_blank">https://fastapi.tiangolo.com/tutorial/sub-applications-proxy/</a>: <a href="https://github.com/tiangolo/fastapi/pull/26" target="_blank">#26</a> by <a href="https://github.com/kabirkhan" target="_blank">@kabirkhan</a>.
* Update <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">docs/tutorial for SQLAlchemy</a> including note about *DB Browser for SQLite*.
## 0.3.0
* Fix/add SQLAlchemy support, including ORM, and update <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">docs for SQLAlchemy</a>: <a href="https://github.com/tiangolo/fastapi/pull/30" target="_blank">#30</a>.
## 0.2.1
* Fix `jsonable_encoder` for Pydantic models with `Config` but without `json_encoders`: <a href="https://github.com/tiangolo/fastapi/pull/29" target="_blank">#29</a>.
## 0.2.0
* Fix typos in Security section: <a href="https://github.com/tiangolo/fastapi/pull/24" target="_blank">#24</a> by <a href="https://github.com/kkinder" target="_blank">@kkinder</a>.
* Add support for Pydantic custom JSON encoders: <a href="https://github.com/tiangolo/fastapi/pull/21" target="_blank">#21</a> by <a href="https://github.com/euri10" target="_blank">@euri10</a>.
## 0.1.19
* Upgrade Starlette version to the current latest `0.10.1`: <a href="https://github.com/tiangolo/fastapi/pull/17" target="_blank">#17</a> by <a href="https://github.com/euri10" target="_blank">@euri10</a>.

View File

@@ -1,7 +1,7 @@
from fastapi import FastAPI
from .routers.tutorial001 import router as users_router
from .routers.tutorial002 import router as items_router
from .routers.items import router as items_router
from .routers.users import router as users_router
app = FastAPI()

View File

@@ -3,11 +3,11 @@ from fastapi import APIRouter
router = APIRouter()
@router.get("/")
@router.get("/", tags=["items"])
async def read_items():
return [{"name": "Item Foo"}, {"name": "item Bar"}]
@router.get("/{item_id}")
@router.get("/{item_id}", tags=["items"])
async def read_item(item_id: str):
return {"name": "Fake Specific Item", "item_id": item_id}

View File

@@ -3,16 +3,16 @@ from fastapi import APIRouter
router = APIRouter()
@router.get("/users/")
@router.get("/users/", tags=["users"])
async def read_users():
return [{"username": "Foo"}, {"username": "Bar"}]
@router.get("/users/me")
@router.get("/users/me", tags=["users"])
async def read_user_me():
return {"username": "fakecurrentuser"}
@router.get("/users/{username}")
@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
return {"username": username}

View File

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

View File

@@ -18,6 +18,6 @@ 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

@@ -18,6 +18,6 @@ async def read_items(commons=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

@@ -18,6 +18,6 @@ async def read_items(commons: CommonQueryParams = Depends()):
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

@@ -0,0 +1,12 @@
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {"foo": "The Foo Wrestlers"}
@app.get("/items/{item_id}")
async def create_item(item_id: str):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return {"item": items[item_id]}

View File

@@ -0,0 +1,16 @@
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {"foo": "The Foo Wrestlers"}
@app.get("/items-header/{item_id}")
async def create_item_header(item_id: str):
if item_id not in items:
raise HTTPException(
status_code=404,
detail="Item not found",
headers={"X-Error": "There goes my error"},
)
return {"item": items[item_id]}

View File

@@ -0,0 +1,15 @@
from fastapi import FastAPI
from starlette.exceptions import HTTPException
from starlette.responses import PlainTextResponse
app = FastAPI()
@app.exception_handler(HTTPException)
async def http_exception(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.get("/")
async def root():
return {"message": "Hello World"}

View File

@@ -1,10 +1,13 @@
from uuid import UUID
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: UUID):
return {"item_id": item_id}
@app.get("/users/me")
async def read_user_me():
return {"user_id": "the current user"}
@app.get("/users/{user_id}")
async def read_user(user_id: str):
return {"user_id": user_id}

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

@@ -0,0 +1,8 @@
from fastapi import FastAPI
app = FastAPI()
@app.post("/items/", status_code=201)
async def create_item(name: str):
return {"name": name}

View File

@@ -0,0 +1,9 @@
from fastapi import FastAPI
from starlette.status import HTTP_201_CREATED
app = FastAPI()
@app.post("/items/", status_code=HTTP_201_CREATED)
async def create_item(name: str):
return {"name": name}

View File

@@ -1,7 +1,6 @@
from fastapi import Depends, FastAPI, Security
from fastapi import Depends, FastAPI, HTTPException, Security
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from starlette.exceptions import HTTPException
fake_users_db = {
"johndoe": {
@@ -10,7 +9,14 @@ fake_users_db = {
"email": "johndoe@example.com",
"hashed_password": "fakehashedsecret",
"disabled": False,
}
},
"alice": {
"username": "alice",
"full_name": "Alice Wonderson",
"email": "alice@example.com",
"hashed_password": "fakehashedsecret2",
"disabled": True,
},
}
app = FastAPI()
@@ -68,7 +74,7 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends()):
if not user_dict:
raise HTTPException(status_code=400, detail="Incorrect username or password")
user = UserInDB(**user_dict)
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 username or password")

View File

@@ -1,12 +1,11 @@
from datetime import datetime, timedelta
import jwt
from fastapi import Depends, FastAPI, Security
from fastapi import Depends, FastAPI, HTTPException, Security
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt import PyJWTError
from passlib.context import CryptContext
from pydantic import BaseModel
from starlette.exceptions import HTTPException
from starlette.status import HTTP_403_FORBIDDEN
# to get a string like this run:

View File

@@ -1,16 +1,17 @@
from fastapi import FastAPI
from sqlalchemy import Boolean, Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.orm import sessionmaker
from starlette.requests import Request
# SQLAlchemy specific code, as with any other app
SQLALCHEMY_DATABASE_URI = "postgresql://user:password@postgresserver/db"
SQLALCHEMY_DATABASE_URI = "sqlite:///./test.db"
# SQLALCHEMY_DATABASE_URI = "postgresql://user:password@postgresserver/db"
engine = create_engine(SQLALCHEMY_DATABASE_URI, convert_unicode=True)
db_session = scoped_session(
sessionmaker(autocommit=False, autoflush=False, bind=engine)
engine = create_engine(
SQLALCHEMY_DATABASE_URI, connect_args={"check_same_thread": False}
)
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class CustomBase:
@@ -30,15 +31,37 @@ class User(Base):
is_active = Column(Boolean(), default=True)
def get_user(username, db_session):
return db_session.query(User).filter(User.id == username).first()
Base.metadata.create_all(bind=engine)
db_session = Session()
first_user = db_session.query(User).first()
if not first_user:
u = User(email="johndoe@example.com", hashed_password="notreallyhashed")
db_session.add(u)
db_session.commit()
db_session.close()
# Utility
def get_user(db_session, user_id: int):
return db_session.query(User).filter(User.id == user_id).first()
# FastAPI specific code
app = FastAPI()
@app.get("/users/{username}")
def read_user(username: str):
user = get_user(username, db_session)
@app.get("/users/{user_id}")
def read_user(request: Request, user_id: int):
user = get_user(request._scope["db"], user_id=user_id)
return user
@app.middleware("http")
async def close_db(request, call_next):
request._scope["db"] = Session()
response = await call_next(request)
request._scope["db"].close()
return response

View File

@@ -0,0 +1,19 @@
from fastapi import FastAPI
app = FastAPI()
@app.get("/app")
def read_main():
return {"message": "Hello World from main app"}
subapi = FastAPI(openapi_prefix="/subapi")
@subapi.get("/sub")
def read_sub():
return {"message": "Hello World from sub API"}
app.mount("/subapi", subapi)

View File

@@ -0,0 +1,10 @@
from fastapi import FastAPI
from starlette.requests import Request
app = FastAPI()
@app.get("/items/{item_id}")
def read_root(item_id: str, request: Request):
client_host = request.client.host
return {"client_host": client_host, "item_id": item_id}

View File

@@ -1,13 +1,284 @@
Coming soon...
If you are building an application or a web API, it's rarely the case that you can put everything on a single file.
```Python
{!./src/bigger_applications/app/routers/tutorial001.py!}
**FastAPI** provides a convenience tool to structure your application while keeping all the flexibility.
## An example file structure
Let's say you have a file structure like this:
```
.
├── app
│   ├── __init__.py
│   ├── main.py
│   └── routers
│   ├── __init__.py
│   ├── items.py
│   └── users.py
```
```Python
{!./src/bigger_applications/app/routers/tutorial002.py!}
!!! tip
There are two `__init__.py` files: one in each directory or subdirectory.
This is what allows importing code from one file into another.
For example, in `app/main.py` you could have a line like:
```
from app.routers import items
```
* The `app` directory contains everything.
* This `app` directory has an empty file `app/__init__.py`.
* So, the `app` directory is a "Python package" (a collection of "Python modules").
* The `app` directory also has a `app/main.py` file.
* As it is inside a Python package directory (because there's a file `__init__.py`), it is a "module" of that package: `app.main`.
* There's a subdirectory `app/routers/`.
* The subdirectory `app/routers` also has an empty file `__init__.py`.
* So, it is a "Python subpackage".
* The file `app/routers/items.py` is beside the `app/routers/__init__.py`.
* So, it's a submodule: `app.routers.items`.
* The file `app/routers/users.py` is beside the `app/routers/__init__.py`.
* So, it's a submodule: `app.routers.users`.
## `APIRouter`
Let's say the file dedicated to handling just users is the submodule at `/app/routers/users.py`.
You want to have the *path operations* related to your users separated from the rest of the code, to keep it organized.
But it's still part of the same **FastAPI** application/web API (it's part of the same "Python Package").
You can create the *path operations* for that module using `APIRouter`.
### Import `APIRouter`
You import it and create an "instance" the same way you would with the class `FastAPI`:
```Python hl_lines="1 3"
{!./src/bigger_applications/app/routers/users.py!}
```
```Python
{!./src/bigger_applications/app/tutorial003.py!}
### Path operations with `APIRouter`
And then you use it to declare your *path operations*.
Use it the same way you would use the `FastAPI` class:
```Python hl_lines="6 11 16"
{!./src/bigger_applications/app/routers/users.py!}
```
You can think of `APIRouter` as a "mini `FastAPI`" class.
All the same options are supported.
All the same parameters, responses, dependencies, tags, etc.
!!! tip
In this example, the variable is called `router`, but you can name it however you want.
We are going to include this `APIrouter` in the main `FastAPI` app, but first, let's add another `APIRouter`.
## Another module with `APIRouter`
Let's say you also have the endpoints dedicated to handling "Items" from your application in the module at `app/routers/items.py`.
You have path operations for:
* `/items/`
* `/items/{item_id}`
It's all the same structure as with `app/routers/users.py`.
But let's say that this time we are more lazy.
And we don't want to have to explicitly type `/items/` in every path operation, we can do it later:
```Python hl_lines="6 11 16"
{!./src/bigger_applications/app/routers/items.py!}
```
## The main `FastAPI`
Now, let's see the module at `app/main.py`.
Here's where you import and use the class `FastAPI`.
This will be the main file in your application that ties everything together.
### Import `FastAPI`
You import and create a `FastAPI` class as normally:
```Python hl_lines="1 6"
{!./src/bigger_applications/app/main.py!}
```
### Import the `APIRouter`
But this time we are not adding path operations directly with the `FastAPI` `app`.
We import the `APIRouter`s from the other files:
```Python hl_lines="3 4"
{!./src/bigger_applications/app/main.py!}
```
As the file `app/routers/items.py` is part of the same Python package, we can import it using "dot notation".
### How the importing works
The section:
```Python
from .routers.items import router
```
Means:
* Starting in the same package that this module (the file `app/main.py`) lives in (the directory `app/`)...
* look for the subpackage `routers` (the directory at `app/routers/`)...
* and from it, the submodule `items` (the file at `app/routers/items.py`)...
* and from that submodule, import the variable `router`.
The variable `router` is the same one we created in the file `app/routers/items.py`. It's an `APIRouter`.
We could also import it like:
```Python
from app.routers.items import router
```
!!! info
The first version is a "relative import".
The second version is an "absolute import".
To learn more about Python Packages and Modules, read <a href="https://docs.python.org/3/tutorial/modules.html" target="_blank">the official Python documentation about Modules</a>.
### Avoid name collisions
We are importing a variable named `router` from the submodule `items`.
But we also have another variable named `router` in the submodule `users`.
If we import one after the other, like:
```Python
from .routers.items import router
from .routers.users import router
```
The `router` from `users` will overwrite the one form `items` and we won't be able to use them at the same time.
So, to be able to use both of them in the same file, we rename them while importing them using `as`:
```Python hl_lines="3 4"
{!./src/bigger_applications/app/main.py!}
```
### Include an `APIRouter`
Now, let's include the router from the submodule `users`, now in the variable `users_router`:
```Python hl_lines="8"
{!./src/bigger_applications/app/main.py!}
```
With `app.include_router()` we can add an `APIRouter` to the main `FastAPI` application.
It will include all the routes from that router as part of it.
!!! note "Technical Details"
It will actually internally create a path operation for each path operation that was declared in the `APIRouter`.
So, behind the scenes, it will actually work as if everything was the same single app.
!!! check
You don't have to worry about performance when including routers.
This will take microseconds and will only happen at startup.
So it won't affect performance.
### Include an `APIRouter` with a prefix
Now, let's include the router form the `items` submodule, now in the variable `items_router`.
But, remember that we were lazy and didn't add `/items/` to all the path operations?
We can add a prefix to all the path operations using the parameter `prefix` of `app.include_router()`.
As the path of each path operation has to start with `/`, like in:
```Python hl_lines="1"
@router.get("/{item_id}", tags=["items"])
async def read_item(item_id: str):
...
```
...the prefix must not include a final `/`.
So, the prefix in this case would be `/items`:
```Python hl_lines="9"
{!./src/bigger_applications/app/main.py!}
```
The end result is that the item paths are now:
* `/items/`
* `/items/{item_id}`
...as we intended.
!!! check
The `prefix` parameter is (as in many other cases) just a feature from **FastAPI** to help you avoid code duplication.
!!! tip
You could also add path operations directly, for example with: `@app.get(...)`.
Apart from `app.include_router()`, in the same **FastAPI** app.
It would still work the same.
!!! info "Very Technical Details"
**Note**: this is a very technical detail that you probably can **just skip**.
---
The `APIRouter`s are not "mounted", they are not isolated from the rest of the application.
This is because we want to include their path operations in the OpenAPI schema and the user interfaces.
As we cannot just isolate them and "mount" them independently of the rest, the path operations are "cloned" (re-created), not included directly.
## Check the automatic API docs
Now, run `uvicorn`, using the module `app.main` and the variable `app`:
```bash
uvicorn app.main:app --debug
```
And open the docs at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
You will see the automatic API docs, including the paths from all the submodules:
<img src="/img/tutorial/bigger-applications/image01.png">

View File

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

View File

@@ -9,7 +9,7 @@ First, you have to import it:
```
!!! warning
Notice that `Schema` is imported directly from `pydantic`, not form `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc).
Notice that `Schema` is imported directly from `pydantic`, not from `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc).
## Declare model attributes
@@ -37,7 +37,7 @@ In `Schema`, `Path`, `Query`, `Body` and others you'll see later, you can declar
Those parameters will be added as-is to the output JSON Schema.
If you know JSON Schema and want to add extra information appart from what we have discussed here, you can pass that as extra keyword arguments.
If you know JSON Schema and want to add extra information apart from what we have discussed here, you can pass that as extra keyword arguments.
!!! warning
Have in mind that extra parameters passed won't add any validation, only annotation, for documentation purposes.
@@ -48,6 +48,10 @@ For example, you can use that functionality to pass a <a href="http://json-schem
{!./src/body_schema/tutorial002.py!}
```
And it would look in the `/docs` like this:
<img src="/img/tutorial/body-schema/image01.png">
## Recap
You can use Pydantic's `Schema` to declare extra validations and metadata for model attributes.

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

View File

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

View File

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

View File

@@ -25,6 +25,8 @@ To do that, we declare a method `__call__`:
{!./src/dependencies/tutorial006.py!}
```
In this case, this `__call__` is what **FastAPI** will use to check for additional parameters and sub-dependencies, and this is what will be called to pass a value to the parameter in your *path operation function* later.
## Parameterize the instance
And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency:

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,116 @@ Here's a general idea of how the models could look like with their password fiel
{!./src/extra_models/tutorial001.py!}
```
### About `**user_in.dict()`
#### Pydantic's `.dict()`
`user_in` is a Pydantic model of class `UserIn`.
Pydantic models have a `.dict()` method that returns a `dict` with the model's data.
So, if we create a Pydantic object `user_in` like:
```Python
user_in = UserIn(username="john", password="secret", email="john.doe@example.com")
```
and then we call:
```Python
user_dict = user_in.dict()
```
we now have a `dict` with the data in the variable `user_dict` (it's a `dict` instead of a Pydantic model object).
And if we call:
```Python
print(user_dict)
```
we would get a Python `dict` with:
```Python
{
'username': 'john',
'password': 'secret',
'email': 'john.doe@example.com',
'full_name': None,
}
```
#### Unwrapping a `dict`
If we take a `dict` like `user_dict` and pass it to a function (or class) with `**user_dict`, Python will "unwrap" it. It will pass the keys and values of the `user_dict` directly as key-value arguments.
So, continuing with the `user_dict` from above, writing:
```Python
UserInDB(**user_dict)
```
Would result in something equivalent to:
```Python
UserInDB(
username="john",
password="secret",
email="john.doe@example.com",
full_name=None,
)
```
Or more exactly, using `user_dict` directly, with whatever contents it might have in the future:
```Python
UserInDB(
username = user_dict["username"],
password = user_dict["password"],
email = user_dict["email"],
full_name = user_dict["full_name"],
)
```
#### A Pydantic model from the contents of another
As in the example above we got `user_dict` from `user_in.dict()`, this code:
```Python
user_dict = user_in.dict()
UserInDB(**user_dict)
```
would be equivalent to:
```Python
UserInDB(**user_in.dict())
```
...because `user_in.dict()` is a `dict`, and then we make Python "unwrap" it by passing it to `UserInDB` prepended with `**`.
So, we get a Pydantic model from the data in another Pydantic model.
#### Unrapping a `dict` and extra keywords
And then adding the extra keyword argument `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.
@@ -30,7 +142,7 @@ And these models are all sharing a lot of the data and duplicating attribute nam
We could do better.
We can declare a `Userbase` model that serves as a base for our other models. And then we can make subclasses of that model that inherit its attributes (type declarations, validation, etc).
We can declare a `UserBase` model that serves as a base for our other models. And then we can make subclasses of that model that inherit its attributes (type declarations, validation, etc).
All the data conversion, validation, documentation, etc. will still work as normally.
@@ -42,4 +154,6 @@ That way, we can declare just the differences between the models (with plaintext
## Recap
Use multiple Pydantic models and inherit freely for each case. You don't need to have a single data model per entity if that entity must be able to have different "states". As the case with the user "entity" with a state including `password`, `password_hash` and no password.
Use multiple Pydantic models and inherit freely for each case.
You don't need to have a single data model per entity if that entity must be able to have different "states". As the case with the user "entity" with a state including `password`, `password_hash` and no password.

View File

@@ -1 +0,0 @@
Coming soon...

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`

View File

@@ -0,0 +1,99 @@
There are many situations in where you need to notify an error to the client that is using your API.
This client could be a browser with a frontend, the code from someone else, an IoT device, etc.
You could need to tell that client that:
* He doesn't have enough privileges for that operation.
* He doesn't have access to that resource.
* The item he was trying to access doesn't exist.
* etc.
In these cases, you would normally return an **HTTP status code** in the range of **400** (from 400 to 499).
This is similar to the 200 HTTP status codes (from 200 to 299). Those "200" status codes mean that somehow there was a "success" in the request.
The status codes in the 400 range mean that there was an error from the client.
Remember all those **"404 Not Found"** errors (and jokes)?
## Use `HTTPException`
To return HTTP responses with errors to the client you use `HTTPException`.
### Import `HTTPException`
```Python hl_lines="1"
{!./src/handling_errors/tutorial001.py!}
```
### Raise an `HTTPException` in your code
`HTTPException` is a normal Python exception with additional data relevant for APIs.
Because it's a Python exception, you don't `return` it, you `raise` it.
This also means that if you are inside a utility function that you are calling inside of your path operation function, and you raise the `HTTPException` from inside of that utility function, it won't run the rest of the code in the path operation function, it will terminate that request right away and send the HTTP error from the `HTTPException` to the client.
The benefit of raising an exception over `return`ing a value will be more evident in the section about Dependencies and Security.
In this example, when the client request an item by an ID that doesn't exist, raise an exception with a status code of `404`:
```Python hl_lines="11"
{!./src/handling_errors/tutorial001.py!}
```
### The resulting response
If the client requests `http://example.com/items/foo` (an `item_id` `"foo"`), he will receive an HTTP status code of 200, and a JSON response of:
```JSON
{
"item": "The Foo Wrestlers"
}
```
But if the client requests `http://example.com/items/bar` (a non-existent `item_id` `"bar"`), he will receive an HTTP status code of 404 (the "not found" error), and a JSON response of:
```JSON
{
"detail": "Item not found"
}
```
!!! tip
When raising an `HTTPException`, you can pass any value that can be converted to JSON as the parameter `detail`, not only `str`.
You could pass a `dict`, a `list`, etc.
They are handled automatically by **FastAPI** and converted to JSON.
### Adding custom headers
There are some situations in where it's useful to be able to add custom headers to the HTTP error. For example, for some types of security.
You probably won't need to use it directly in your code.
But in case you needed it for an advanced scenario, you can add custom headers:
```Python hl_lines="14"
{!./src/handling_errors/tutorial002.py!}
```
### Installing custom handlers
If you need to add other custom exception handlers, or override the default one (that sends the errors as JSON), you can use <a href="https://www.starlette.io/exceptions/" target="_blank">the same exception utilities from Starlette</a>.
For example, you could override the default exception handler with:
```Python hl_lines="2 3 8 9 10"
{!./src/handling_errors/tutorial003.py!}
```
...this would make it return "plain text" responses with the errors, instead of JSON responses.
!!! info
Note that in this example we set the exception handler with Starlette's `HTTPException` instead of FastAPI's `HTTPException`.
This would ensure that if you use a plug-in or any other third-party tool that raises Starlette's `HTTPException` directly, it will be catched by your exception handler.

View File

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

View File

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

@@ -98,6 +98,24 @@ You can use the same type declarations with `str`, `float`, `bool` and many othe
These are explored in the next chapters of the tutorial.
## Order matters
When creating *path operations*, you can find situations where you have a fixed path.
Like `/users/me`, let's say that it's to get data about the current user.
And then you can also have a path `/users/{user_id}` to get data about a specific user by some user ID.
Because path operations are evaluated in order, you need to make sure that the path for `/users/me` is declared before the one for `/users/{user_id}`:
```Python hl_lines="6 11"
{!./src/path_params/tutorial003.py!}
```
Otherwise, the path for `/users/{user_id}` would match also for `/users/me`, "thinking" that it's receiving a parameter `user_id` with a value of `"me"`.
## Recap
With **FastAPI**, by using short, intuitive and standard Python type declarations, you get:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,71 @@
The same way you can specify a response model, you can also declare the HTTP status code used for the response with the parameter `status_code` in any of the path operations:
* `@app.get()`
* `@app.post()`
* `@app.put()`
* `@app.delete()`
* etc.
```Python hl_lines="6"
{!./src/response_status_code/tutorial001.py!}
```
!!! note
Notice that `status_code` is a parameter of the "decorator" method (`get`, `post`, etc). Not of your path operation function, like all the parameters and body.
The `status_code` parameter receives a number with the HTTP status code.
It will:
* Return that status code in the response.
* Document it as such in the OpenAPI schema (and so, in the user interfaces):
<img src="/img/tutorial/response-status-code/image01.png">
## About HTTP status codes
!!! note
If you already know what HTTP status codes are, skip to the next section.
In HTTP, you send a numeric status code of 3 digits as part of the response.
These status codes have a name associated to recognize them, but the important part is the number.
In short:
* `100` and above are for "Information". You rarely use them directly.
* **`200`** and above are for "Successful" responses. These are the ones you would use the most.
* `200` is the default status code, which means everything was "OK".
* Another example would be `201`, "Created". It is commonly used after creating a new record in the database.
* `300` and above are for "Redirection".
* **`400`** and above are for "Client error" responses. These are the second type you would probably use the most.
* An example is `404`, for a "Not Found" response.
* For generic errors from the client, you can just use `400`.
* `500` and above are for server errors. You almost never use them directly. When something goes wrong at some part in your application code, or server, it will automatically return one of these status codes.
!!! tip
To know more about each status code and which code is for what, check the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> documentation about HTTP status codes</a>.
## Shortcut to remember the names
Let's see the previous example again:
```Python hl_lines="6"
{!./src/response_status_code/tutorial001.py!}
```
`201` is the status code for "Created".
But you don't have to memorize what each of these codes mean.
You can use the convenience variables from `starlette.status`.
```Python hl_lines="2 7"
{!./src/response_status_code/tutorial002.py!}
```
They are just a convenience, they hold the same number, but that way you can use the editor's autocomplete to find them:
<img src="/img/tutorial/response-status-code/image02.png">

View File

@@ -1,4 +1,4 @@
There are many ways to handle security, authentication and autorization.
There are many ways to handle security, authentication and authorization.
And it normally is a complex and "difficult" topic.
@@ -14,9 +14,9 @@ If you don't care about any of these terms and you just need to add security wit
## OAuth2
OAuth2 is a specification that defines several ways to handle authentication and autorization.
OAuth2 is a specification that defines several ways to handle authentication and authorization.
It is quite an extensive especification and covers several complex use cases.
It is quite an extensive specification and covers several complex use cases.
It includes ways to authenticate using a "third party".

View File

@@ -52,7 +52,7 @@ The recommended algorithm is "Bcrypt".
So, install PassLib with Bcrypt:
```Python
```bash
pip install passlib[bcrypt]
```
@@ -81,7 +81,7 @@ 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"
```Python hl_lines="7 50 57 58 61 62 71 72 73 74 75 76 77"
{!./src/security/tutorial004.py!}
```
@@ -112,7 +112,7 @@ 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"
```Python hl_lines="3 6 13 14 15 16 30 31 32 80 81 82 83 84 85 86 87 88"
{!./src/security/tutorial004.py!}
```
@@ -124,7 +124,7 @@ 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"
```Python hl_lines="91 92 93 94 95 96 97 98 99 100"
{!./src/security/tutorial004.py!}
```
@@ -134,7 +134,7 @@ 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"
```Python hl_lines="114 115 116 117 118"
{!./src/security/tutorial004.py!}
```

View File

@@ -48,7 +48,7 @@ Now let's use the utilities provided by **FastAPI** to handle this.
First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depends` for the path `/token`:
```Python hl_lines="2 66"
```Python hl_lines="2 73"
{!./src/security/tutorial003.py!}
```
@@ -78,9 +78,9 @@ Now, get the user data from the (fake) database, using the `username` from the f
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:
For the error, we use the exception `HTTPException`:
```Python hl_lines="4 67 68 69"
```Python hl_lines="1 73 74 75"
{!./src/security/tutorial003.py!}
```
@@ -94,7 +94,21 @@ You should never save plaintext passwords, so, we'll use the (fake) password has
If the passwords don't match, we return the same error.
```Python hl_lines="70 71 72 73"
#### 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="76 77 78 79"
{!./src/security/tutorial003.py!}
```
@@ -114,6 +128,9 @@ UserInDB(
)
```
!!! info
For a more complete explanation of `**user_dict` check back in <a href="/tutorial/extra-models/#about-user_indict" target="_blank">the documentation for **Extra Models**</a>.
## Return the token
The response of the `token` endpoint must be a JSON object.
@@ -129,7 +146,7 @@ For this simple example, we are going to just be completely insecure and return
But for now, let's focus on the specific details we need.
```Python hl_lines="75"
```Python hl_lines="81"
{!./src/security/tutorial003.py!}
```
@@ -145,7 +162,7 @@ Both of these dependencies will just return an HTTP error if the user doesn't ex
So, in our endpoint, we will only get a user if the user exists, was correctly authenticated, and is active:
```Python hl_lines="50 51 52 53 54 55 56 59 60 61 62 79"
```Python hl_lines="56 57 58 59 60 61 62 65 66 67 68 85"
{!./src/security/tutorial003.py!}
```
@@ -160,6 +177,7 @@ Click the "Authorize" button.
Use the credentials:
User: `johndoe`
Password: `secret`
<img src="/img/tutorial/security/image04.png">
@@ -194,6 +212,24 @@ If you click the lock icon and logout, and then try the same operation again, yo
}
```
### 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.

View File

@@ -12,7 +12,9 @@ You can easily adapt it to any database supported by SQLAlchemy, like:
* Oracle
* Microsoft SQL Server, etc.
In this example, we'll use **PostgreSQL**.
In this example, we'll use **SQLite**, because it uses a single file and Python has integrated support. So, you can copy this example and run it as is.
Later, for your production application, you might want to use a database server like **PostgreSQL**.
!!! note
Notice that most of the code is the standard `SQLAlchemy` code you would use with any framework.
@@ -23,41 +25,92 @@ In this example, we'll use **PostgreSQL**.
For now, don't pay attention to the rest, only the imports:
```Python hl_lines="3 4 5"
```Python hl_lines="2 3 4"
{!./src/sql_databases/tutorial001.py!}
```
## Define the database
Define the database that SQLAlchemy should connect to:
Define the database that SQLAlchemy should "connect" to:
```Python hl_lines="8"
{!./src/sql_databases/tutorial001.py!}
```
!!! tip
This is the main line that you would have to modify if you wanted to use a different database than **PostgreSQL**.
In this example, we are "connecting" to a SQLite database (opening a file with the SQLite database).
## Create the SQLAlchemy `engine`
The file will be located at the same directory in the file `test.db`. That's why the last part is `./test.db`.
```Python hl_lines="10"
{!./src/sql_databases/tutorial001.py!}
If you were using a **PostgreSQL** database instead, you would just have to uncomment the line:
```Python
SQLALCHEMY_DATABASE_URI = "postgresql://user:password@postgresserver/db"
```
## Create a `scoped_session`
...and adapt it with your database data and credentials (equivalently for MySQL, MariaDB or any other).
!!! tip
This is the main line that you would have to modify if you wanted to use a different database.
## Create the SQLAlchemy `engine`
```Python hl_lines="11 12 13"
{!./src/sql_databases/tutorial001.py!}
```
!!! note "Very Technical Details"
Don't worry too much if you don't understand this. You can still use the code.
### Note
This `scoped_session` is a feature of SQLAlchemy.
The argument:
The resulting object, the `db_session` can then be used anywhere a a normal SQLAlchemy session.
```Python
connect_args={"check_same_thread": False}
```
...is needed only for `SQLite`. It's not needed for other databases.
!!! info "Technical Details"
That argument `check_same_thread` is there mainly to be able to run the tests that cover this example.
It can be used as a global because it is implemented to work independently on each "<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>", so the actions you perform with it in one path operation function won't affect the actions performed (possibly concurrently) by other path operation functions.
## Create a `Session` class
Each instance of the `Session` class will have a connection to the database.
This is not a connection to the database yet, but once we create an instance of this class, that instance will have the actual connection to the database.
```Python hl_lines="14"
{!./src/sql_databases/tutorial001.py!}
```
## Create a middleware to handle sessions
Now let's temporarily jump to the end of the file, to use the `Session` class we created above.
We need to have an independent `Session` per request, use the same session through all the request and then close it after the request is finished.
And then a new session will be created for the next request.
For that, we will create a new middleware.
A "middleware" is a function that is always executed for each request, and have code before and after the request.
The middleware we will create (just a function) will create a new SQLAlchemy `Session` for each request, add it to the request and then close it once the request is finished.
```Python hl_lines="62 63 64 65 66 67"
{!./src/sql_databases/tutorial001.py!}
```
### About `request._scope`
`request._scope` is a "private property" of each request. We normally shouldn't need to use a "private property" from a Python object.
But we need to attach the session to the request to be able to ensure a single session/database-connection is used through all the request, and then closed afterwards.
In the near future, Starlette <a href="https://github.com/encode/starlette/issues/379" target="_blank">will have a way to attach custom objects to each request</a>.
When that happens, this tutorial will be updated to use the new official way of doing it.
## Create a `CustomBase` model
@@ -65,17 +118,17 @@ This is more of a trick to facilitate your life than something required.
But by creating this `CustomBase` class and inheriting from it, your models will have automatic `__tablename__` attributes (that are required by SQLAlchemy).
That way you don't have to declare them explicitly.
That way you don't have to declare them explicitly in every model.
So, your models will behave very similarly to, for example, Flask-SQLAlchemy.
```Python hl_lines="16 17 18 19 20"
```Python hl_lines="17 18 19 20 21"
{!./src/sql_databases/tutorial001.py!}
```
## Create the SQLAlchemy `Base` model
```Python hl_lines="23"
```Python hl_lines="24"
{!./src/sql_databases/tutorial001.py!}
```
@@ -85,15 +138,43 @@ 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="26 27 28 29 30"
```Python hl_lines="27 28 29 30 31"
{!./src/sql_databases/tutorial001.py!}
```
## Initialize your application
In a very simplistic way, initialize your database (create the tables, etc) and make sure you have a first user:
```Python hl_lines="34 36 38 39 40 41 42 44"
{!./src/sql_databases/tutorial001.py!}
```
!!! info
Notice that we close the session with `db_session.close()`.
We close this session because we only used it to create this first user.
Every new request will get its own new session.
### Note
Normally you would probably initialize your database (create tables, etc) with <a href="https://alembic.sqlalchemy.org/en/latest/" target="_blank">Alembic</a>.
And you would also use Alembic for migrations (that's its main job). For whenever you change the structure of your database, add a new column, a new table, etc.
The same way, you would probably make sure there's a first user in an external script that runs before your application, or as part of the application startup.
In this example we are doing those two operations in a very simple way, directly in the code, to focus on the main points.
Also, as all the functionality is self-contained in the same code, you can copy it and run it directly, and it will work as is.
## Get a user
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:
By creating a function that is only dedicated to getting your user from a `user_id` (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 tests, written in code, that check if another piece of code is working correctly.">unit tests</abbr> for it:
```Python hl_lines="33 34"
```Python hl_lines="48 49"
{!./src/sql_databases/tutorial001.py!}
```
@@ -103,35 +184,39 @@ Now, finally, here's the standard **FastAPI** code.
Create your app and path operation function:
```Python hl_lines="38 41 42 43 44"
```Python hl_lines="53 56 57 58 59"
{!./src/sql_databases/tutorial001.py!}
```
As we are using SQLAlchemy's `scoped_session`, we don't even have to create a dependency with `Depends`.
We are creating the database session before each request, attaching it to the request, and then closing it afterwards.
We can just call `get_user` directly from inside of the path operation function and use the global `db_session`.
All of this is done in the middleware explained above.
Because of that, we can use the `Request` to access the database session with `request._scope["db"]`.
Then we can just call `get_user` directly from inside of the path operation function and use that session.
## Create the path operation function
Here we are using SQLAlchemy code inside of the path operation function, and it in turn will go and communicate with an external database.
Here we are using SQLAlchemy code inside of the path operation function, and in turn it will go and communicate with an external database.
That could potentially require some "waiting".
But as SQLAlchemy doesn't have compatibility for using `await`, as would be with something like:
But as SQLAlchemy doesn't have compatibility for using `await` directly, as would be with something like:
```Python
user = await get_user(username, db_session)
user = await get_user(db_session, user_id=user_id)
```
...and instead we are using:
```Python
user = get_user(username, db_session)
user = get_user(db_session, user_id=user_id)
```
Then we should declare the path operation without `async def`, just with a normal `def`:
```Python hl_lines="42"
```Python hl_lines="57"
{!./src/sql_databases/tutorial001.py!}
```
@@ -140,3 +225,55 @@ Then we should declare the path operation without `async def`, just with a norma
Because we are using SQLAlchemy directly and we don't require any kind of plug-in for it to work with **FastAPI**, we could integrate database <abbr title="Automatically updating the database to have any new column we define in our models.">migrations</abbr> with <a href="https://alembic.sqlalchemy.org" target="_blank">Alembic</a> directly.
You would probably want to declare your database and models in a different file or set of files, this would allow Alembic to import it and use it without even needing to have **FastAPI** installed for the migrations.
## Check it
You can copy this code and use it as is.
!!! info
In fact, the code shown here is part of the tests. As most of the code in these docs.
You can copy it, let's say, to a file `main.py`.
Then you can run it with Uvicorn:
```bash
uvicorn main:app --debug
```
And then, you can open your browser at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
And you will be able to interact with your **FastAPI** application, reading data from a real database:
<img src="/img/tutorial/sql-databases/image01.png">
## Response schema and security
This section has the minimum code to show how it works and how you can integrate SQLAlchemy with FastAPI.
But it is recommended that you also create a response model with Pydantic, as described in the section about <a href="/tutorial/extra-models/" target="_blank">Extra Models</a>.
That way you will document the schema of the responses of your API, and you will be able to limit/filter the returned data.
Limiting the returned data is important for security, as for example, you shouldn't be returning the `hashed_password` to the clients.
That's something that you can improve in this example application, here's the current response data:
```JSON
{
"is_active": true,
"hashed_password": "notreallyhashed",
"email": "johndoe@example.com",
"id": 1
}
```
## Interact with the database direclty
If you want to explore the SQLite database (file) directly, independently of FastAPI, to debug its contents, add tables, columns, records, modify data, etc. you can use <a href="https://sqlitebrowser.org/" target="_blank">DB Browser for SQLite</a>.
It will look like this:
<img src="/img/tutorial/sql-databases/image02.png">

View File

@@ -0,0 +1,95 @@
There are at least two situations where you could need to create your **FastAPI** application using some specific paths.
But then you need to set them up to be served with a path prefix.
It could happen if you have a:
* **Proxy** server.
* You are "**mounting**" a FastAPI application inside another FastAPI application (or inside another ASGI application, like Starlette).
## Proxy
Having a proxy in this case means that you could declare a path at `/app`, but then, you could need to add a layer on top (the Proxy) that would put your **FastAPI** application under a path like `/api/v1`.
In this case, the original path `/app` will actually be served at `/api/v1/app`.
Even though your application "thinks" it is serving at `/app`.
And the Proxy could be re-writing the path "on the fly" to keep your application convinced that it is serving at `/app`.
Up to here, everything would work as normally.
But then, when you open the integrated docs, they would expect to get the OpenAPI schema at `/openapi.json`, instead of `/api/v1/openapi.json`.
So, the frontend (that runs in the browser) would try to reach `/openapi.json` and wouldn't be able to get the OpenAPI schema.
So, it's needed that the frontend looks for the OpenAPI schema at `/api/v1/openapi.json`.
And it's also needed that the returned JSON OpenAPI schema has the defined path at `/api/v1/app` (behind the proxy) instead of `/app`.
---
For these cases, you can declare an `openapi_prefix` parameter in your `FastAPI` application.
See the section below, about "mounting", for an example.
## Mounting a **FastAPI** application
"Mounting" means adding a complete "independent" application in a specific path, that then takes care of handling all the sub-paths.
You could want to do this if you have several "independent" applications that you want to separate, having their own independent OpenAPI schema and user interfaces.
### Top-level application
First, create the main, top-level, **FastAPI** application, and its path operations:
```Python hl_lines="3 6 7 8"
{!./src/sub_applications/tutorial001.py!}
```
### Sub-application
Then, create your sub-application, and its path operations.
This sub-application is just another standard FastAPI application, but this is the one that will be "mounted".
When creating the sub-application, use the parameter `openapi_prefix`. In this case, with a prefix of `/subapi`:
```Python hl_lines="11 14 15 16"
{!./src/sub_applications/tutorial001.py!}
```
### Mount the sub-application
In your top-level application, `app`, mount the sub-application, `subapi`.
Here you need to make sure you use the same path that you used for the `openapi_prefix`, in this case, `/subapi`:
```Python hl_lines="11 19"
{!./src/sub_applications/tutorial001.py!}
```
## Check the automatic API docs
Now, run `uvicorn`, if your file is at `main.py`, it would be:
```bash
uvicorn main:app --debug
```
And open the docs at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
You will see the automatic API docs for the main app, including only its own paths:
<img src="/img/tutorial/sub-applications/image01.png">
And then, open the docs for the sub-application, at <a href="http://127.0.0.1:8000/subapi/docs" target="_blank">http://127.0.0.1:8000/subapi/docs</a>.
You will see the automatic API docs for the sub-application, including only its own sub-paths, with their correct prefix:
<img src="/img/tutorial/sub-applications/image02.png">
If you try interacting with any of the two user interfaces, they will work, because the browser will be able to talk to the correct path (or sub-path).

View File

@@ -0,0 +1,55 @@
Up to now, you have been declaring the parts of the request that you need with their types.
Taking data from:
* The path as parameters.
* Headers.
* Cookies.
* etc.
And by doing so, **FastAPI** is validating that data, converting it and generating documentation for your API automatically.
But there are situations where you might need to access the `Request` object directly.
## Details about the `Request` object
As **FastAPI** is actually **Starlette** underneath, with a layer of several tools on top, you can use Starlette's <a href="https://www.starlette.io/requests/" target="_blank">`Request`</a> object directly when you need to.
It would also mean that if you get data from the `Request` object directly (for example, read the body) it won't be validated, converted or annotated (with OpenAPI, for the automatic documentation) by FastAPI.
Although any other parameter declared normally (for example, the body with a Pydantic model) would still be validated, converted, annotated, etc.
But there are specific cases where it's useful to get the `Request` object.
## Use the `Request` object direclty
Let's imagine you want to get the client's IP address/host inside of your *path operation function*.
For that you need to access the request directly.
### Import the `Request`
First, import the `Request` class from Starlette:
```Python hl_lines="2"
{!./src/using_request_directly/tutorial001.py!}
```
### Declare the `Request` parameter
Then declare a *path operation function* parameter with the type being the `Request` class:
```Python hl_lines="8"
{!./src/using_request_directly/tutorial001.py!}
```
!!! tip
Note that in this case, we are declaring a path parameter besides the request parameter.
So, the path parameter will be extracted, validated, converted to the specified type and annotated with OpenAPI.
The same way, you can declare any other parameter as normally, and additionally, get the `Request` too.
## `Request` documentation
You can read more details about the <a href="https://www.starlette.io/requests/" target="_blank">`Request` object in the official Starlette documentation site</a>.

View File

@@ -1,7 +1,8 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.1.14"
__version__ = "0.5.1"
from .applications import FastAPI
from .routing import APIRouter
from .params import Body, Path, Query, Header, Cookie, Form, File, Security, Depends
from .exceptions import HTTPException

View File

@@ -13,7 +13,13 @@ from starlette.responses import JSONResponse, Response
async def http_exception(request: Request, exc: HTTPException) -> JSONResponse:
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
headers = getattr(exc, "headers", None)
if headers:
return JSONResponse(
{"detail": exc.detail}, status_code=exc.status_code, headers=headers
)
else:
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
class FastAPI(Starlette):
@@ -25,6 +31,7 @@ class FastAPI(Starlette):
description: str = "",
version: str = "0.1.0",
openapi_url: Optional[str] = "/openapi.json",
openapi_prefix: str = "",
docs_url: Optional[str] = "/docs",
redoc_url: Optional[str] = "/redoc",
**extra: Dict[str, Any],
@@ -43,6 +50,7 @@ class FastAPI(Starlette):
self.description = description
self.version = version
self.openapi_url = openapi_url
self.openapi_prefix = openapi_prefix.rstrip("/")
self.docs_url = docs_url
self.redoc_url = redoc_url
self.extra = extra
@@ -66,6 +74,7 @@ class FastAPI(Starlette):
openapi_version=self.openapi_version,
description=self.description,
routes=self.routes,
openapi_prefix=self.openapi_prefix,
)
return self.openapi_schema
@@ -80,7 +89,8 @@ class FastAPI(Starlette):
self.add_route(
self.docs_url,
lambda r: get_swagger_ui_html(
openapi_url=self.openapi_url, title=self.title + " - Swagger UI"
openapi_url=self.openapi_prefix + self.openapi_url,
title=self.title + " - Swagger UI",
),
include_in_schema=False,
)
@@ -88,7 +98,8 @@ class FastAPI(Starlette):
self.add_route(
self.redoc_url,
lambda r: get_redoc_html(
openapi_url=self.openapi_url, title=self.title + " - ReDoc"
openapi_url=self.openapi_prefix + self.openapi_url,
title=self.title + " - ReDoc",
),
include_in_schema=False,
)

View File

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

View File

@@ -12,38 +12,70 @@ def jsonable_encoder(
exclude: Set[str] = set(),
by_alias: bool = False,
include_none: bool = True,
custom_encoder: dict = {},
sqlalchemy_safe: bool = True,
) -> Any:
if isinstance(obj, BaseModel):
encoder = getattr(obj.Config, "json_encoders", custom_encoder)
return jsonable_encoder(
obj.dict(include=include, exclude=exclude, by_alias=by_alias),
include_none=include_none,
custom_encoder=encoder,
sqlalchemy_safe=sqlalchemy_safe,
)
if isinstance(obj, Enum):
return obj.value
if isinstance(obj, (str, int, float, type(None))):
return obj
if isinstance(obj, dict):
return {
jsonable_encoder(
key, by_alias=by_alias, include_none=include_none
): jsonable_encoder(value, by_alias=by_alias, include_none=include_none)
for key, value in obj.items()
if value is not None or include_none
}
encoded_dict = {}
for key, value in obj.items():
if (
(
not sqlalchemy_safe
or (not isinstance(key, str))
or (not key.startswith("_sa"))
)
and (value is not None or include_none)
and ((include and key in include) or key not in exclude)
):
encoded_key = jsonable_encoder(
key,
by_alias=by_alias,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
)
encoded_value = jsonable_encoder(
value,
by_alias=by_alias,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
)
encoded_dict[encoded_key] = encoded_value
return encoded_dict
if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)):
return [
jsonable_encoder(
item,
include=include,
exclude=exclude,
by_alias=by_alias,
include_none=include_none,
encoded_list = []
for item in obj:
encoded_list.append(
jsonable_encoder(
item,
include=include,
exclude=exclude,
by_alias=by_alias,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
)
)
for item in obj
]
return encoded_list
errors = []
try:
encoder = ENCODERS_BY_TYPE[type(obj)]
if custom_encoder and type(obj) in custom_encoder:
encoder = custom_encoder[type(obj)]
else:
encoder = ENCODERS_BY_TYPE[type(obj)]
return encoder(obj)
except KeyError as e:
errors.append(e)
@@ -56,4 +88,10 @@ def jsonable_encoder(
except Exception as e:
errors.append(e)
raise ValueError(errors)
return jsonable_encoder(data, by_alias=by_alias, include_none=include_none)
return jsonable_encoder(
data,
by_alias=by_alias,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
)

9
fastapi/exceptions.py Normal file
View File

@@ -0,0 +1,9 @@
from starlette.exceptions import HTTPException as StarletteHTTPException
class HTTPException(StarletteHTTPException):
def __init__(
self, status_code: int, detail: str = None, headers: dict = None
) -> None:
super().__init__(status_code=status_code, detail=detail)
self.headers = headers

View File

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

View File

@@ -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
@@ -211,7 +215,8 @@ def get_openapi(
version: str,
openapi_version: str = "3.0.2",
description: str = None,
routes: Sequence[BaseRoute]
routes: Sequence[BaseRoute],
openapi_prefix: str = ""
) -> Dict:
info = {"title": title, "version": version}
if description:
@@ -230,7 +235,7 @@ def get_openapi(
if result:
path, security_schemes, path_definitions = result
if path:
paths.setdefault(route.path, {}).update(path)
paths.setdefault(openapi_prefix + route.path, {}).update(path)
if security_schemes:
components.setdefault("securitySchemes", {}).update(
security_schemes

View File

@@ -7,7 +7,8 @@ from fastapi import params
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import get_body_field, get_dependant, solve_dependencies
from fastapi.encoders import jsonable_encoder
from pydantic import BaseConfig, BaseModel, Schema
from fastapi.utils import UnconstrainedConfig
from pydantic import BaseModel, Schema
from pydantic.error_wrappers import ErrorWrapper, ValidationError
from pydantic.fields import Field
from pydantic.utils import lenient_issubclass
@@ -17,7 +18,7 @@ from starlette.exceptions import HTTPException
from starlette.formparsers import UploadFile
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.routing import get_name, request_response
from starlette.routing import compile_path, get_name, request_response
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
@@ -130,7 +131,7 @@ class APIRoute(routing.Route):
class_validators=[],
default=None,
required=False,
model_config=BaseConfig(),
model_config=UnconstrainedConfig,
schema=Schema(None),
)
else:
@@ -148,9 +149,7 @@ class APIRoute(routing.Route):
self.include_in_schema = include_in_schema
self.content_type = content_type
self.path_regex, self.path_format, self.param_convertors = self.compile_path(
path
)
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
assert inspect.isfunction(endpoint) or inspect.ismethod(
endpoint
), f"An endpoint must be a function or method"

View File

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

View File

@@ -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,18 @@ 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"
)
if scheme.lower() != "bearer":
raise HTTPException(
status_code=HTTP_403_FORBIDDEN,
detail="Invalid authentication credentials",
)
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
class HTTPDigest(HTTPBase):
@@ -39,4 +92,15 @@ 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"
)
if scheme.lower() != "digest":
raise HTTPException(
status_code=HTTP_403_FORBIDDEN,
detail="Invalid authentication credentials",
)
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)

View File

@@ -3,6 +3,7 @@ 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 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
@@ -118,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):
@@ -130,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

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

View File

@@ -11,7 +11,10 @@ theme:
repo_name: tiangolo/fastapi
repo_url: https://github.com/tiangolo/fastapi
edit_uri: ""
edit_uri: ''
google_analytics:
- 'UA-133183413-1'
- 'auto'
nav:
- FastAPI: 'index.md'
@@ -23,8 +26,8 @@ nav:
- Path Parameters: 'tutorial/path-params.md'
- Query Parameters: 'tutorial/query-params.md'
- Request Body: 'tutorial/body.md'
- Query Parameters - String Validations: 'tutorial/query-params-str-validations.md'
- Path Parameters - Numeric Validations: 'tutorial/path-params-numeric-validations.md'
- Query Parameters and String Validations: 'tutorial/query-params-str-validations.md'
- Path Parameters and Numeric Validations: 'tutorial/path-params-numeric-validations.md'
- Body - Multiple Parameters: 'tutorial/body-multiple-params.md'
- Body - Schema: 'tutorial/body-schema.md'
- Body - Nested Models: 'tutorial/body-nested-models.md'
@@ -33,15 +36,16 @@ nav:
- Header Parameters: 'tutorial/header-params.md'
- Response Model: 'tutorial/response-model.md'
- Extra Models: 'tutorial/extra-models.md'
- Response Status Code: 'tutorial/response-status-code.md'
- Form Data: 'tutorial/request-forms.md'
- Request Files: 'tutorial/request-files.md'
- Request Forms and Files: 'tutorial/request-forms-and-files.md'
- Handling Errors: 'tutorial/handling-errors.md'
- Path Operation Configuration: 'tutorial/path-operation-configuration.md'
- Path Operation Advanced Configuration: 'tutorial/path-operation-advanced-configuration.md'
- Custom Response: 'tutorial/custom-response.md'
- Dependencies:
- Dependencies Intro: 'tutorial/dependencies/intro.md'
- First Steps - Functions: 'tutorial/dependencies/first-steps-functions.md'
- First Steps: 'tutorial/dependencies/first-steps.md'
- Classes as Dependencies: 'tutorial/dependencies/classes-as-dependencies.md'
- Sub-dependencies: 'tutorial/dependencies/sub-dependencies.md'
- Advanced Dependencies: 'tutorial/dependencies/advanced-dependencies.md'
@@ -51,14 +55,20 @@ nav:
- 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'
- Using the Request Directly: 'tutorial/using-request-directly.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'
- Sub Applications - Behind a Proxy: 'tutorial/sub-applications-proxy.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'
- Help FastAPI - Get Help: 'help-fastapi.md'
- Development - Contributing: 'contributing.md'
- Release Notes: release-notes.md
markdown_extensions:
- markdown.extensions.codehilite:

View File

@@ -19,8 +19,8 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
]
requires = [
"starlette >=0.9.7",
"pydantic >=0.16"
"starlette >=0.9.11,<=0.10.1",
"pydantic >=0.17,<=0.18.2"
]
description-file = "README.md"
requires-python = ">=3.6"
@@ -36,7 +36,8 @@ test = [
"black",
"isort",
"requests",
"email_validator"
"email_validator",
"sqlalchemy"
]
doc = [
"mkdocs",

5
scripts/test.sh Normal file → Executable file
View File

@@ -6,6 +6,11 @@ set -x
export VERSION_SCRIPT="import sys; print('%s.%s' % sys.version_info[0:2])"
export PYTHON_VERSION=`python -c "$VERSION_SCRIPT"`
# Remove temporary DB
if [ -f ./test.db ]; then
rm ./test.db
fi
export PYTHONPATH=./docs/src
pytest --cov=fastapi --cov=tests --cov=docs/src --cov-report=term-missing ${@}
mypy fastapi --disallow-untyped-defs

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

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

View File

@@ -0,0 +1,34 @@
from datetime import datetime, timezone
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.testclient import TestClient
class ModelWithDatetimeField(BaseModel):
dt_field: datetime
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
app = FastAPI()
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
@app.get("/model", response_model=ModelWithDatetimeField)
def get_model():
return model
client = TestClient(app)
def test_dt():
with client:
response = client.get("/model")
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}

View File

@@ -343,7 +343,7 @@ def test_head():
def test_options():
response = client.head("/items/foo")
response = client.options("/items/foo")
assert response.status_code == 200
assert response.headers["x-fastapi-item-id"] == "foo"

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

@@ -1,5 +1,9 @@
from datetime import datetime, timezone
from enum import Enum
import pytest
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
class Person:
@@ -32,6 +36,29 @@ class Unserializable:
raise NotImplementedError()
class ModelWithCustomEncoder(BaseModel):
dt_field: datetime
class Config:
json_encoders = {
datetime: lambda dt: dt.replace(
microsecond=0, tzinfo=timezone.utc
).isoformat()
}
class RoleEnum(Enum):
admin = "admin"
normal = "normal"
class ModelWithConfig(BaseModel):
role: RoleEnum = None
class Config:
use_enum_values = True
def test_encode_class():
person = Person(name="Foo")
pet = Pet(owner=person, name="Firulais")
@@ -48,3 +75,13 @@ def test_encode_unsupported():
unserializable = Unserializable()
with pytest.raises(ValueError):
jsonable_encoder(unserializable)
def test_encode_custom_json_encoders_model():
model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
def test_encode_model_with_config():
model = ModelWithConfig(role=RoleEnum.admin)
assert jsonable_encoder(model) == {"role": "admin"}

View File

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

View File

@@ -0,0 +1,118 @@
from typing import List
from fastapi import FastAPI, Query
from starlette.testclient import TestClient
app = FastAPI()
@app.get("/items/")
def read_items(q: List[int] = Query(None)):
return {"q": q}
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Items Get",
"operationId": "read_items_items__get",
"parameters": [
{
"required": False,
"schema": {
"title": "Q",
"type": "array",
"items": {"type": "integer"},
},
"name": "q",
"in": "query",
}
],
}
}
},
"components": {
"schemas": {
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {"type": "string"},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
}
},
}
multiple_errors = {
"detail": [
{
"loc": ["query", "q", 0],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
{
"loc": ["query", "q", 1],
"msg": "value is not a valid integer",
"type": "type_error.integer",
},
]
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_multi_query():
response = client.get("/items/?q=5&q=6")
assert response.status_code == 200
assert response.json() == {"q": [5, 6]}
def test_multi_query_incorrect():
response = client.get("/items/?q=five&q=six")
assert response.status_code == 422
assert response.json() == multiple_errors

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

97
tests/test_put_no_body.py Normal file
View File

@@ -0,0 +1,97 @@
from fastapi import FastAPI
from starlette.testclient import TestClient
app = FastAPI()
@app.put("/items/{item_id}")
def save_item_no_body(item_id: str):
return {"item_id": item_id}
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/{item_id}": {
"put": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Save Item No Body Put",
"operationId": "save_item_no_body_items__item_id__put",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"name": "item_id",
"in": "path",
}
],
}
}
},
"components": {
"schemas": {
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {"type": "string"},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_put_no_body():
response = client.put("/items/foo")
assert response.status_code == 200
assert response.json() == {"item_id": "foo"}
def test_put_no_body_with_body():
response = client.put("/items/foo", json={"name": "Foo"})
assert response.status_code == 200
assert response.json() == {"item_id": "foo"}

View File

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

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