Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daaf654868 | ||
|
|
6e0553b4cf | ||
|
|
8e1ecaf221 | ||
|
|
9e610030fb | ||
|
|
9940c1511e | ||
|
|
24d94298d0 | ||
|
|
e3b4019fa3 | ||
|
|
502ab432b8 | ||
|
|
9051ec3816 | ||
|
|
22f4e18cdd | ||
|
|
4473e6a096 | ||
|
|
984dd71d13 | ||
|
|
bf53518141 | ||
|
|
0ed55eb7d3 | ||
|
|
12e087f0b5 | ||
|
|
ba10838c30 | ||
|
|
656e1c7ce9 | ||
|
|
88b31e6a4d | ||
|
|
2c3b826810 | ||
|
|
aa64eecda6 | ||
|
|
712b18a58a | ||
|
|
a809da5567 | ||
|
|
80b68cd97d | ||
|
|
894e131e03 | ||
|
|
8772e2f2ee | ||
|
|
7edbd9345b | ||
|
|
56819fdd89 | ||
|
|
febf8e7341 | ||
|
|
293ebd7cc2 | ||
|
|
54e3949f74 | ||
|
|
acbcbba94f | ||
|
|
f7b7a099c3 | ||
|
|
0ea0d0e82a | ||
|
|
890f1f7899 | ||
|
|
783816a7e3 | ||
|
|
7863490c8c | ||
|
|
955e9fcb31 | ||
|
|
9484f939ed | ||
|
|
9745a5d1ae | ||
|
|
92c825be6a | ||
|
|
32438c85f6 | ||
|
|
02e53fde90 | ||
|
|
902cdaf010 | ||
|
|
04d77bb1c4 | ||
|
|
6d9fc08a7e | ||
|
|
5c9c088a2a | ||
|
|
014c7df142 | ||
|
|
9259dc228a | ||
|
|
de431d948d | ||
|
|
3d2c0993c1 | ||
|
|
37bc3614fd | ||
|
|
188da34529 | ||
|
|
d692c28f52 | ||
|
|
8568862a19 | ||
|
|
dfa067b061 | ||
|
|
0d1b97fb94 | ||
|
|
df1e754380 |
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
@@ -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.
|
||||
1
.gitignore
vendored
@@ -10,3 +10,4 @@ site
|
||||
.coverage
|
||||
coverage.xml
|
||||
.netlify
|
||||
test.db
|
||||
|
||||
10
Pipfile
@@ -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.11.1"
|
||||
pydantic = "==0.18.2"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
|
||||
384
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "a0f966a95cb84845ca4aad02c44fc0e7c5e2047fc44dcf19a95a4abaa02d0197"
|
||||
"sha256": "6b55a2dcce8b6bd5a1be8f170acb18478149218a01d1b026981a6297800cd3fa"
|
||||
},
|
||||
"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:9d48b35d1fc7521d59ae53c421297ab3878d3c7cd4b75266d77f6c73cccb78bb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.9.9"
|
||||
"version": "==0.11.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:6d8c66a65635d46d54de59b027a1dda40abbe2275b3164b634835ac9c13fd048",
|
||||
"sha256:6eab21c6e34df2c05416faa40d0c59963008fff29b6f0ccfe8fa28152ab3e383"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.6"
|
||||
},
|
||||
"flit": {
|
||||
"hashes": [
|
||||
"sha256:6aefa6ff89a993af7a7af40d3df3d0387d6663df99797981ec41b1431ec6d1e1",
|
||||
"sha256:9969db9708305b64fd8acf20043fcff144f910222397a221fd29871f02ed4a6f"
|
||||
"sha256:1d93f7a833ed8a6e120ddc40db5c4763bc39bccc75c05081ec8285ece718aefb",
|
||||
"sha256:6f6f0fb83c51ffa3a150fa41b5ac118df9ea4a87c2c06dff4ebf9adbe7b52b36"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.2.1"
|
||||
"version": "==1.3"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
@@ -233,11 +229,11 @@
|
||||
},
|
||||
"ipython": {
|
||||
"hashes": [
|
||||
"sha256:6a9496209b76463f1dec126ab928919aaf1f55b38beb9219af3fe202f6bbdd12",
|
||||
"sha256:f69932b1e806b38a7818d9a1e918e5821b685715040b48e59c657b3c7961b742"
|
||||
"sha256:06de667a9e406924f97781bda22d5d76bfb39762b678762d86a466e63f65dc39",
|
||||
"sha256:5d3e020a6b5f29df037555e5c45ab1088d6a7cf3bd84f47e0ba501eeb0c3ec82"
|
||||
],
|
||||
"markers": "python_version >= '3.3'",
|
||||
"version": "==7.2.0"
|
||||
"version": "==7.3.0"
|
||||
},
|
||||
"ipython-genutils": {
|
||||
"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,27 @@
|
||||
},
|
||||
"mkdocs-material": {
|
||||
"hashes": [
|
||||
"sha256:037712dd7e2128a9b596943bcd92ebc9ad28800906dcee447e2fc008dd9dbbff",
|
||||
"sha256:52522c8553a6d6da8fca2afe43297e8f88acdcf8ccf752a118148f1328f761e2"
|
||||
"sha256:63c49a7020e5d187d5adcd441b259e0b81ad418599b22e2c2574b419ed833851",
|
||||
"sha256:90a240f268f182a96098490d35bb75d5efc86b2f67d63a82b8750da20a72ef60"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1.0"
|
||||
"version": "==4.0.1"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
|
||||
"sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
|
||||
"sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
|
||||
"sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40",
|
||||
"sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"
|
||||
],
|
||||
"version": "==4.3.0"
|
||||
"markers": "python_version > '2.7'",
|
||||
"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 +418,10 @@
|
||||
},
|
||||
"nbconvert": {
|
||||
"hashes": [
|
||||
"sha256:08d21cf4203fabafd0d09bbd63f06131b411db8ebeede34b0fd4be4548351779",
|
||||
"sha256:a8a2749f972592aa9250db975304af6b7337f32337e523a2c995cc9e12c07807"
|
||||
"sha256:302554a2e219bc0fc84f3edd3e79953f3767b46ab67626fdec16e38ba3f7efe4",
|
||||
"sha256:5de8fb2284422272a1d45abc77c07b888127550a6d602ce619592a2b08a474ff"
|
||||
],
|
||||
"version": "==5.4.0"
|
||||
"version": "==5.4.1"
|
||||
},
|
||||
"nbformat": {
|
||||
"hashes": [
|
||||
@@ -484,17 +445,10 @@
|
||||
},
|
||||
"parso": {
|
||||
"hashes": [
|
||||
"sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2",
|
||||
"sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24"
|
||||
"sha256:4580328ae3f548b358f4901e38c0578229186835f0fa0846e47369796dd5bcc9",
|
||||
"sha256:68406ebd7eafe17f8e40e15a84b56848eccbf27d7c1feb89e93d8fca395706db"
|
||||
],
|
||||
"version": "==0.3.1"
|
||||
},
|
||||
"pep8-naming": {
|
||||
"hashes": [
|
||||
"sha256:1b419fa45b68b61cd8c5daf4e0c96d28915ad14d3d5f35fcc1e7e95324a33a2e",
|
||||
"sha256:4eedfd4c4b05e48796f74f5d8628c068ff788b9c2b08471ad408007fc6450e5a"
|
||||
],
|
||||
"version": "==0.4.1"
|
||||
"version": "==0.3.4"
|
||||
},
|
||||
"pexpect": {
|
||||
"hashes": [
|
||||
@@ -513,10 +467,10 @@
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095",
|
||||
"sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f"
|
||||
"sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616",
|
||||
"sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"
|
||||
],
|
||||
"version": "==0.8.0"
|
||||
"version": "==0.8.1"
|
||||
},
|
||||
"prometheus-client": {
|
||||
"hashes": [
|
||||
@@ -526,18 +480,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 +503,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 +522,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 +531,39 @@
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
"sha256:59880cc33ac293515892b2969aa8f4ed2cec592cbd0be4c4e20f2410468bbc62"
|
||||
"sha256:07f7ae71291af8b0dbad8c2ab630d8223e4a8c4e10fc37badda158c02e753acf"
|
||||
],
|
||||
"version": "==0.14.8"
|
||||
"version": "==0.14.10"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:f689bf2fc18c4585403348dd56f47d87780bf217c53ed9ae7a3e2d7faa45f8e9",
|
||||
"sha256:f812ea39a0153566be53d88f8de94839db1e8a05352ed8a49525d7d7f37861e9"
|
||||
"sha256:067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c",
|
||||
"sha256:9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.2"
|
||||
"version": "==4.3.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": [
|
||||
@@ -669,33 +583,23 @@
|
||||
},
|
||||
"pyzmq": {
|
||||
"hashes": [
|
||||
"sha256:25a0715c8f69cf72f67cfe5a68a3f3ed391c67c063d2257bec0fe7fc2c7f08f8",
|
||||
"sha256:2bab63759632c6b9e0d5bf19cc63c3b01df267d660e0abcf230cf0afaa966349",
|
||||
"sha256:30ab49d99b24bf0908ebe1cdfa421720bfab6f93174e4883075b7ff38cc555ba",
|
||||
"sha256:32c7ca9fc547a91e3c26fc6080b6982e46e79819e706eb414dd78f635a65d946",
|
||||
"sha256:41219ae72b3cc86d97557fe5b1ef5d1adc1057292ec597b50050874a970a39cf",
|
||||
"sha256:4b8c48a9a13cea8f1f16622f9bd46127108af14cd26150461e3eab71e0de3e46",
|
||||
"sha256:55724997b4a929c0d01b43c95051318e26ddbae23565018e138ae2dc60187e59",
|
||||
"sha256:65f0a4afae59d4fc0aad54a917ab599162613a761b760ba167d66cc646ac3786",
|
||||
"sha256:6f88591a8b246f5c285ee6ce5c1bf4f6bd8464b7f090b1333a446b6240a68d40",
|
||||
"sha256:75022a4c60dcd8765bb9ca32f6de75a0ec83b0d96e0309dc479f4c7b21f26cb7",
|
||||
"sha256:76ea493bfab18dcb090d825f3662b5612e2def73dffc196d51a5194b0294a81d",
|
||||
"sha256:7b60c045b80709e4e3c085bab9b691e71761b44c2b42dbb047b8b498e7bc16b3",
|
||||
"sha256:8e6af2f736734aef8ed6f278f9f552ec7f37b1a6b98e59b887484a840757f67d",
|
||||
"sha256:9ac2298e486524331e26390eac14e4627effd3f8e001d4266ed9d8f1d2d31cce",
|
||||
"sha256:9ba650f493a9bc1f24feca1d90fce0e5dd41088a252ac9840131dfbdbf3815ca",
|
||||
"sha256:a02a4a385e394e46012dc83d2e8fd6523f039bb52997c1c34a2e0dd49ed839c1",
|
||||
"sha256:a3ceee84114d9f5711fa0f4db9c652af0e4636c89eabc9b7f03a3882569dd1ed",
|
||||
"sha256:a72b82ac1910f2cf61a49139f4974f994984475f771b0faa730839607eeedddf",
|
||||
"sha256:ab136ac51027e7c484c53138a0fab4a8a51e80d05162eb7b1585583bcfdbad27",
|
||||
"sha256:c095b224300bcac61e6c445e27f9046981b1ac20d891b2f1714da89d34c637c8",
|
||||
"sha256:c5cc52d16c06dc2521340d69adda78a8e1031705924e103c0eb8fc8af861d810",
|
||||
"sha256:d612e9833a89e8177f8c1dc68d7b4ff98d3186cd331acd616b01bbdab67d3a7b",
|
||||
"sha256:e828376a23c66c6fe90dcea24b4b72cd774f555a6ee94081670872918df87a19",
|
||||
"sha256:e9767c7ab2eb552796440168d5c6e23a99ecaade08dda16266d43ad461730192",
|
||||
"sha256:ebf8b800d42d217e4710d1582b0c8bff20cdcb4faad7c7213e52644034300924"
|
||||
"sha256:15f0bf7cd80020f165635595e197603aedb37fddf4164ad5ae226afc43242f7b",
|
||||
"sha256:1756dc72e192c670490e38c788c3a35f901adc74ee436e5131d5a3e85fdd7dc6",
|
||||
"sha256:1d1eb490da54679d724b08ef3ee04530849023670c4ba7e400ed2cdf906720c4",
|
||||
"sha256:228402625796821f08706f58cc42a3c51c9897d723550babaefe4feec2b6dacc",
|
||||
"sha256:3928534fa00a2aabfcfdb439c08ba37fbe99ab0cf57776c8db8d2b73a51693ba",
|
||||
"sha256:3d2a295b1086d450981f73d3561ac204a0cc9c8ded386a4a34327d918f3b1d0a",
|
||||
"sha256:4fd8621a309db6ec23ef1369f43cdf7a9b0dc217d8ff9ca4095a6e932b379bda",
|
||||
"sha256:54fe55a1694ffe608c8e4c5183e83cab7a91f3e5c84bd6f188868d6676c12aba",
|
||||
"sha256:618887be4ad754228c0cbba7631f6574608b4430fe93974e6322324f1304fdac",
|
||||
"sha256:69130efb6efa936de601cb135a8a4eec1caccd4ea2b784237145ff4075c2d3ae",
|
||||
"sha256:6e7f78eeac82140bde7e60e975c6e6b1b678a4dd377782ab63319c1c78bf3aa1",
|
||||
"sha256:6ee760cdb84e43574da6b3f2f1fc1251e8acf87253900d28a06451c5f5de39e9",
|
||||
"sha256:97cb1b7cd2c46e87b0a26651eccd2bbb8c758035efd1635ebb81ac36aa76a88c",
|
||||
"sha256:abfa774dbadacc849121ed92eae05189d226daab583388b499472e1bbb17ef69",
|
||||
"sha256:b30c339eb58355f51f4f54dd61d785f1ff58c86bca1c3a5916977631d121867b"
|
||||
],
|
||||
"version": "==17.1.2"
|
||||
"version": "==18.0.0"
|
||||
},
|
||||
"qtconsole": {
|
||||
"hashes": [
|
||||
@@ -712,19 +616,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 +623,6 @@
|
||||
],
|
||||
"version": "==1.5.0"
|
||||
},
|
||||
"setoptconf": {
|
||||
"hashes": [
|
||||
"sha256:5b0b5d8e0077713f5d5152d4f63be6f048d9a1bb66be15d089a11c898c3cf49c"
|
||||
],
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||
@@ -745,12 +630,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 +660,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 +673,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 +729,6 @@
|
||||
"sha256:fa618be8435447a017fd1bf2c7ae922d0428056cfc7449f7a8641edf76b48265"
|
||||
],
|
||||
"version": "==3.4.2"
|
||||
},
|
||||
"wrapt": {
|
||||
"hashes": [
|
||||
"sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6"
|
||||
],
|
||||
"version": "==1.10.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
README.md
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal-vector.svg" alt='FastAPI'></a>
|
||||
<a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<em>FastAPI framework, high performance, easy to learn, fast to code, ready for production</em>
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
---
|
||||
|
||||
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:
|
||||
|
||||
@@ -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.
|
||||
@@ -292,7 +292,7 @@ Coming back to the previous code example, **FastAPI** will:
|
||||
* All this would also work for deeply nested JSON objects.
|
||||
* Convert from and to JSON automatically.
|
||||
* Document everything with OpenAPI, that can be used by:
|
||||
* Interactive documentation sytems.
|
||||
* 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).
|
||||
|
||||
@@ -6,7 +6,7 @@ What inspired **FastAPI**, how it compares to other alternatives and what it lea
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
@@ -54,6 +54,47 @@ Given the simplicity of Flask, it seemed like a good match for building APIs. Th
|
||||
|
||||
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.
|
||||
@@ -80,7 +121,7 @@ That's why when talking about version 2.0 it's common to say "Swagger", and for
|
||||
|
||||
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>
|
||||
### <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.
|
||||
|
||||
@@ -121,7 +162,7 @@ It is a plug-in for many frameworks (and there's a plug-in for Starlette too).
|
||||
|
||||
The way it works is that you write the definition of the schema using YAML format inside the docstring of each function handling a route.
|
||||
|
||||
And it generates Swagger 2.0 schemas (OpenAPI 2.0).
|
||||
And it generates OpenAPI schemas.
|
||||
|
||||
That's how it works in Flask, Starlette, Responder, etc.
|
||||
|
||||
@@ -140,7 +181,7 @@ The editor can't help much with that. And if we modify parameters or Marshmallow
|
||||
|
||||
It's a Flask plug-in, that ties together Webargs, Marshmallow and APISpec.
|
||||
|
||||
It uses the information from Webargs and Marshmallow to automatically generate Swagger 2.0 schemas, using APISpec.
|
||||
It 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.
|
||||
|
||||
|
||||
123
docs/contributing.md
Normal 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.
|
||||
@@ -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>):
|
||||
|
||||

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

|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
100
docs/help-fastapi.md
Normal 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!
|
||||
BIN
docs/img/icon-transparent-bg.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
docs/img/icon-white-bg.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
docs/img/logo-margin/logo-teal.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/img/logo-margin/logo-white-bg.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
docs/img/tutorial/bigger-applications/image01.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
docs/img/tutorial/body-schema/image01.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
docs/img/tutorial/graphql/image01.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
docs/img/tutorial/response-status-code/image01.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
docs/img/tutorial/response-status-code/image02.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
docs/img/tutorial/sql-databases/image01.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
docs/img/tutorial/sql-databases/image02.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
docs/img/tutorial/sub-applications/image01.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
docs/img/tutorial/sub-applications/image02.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal-vector.svg" alt='FastAPI'></a>
|
||||
<a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<em>FastAPI framework, high performance, easy to learn, fast to code, ready for production</em>
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
---
|
||||
|
||||
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:
|
||||
|
||||
@@ -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.
|
||||
@@ -292,7 +292,7 @@ Coming back to the previous code example, **FastAPI** will:
|
||||
* All this would also work for deeply nested JSON objects.
|
||||
* Convert from and to JSON automatically.
|
||||
* Document everything with OpenAPI, that can be used by:
|
||||
* Interactive documentation sytems.
|
||||
* 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).
|
||||
|
||||
@@ -1,5 +1,56 @@
|
||||
There is a project generator that you can use to get started, with a lot of the initial set up, security, database and first API endpoints already done for you.
|
||||
|
||||
|
||||
## Full-Stack-FastAPI-PostgreSQL
|
||||
|
||||
GitHub: <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql" target="_blank">https://github.com/tiangolo/full-stack-fastapi-postgresql</a>
|
||||
|
||||
### Features
|
||||
|
||||
* Full **Docker** integration (Docker based).
|
||||
* Docker Swarm Mode deployment.
|
||||
* **Docker Compose** integration and optimization for local development
|
||||
* **Production ready** Python web server using Uvicorn and Gunicorn.
|
||||
* Python **[FastAPI](https://github.com/tiangolo/fastapi)** backend:
|
||||
* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic).
|
||||
* **Intuitive**: Great editor support. <abbr title="also known as auto-complete, autocompletion, IntelliSense">Completion</abbr> everywhere. Less time debugging.
|
||||
* **Easy**: Designed to be easy to use and learn. Less time reading docs.
|
||||
* **Short**: Minimize code duplication. Multiple features from each parameter declaration.
|
||||
* **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>.
|
||||
* [**Many other features**](https://github.com/tiangolo/fastapi) including automatic validation, serialization, interactive documentation, authentication with OAuth2 JWT tokens, etc.
|
||||
* **Secure password** hashing by default.
|
||||
* **JWT token** authentication.
|
||||
* **SQLAlchemy** models (independent of Flask extensions, so they can be used with Celery workers directly).
|
||||
* Basic starting models for users (modify and remove as you need).
|
||||
* **Alembic** migrations.
|
||||
* **CORS** (Cross Origin Resource Sharing).
|
||||
* **Celery** worker that can import and use models and code from the rest of the backend selectively (you don't have to install the complete app in each worker).
|
||||
* REST backend tests based on **Pytest**, integrated with Docker, so you can test the full API interaction, independent on the database. As it runs in Docker, it can build a new data store from scratch each time (so you can use ElasticSearch, MongoDB, CouchDB, or whatever you want, and just test that the API works).
|
||||
* Easy Python integration with **Jupyter Kernels** for remote or in-Docker development with extensions like Atom Hydrogen or Visual Studio Code Jupyter.
|
||||
* **Vue** frontend:
|
||||
* Generated with Vue CLI.
|
||||
* **JWT Authentication** handling.
|
||||
* Login view.
|
||||
* After login, main dashboard view.
|
||||
* Main dashboard with user creation and edition.
|
||||
* Self user edition.
|
||||
* **Vuex**.
|
||||
* **Vue-router**.
|
||||
* **Vuetify** for beautiful material design components.
|
||||
* **TypeScript**.
|
||||
* Docker server based on **Nginx** (configured to play nicely with Vue-router).
|
||||
* Docker multi-stage building, so you don't need to save or commit compiled code.
|
||||
* Frontend tests ran at build time (can be disabled too).
|
||||
* Made as modular as possible, so it works out of the box, but you can re-generate with Vue CLI or create it as you need, and re-use what you want.
|
||||
* **PGAdmin** for PostgreSQL database, you can modify it to use PHPMyAdmin and MySQL easily.
|
||||
* **Flower** for Celery jobs monitoring.
|
||||
* Load balancing between frontend and backend with **Traefik**, so you can have both under the same domain, separated by path, but served by different containers.
|
||||
* Traefik integration, including Let's Encrypt **HTTPS** certificates automatic generation.
|
||||
* GitLab **CI** (continuous integration), including frontend and backend testing.
|
||||
|
||||
|
||||
|
||||
## Full-Stack-FastAPI-Couchbase
|
||||
|
||||
GitHub: <a href="https://github.com/tiangolo/full-stack-fastapi-couchbase" target="_blank">https://github.com/tiangolo/full-stack-fastapi-couchbase</a>
|
||||
@@ -10,7 +61,17 @@ GitHub: <a href="https://github.com/tiangolo/full-stack-fastapi-couchbase" targe
|
||||
* Docker Swarm Mode deployment.
|
||||
* **Docker Compose** integration and optimization for local development.
|
||||
* **Production ready** Python web server using Uvicorn and Gunicorn.
|
||||
* Python **FastAPI** backend with all its features.
|
||||
* Python **[FastAPI](https://github.com/tiangolo/fastapi)** backend:
|
||||
* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic).
|
||||
* **Intuitive**: Great editor support. <abbr title="also known as auto-complete, autocompletion, IntelliSense">Completion</abbr> everywhere. Less time debugging.
|
||||
* **Easy**: Designed to be easy to use and learn. Less time reading docs.
|
||||
* **Short**: Minimize code duplication. Multiple features from each parameter declaration.
|
||||
* **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>.
|
||||
* [**Many other features**](https://github.com/tiangolo/fastapi) including automatic validation, serialization, interactive documentation, authentication with OAuth2 JWT tokens, etc.
|
||||
* **Secure password** hashing by default.
|
||||
* **JWT token** authentication.
|
||||
* **CORS** (Cross Origin Resource Sharing).
|
||||
* **Celery** worker that can import and use code from the rest of the backend selectively (you don't have to install the complete app in each worker).
|
||||
* **NoSQL Couchbase** database that supports direct synchronization via Couchbase Sync Gateway for offline-first applications.
|
||||
* **Full Text Search** integrated, using Couchbase.
|
||||
@@ -36,7 +97,7 @@ GitHub: <a href="https://github.com/tiangolo/full-stack-fastapi-couchbase" targe
|
||||
* Docker multi-stage building, so you don't need to save or commit compiled code.
|
||||
* Frontend tests ran at build time (can be disabled too).
|
||||
* Made as modular as possible, so it works out of the box, but you can re-generate with Vue CLI or create it as you need, and re-use what you want.
|
||||
* Flower for Celery jobs monitoring.
|
||||
* **Flower** for Celery jobs monitoring.
|
||||
* Load balancing between frontend and backend with **Traefik**, so you can have both under the same domain, separated by path, but served by different containers.
|
||||
* Traefik integration, including Let's Encrypt **HTTPS** certificates automatic generation.
|
||||
* GitLab **CI** (continuous integration), including frontend and backend testing.
|
||||
|
||||
67
docs/release-notes.md
Normal file
@@ -0,0 +1,67 @@
|
||||
## Next
|
||||
|
||||
## 0.6.3
|
||||
|
||||
* Add Favicons to docs. PR <a href="https://github.com/tiangolo/fastapi/pull/53" target="_blank">#53</a>.
|
||||
|
||||
## 0.6.2
|
||||
|
||||
* Introduce new project generator based on FastAPI and PostgreSQL: <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql" target="_blank">https://github.com/tiangolo/full-stack-fastapi-postgresql</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/52" target="_blank">#52</a>.
|
||||
|
||||
* Update <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">SQL tutorial with SQLAlchemy, using `Depends` to improve editor support and reduce code repetition</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/52" target="_blank">#52</a>.
|
||||
|
||||
* Improve middleware naming in tutorial for SQL with SQLAlchemy <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">https://fastapi.tiangolo.com/tutorial/sql-databases/</a>.
|
||||
|
||||
## 0.6.1
|
||||
|
||||
* Add docs for GraphQL: <a href="https://fastapi.tiangolo.com/tutorial/graphql/" target="_blank">https://fastapi.tiangolo.com/tutorial/graphql/</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/48" target="_blank">#48</a>.
|
||||
|
||||
## 0.6.0
|
||||
|
||||
* Update SQL with SQLAlchemy tutorial at <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">https://fastapi.tiangolo.com/tutorial/sql-databases/</a> using the new official `request.state`. PR <a href="https://github.com/tiangolo/fastapi/pull/45" target="_blank">#45</a>.
|
||||
|
||||
* Upgrade Starlette to version `0.11.1` and add required compatibility changes. PR <a href="https://github.com/tiangolo/fastapi/pull/44" target="_blank">#44</a>.
|
||||
|
||||
## 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>.
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
14
docs/src/graphql/tutorial001.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import graphene
|
||||
from fastapi import FastAPI
|
||||
from starlette.graphql import GraphQLApp
|
||||
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
hello = graphene.String(name=graphene.String(default_value="stranger"))
|
||||
|
||||
def resolve_hello(self, info, name):
|
||||
return "Hello " + name
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
app.add_route("/", GraphQLApp(schema=graphene.Schema(query=Query)))
|
||||
12
docs/src/handling_errors/tutorial001.py
Normal 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]}
|
||||
16
docs/src/handling_errors/tutorial002.py
Normal 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]}
|
||||
15
docs/src/handling_errors/tutorial003.py
Normal 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"}
|
||||
@@ -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}
|
||||
|
||||
8
docs/src/response_status_code/tutorial001.py
Normal 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}
|
||||
9
docs/src/response_status_code/tutorial002.py
Normal 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}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
from fastapi import Depends, 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 Session, 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}
|
||||
)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
class CustomBase:
|
||||
@@ -30,15 +31,42 @@ 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 = SessionLocal()
|
||||
|
||||
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: Session, user_id: int):
|
||||
return db_session.query(User).filter(User.id == user_id).first()
|
||||
|
||||
|
||||
# Dependency
|
||||
def get_db(request: Request):
|
||||
return request.state.db
|
||||
|
||||
|
||||
# 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(user_id: int, db: Session = Depends(get_db)):
|
||||
user = get_user(db, user_id=user_id)
|
||||
return user
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def db_session_middleware(request: Request, call_next):
|
||||
request.state.db = SessionLocal()
|
||||
response = await call_next(request)
|
||||
request.state.db.close()
|
||||
return response
|
||||
|
||||
19
docs/src/sub_applications/tutorial001.py
Normal 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)
|
||||
10
docs/src/using_request_directly/tutorial001.py
Normal 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}
|
||||
@@ -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">
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -19,11 +19,67 @@ Here's a general idea of how the models could look like with their password fiel
|
||||
{!./src/extra_models/tutorial001.py!}
|
||||
```
|
||||
|
||||
#### About `**user_dict`
|
||||
### About `**user_in.dict()`
|
||||
|
||||
`UserInDB(**user_dict)` means:
|
||||
|
||||
Pass the keys and values of the `user_dict` directly as key-value arguments, equivalent to:
|
||||
#### 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(
|
||||
@@ -34,7 +90,28 @@ UserInDB(
|
||||
)
|
||||
```
|
||||
|
||||
And then adding the extra `hashed_password=hashed_password`, like in:
|
||||
#### 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)
|
||||
@@ -65,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.
|
||||
|
||||
@@ -77,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.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Coming soon...
|
||||
44
docs/tutorial/graphql.md
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
**FastAPI** has optional support for GraphQL (provided by Starlette directly), using the `graphene` library.
|
||||
|
||||
You can combine normal FastAPI path operations with GraphQL on the same application.
|
||||
|
||||
## Import and use `graphene`
|
||||
|
||||
GraphQL is implemented with Graphene, you can check <a href="https://docs.graphene-python.org/en/latest/quickstart/" target="_blank">Graphene's docs</a> for more details.
|
||||
|
||||
Import `graphene` and define your GraphQL data:
|
||||
|
||||
```Python hl_lines="1 6 7 8 9 10"
|
||||
{!./src/graphql/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Add Starlette's `GraphQLApp`
|
||||
|
||||
Then import and add Starlette's `GraphQLApp`:
|
||||
|
||||
```Python hl_lines="3 14"
|
||||
{!./src/graphql/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
Here we are using `.add_route`, that is the way to add a route in Starlette (inherited by FastAPI) without declaring the specific operation (as would be with `.get()`, `.post()`, etc).
|
||||
|
||||
## Check it
|
||||
|
||||
Run it with Uvicorn and open your browser at <a href="http://127.0.0.1:8000" target="_blank">http://127.0.0.1:8000</a>.
|
||||
|
||||
You will see GraphiQL web user interface:
|
||||
|
||||
<img src="/img/tutorial/graphql/image01.png">
|
||||
|
||||
|
||||
## More details
|
||||
|
||||
For more details, including:
|
||||
|
||||
* Accessing request information
|
||||
* Adding background tasks
|
||||
* Using normal or async functions
|
||||
|
||||
check the official <a href="https://www.starlette.io/graphql/" target="_blank">Starlette GraphQL docs</a>.
|
||||
99
docs/tutorial/handling-errors.md
Normal 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.
|
||||
@@ -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:
|
||||
|
||||
71
docs/tutorial/response-status-code.md
Normal 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">
|
||||
@@ -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".
|
||||
|
||||
|
||||
@@ -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!}
|
||||
```
|
||||
|
||||
|
||||
@@ -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 74 75 76"
|
||||
```Python hl_lines="1 73 74 75"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -108,7 +108,7 @@ If your database is stolen, the thief won't have your users' plaintext passwords
|
||||
|
||||
So, the thief won't be able to try to use that password in another system (as many users use the same password everywhere, this would be dangerous).
|
||||
|
||||
```Python hl_lines="77 78 79 80"
|
||||
```Python hl_lines="76 77 78 79"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -128,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.
|
||||
@@ -143,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="82"
|
||||
```Python hl_lines="81"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -159,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="57 58 59 60 61 62 63 66 67 68 69 86"
|
||||
```Python hl_lines="56 57 58 59 60 61 62 65 66 67 68 85"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
|
||||
@@ -12,7 +12,13 @@ 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**.
|
||||
|
||||
!!! tip
|
||||
There is an official project generator with **FastAPI** and **PostgreSQL**, all based on **Docker**, including a frontend and more tools: <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql" target="_blank">https://github.com/tiangolo/full-stack-fastapi-postgresql</a>
|
||||
|
||||
|
||||
!!! note
|
||||
Notice that most of the code is the standard `SQLAlchemy` code you would use with any framework.
|
||||
@@ -23,41 +29,111 @@ 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 `SessionLocal` class
|
||||
|
||||
Each instance of the `SessionLocal` class will have a connection to the database.
|
||||
|
||||
This object (class) 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.
|
||||
|
||||
We name it `SessionLocal` to distinguish it form the `Session` we are importing from SQLAlchemy.
|
||||
|
||||
We will use `Session` to declare types later and getter better editor support and completion.
|
||||
|
||||
For now, create the `SessionLocal`:
|
||||
|
||||
```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 `SessionLocal` class we created above.
|
||||
|
||||
We need to have an independent database session/connection (`SessionLocal`) 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.
|
||||
|
||||
This middleware (just a function) will create a new SQLAlchemy `SessionLocal` for each request, add it to the request and then close it once the request is finished.
|
||||
|
||||
```Python hl_lines="67 68 69 70 71 72"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
### About `request.state`
|
||||
|
||||
<a href="https://www.starlette.io/requests/#other-state" target="_blank">`request.state` is a property of each Starlette `Request` object</a>, it is there to store arbitrary objects attached to the request itself, like the database session in this case.
|
||||
|
||||
For us in this case, it helps us ensuring a single session/database-connection is used through all the request, and then closed afterwards (in the middleware).
|
||||
|
||||
## Create a dependency
|
||||
|
||||
To simplify the code, reduce repetition and get better editor support, we will create a dependency that returns this same database session from the request.
|
||||
|
||||
And when using the dependency in a path operation function, we declare it with the type `Session` we imported directly from SQLAlchemy.
|
||||
|
||||
This will then give us better editor support inside the path operation function, because the editor will know that the `db` parameter is of type `Session`.
|
||||
|
||||
```Python hl_lines="53 54 68"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! info "Technical Detail"
|
||||
The parameter `db` is actually of type `SessionLocal`, but this class (created with `sessionmaker()`) is a "proxy" of a SQLAlchemy `Session`, so, the editor doesn't really know what methods are provided.
|
||||
|
||||
But by declaring the type as `Session`, the editor now can know the available methods (`.add()`, `.query()`, `.commit()`, etc) and can provide better support (like completion). The type declaration doesn't affect the actual object.
|
||||
|
||||
## Create a `CustomBase` model
|
||||
|
||||
@@ -65,17 +141,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 +161,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 +207,43 @@ 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="58 61 62 63 64"
|
||||
{!./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.
|
||||
|
||||
Then, in the dependency `get_db()` we are extracting the database session from the request.
|
||||
|
||||
And then we can create the dependency in the path operation function, to get that session directly.
|
||||
|
||||
With that, we can just call `get_user` directly from inside of the path operation function and use that session.
|
||||
|
||||
Having this 3-step process (middleware, dependency, path operation) in this simple example might seem like an overkill. But imagine if you had 20 or 100 path operations, doing this, you would be reducing a lot of code repetition, and getting better support/checks/completion in all those path operation functions.
|
||||
|
||||
## 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="62"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -140,3 +252,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">
|
||||
|
||||
95
docs/tutorial/sub-applications-proxy.md
Normal 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).
|
||||
55
docs/tutorial/using-request-directly.md
Normal 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>.
|
||||
@@ -1,7 +1,8 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.1.17"
|
||||
__version__ = "0.6.3"
|
||||
|
||||
from .applications import FastAPI
|
||||
from .routing import APIRouter
|
||||
from .params import Body, Path, Query, Header, Cookie, Form, File, Security, Depends
|
||||
from .exceptions import HTTPException
|
||||
|
||||
@@ -7,42 +7,48 @@ from pydantic import BaseModel
|
||||
from starlette.applications import Starlette
|
||||
from starlette.exceptions import ExceptionMiddleware, HTTPException
|
||||
from starlette.middleware.errors import ServerErrorMiddleware
|
||||
from starlette.middleware.lifespan import LifespanMiddleware
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, Response
|
||||
from starlette.routing import BaseRoute
|
||||
|
||||
|
||||
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):
|
||||
def __init__(
|
||||
self,
|
||||
debug: bool = False,
|
||||
routes: List[BaseRoute] = None,
|
||||
template_directory: str = None,
|
||||
title: str = "Fast API",
|
||||
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],
|
||||
) -> None:
|
||||
self._debug = debug
|
||||
self.router: routing.APIRouter = routing.APIRouter()
|
||||
self.router: routing.APIRouter = routing.APIRouter(routes)
|
||||
self.exception_middleware = ExceptionMiddleware(self.router, debug=debug)
|
||||
self.error_middleware = ServerErrorMiddleware(
|
||||
self.exception_middleware, debug=debug
|
||||
)
|
||||
self.lifespan_middleware = LifespanMiddleware(self.error_middleware)
|
||||
self.schema_generator = None
|
||||
self.template_env = self.load_template_env(template_directory)
|
||||
|
||||
self.title = title
|
||||
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 +72,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 +87,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 +96,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,
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -8,6 +8,7 @@ def get_swagger_ui_html(*, openapi_url: str, title: str) -> HTMLResponse:
|
||||
<html>
|
||||
<head>
|
||||
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css">
|
||||
<link rel="shortcut icon" href="https://fastapi.tiangolo.com/img/favicon.png">
|
||||
<title>
|
||||
"""
|
||||
+ title
|
||||
@@ -55,6 +56,7 @@ def get_redoc_html(*, openapi_url: str, title: str) -> HTMLResponse:
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
||||
<link rel="shortcut icon" href="https://fastapi.tiangolo.com/img/favicon.png">
|
||||
|
||||
<!--
|
||||
ReDoc doesn't change outer page styles
|
||||
|
||||
@@ -215,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:
|
||||
@@ -234,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
|
||||
|
||||
@@ -18,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
|
||||
|
||||
|
||||
@@ -149,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"
|
||||
|
||||
14
mkdocs.yml
@@ -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'
|
||||
@@ -33,9 +36,11 @@ 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'
|
||||
@@ -50,16 +55,21 @@ 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'
|
||||
- GraphQL: 'tutorial/graphql.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:
|
||||
|
||||
@@ -19,8 +19,8 @@ classifiers = [
|
||||
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
||||
]
|
||||
requires = [
|
||||
"starlette >=0.9.7",
|
||||
"pydantic >=0.17"
|
||||
"starlette ==0.11.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
@@ -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
|
||||
|
||||
34
tests/test_datetime_custom_encoder.py
Normal 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"}
|
||||
@@ -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"}
|
||||
|
||||
156
tests/test_starlette_exception.py
Normal file
@@ -0,0 +1,156 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
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",
|
||||
headers={"X-Error": "Some custom header"},
|
||||
)
|
||||
return {"item": items[item_id]}
|
||||
|
||||
|
||||
@app.get("/starlette-items/{item_id}")
|
||||
async def create_item(item_id: str):
|
||||
if item_id not in items:
|
||||
raise StarletteHTTPException(status_code=404, detail="Item not found")
|
||||
return {"item": items[item_id]}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/{item_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Create Item Get",
|
||||
"operationId": "create_item_items__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
"/starlette-items/{item_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Create Item Get",
|
||||
"operationId": "create_item_starlette-items__item_id__get",
|
||||
"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_get_item():
|
||||
response = client.get("/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item": "The Foo Wrestlers"}
|
||||
|
||||
|
||||
def test_get_item_not_found():
|
||||
response = client.get("/items/bar")
|
||||
assert response.status_code == 404
|
||||
assert response.headers.get("x-error") == "Some custom header"
|
||||
assert response.json() == {"detail": "Item not found"}
|
||||
|
||||
|
||||
def test_get_starlette_item():
|
||||
response = client.get("/starlette-items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item": "The Foo Wrestlers"}
|
||||
|
||||
|
||||
def test_get_starlette_item_not_found():
|
||||
response = client.get("/starlette-items/bar")
|
||||
assert response.status_code == 404
|
||||
assert response.headers.get("x-error") is None
|
||||
assert response.json() == {"detail": "Item not found"}
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from bigger_applications.app.tutorial003 import app
|
||||
from bigger_applications.app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
@@ -17,10 +17,24 @@ openapi_schema = {
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"summary": "Read Users Get",
|
||||
"operationId": "read_users_users__get",
|
||||
}
|
||||
},
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"summary": "Read User Me Get",
|
||||
"operationId": "read_user_me_users_me_get",
|
||||
}
|
||||
},
|
||||
"/users/{username}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
@@ -39,6 +53,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"tags": ["users"],
|
||||
"summary": "Read User Get",
|
||||
"operationId": "read_user_users__username__get",
|
||||
"parameters": [
|
||||
@@ -51,18 +66,6 @@ openapi_schema = {
|
||||
],
|
||||
}
|
||||
},
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read User Me Get",
|
||||
"operationId": "read_user_me_users_me_get",
|
||||
}
|
||||
},
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
@@ -71,6 +74,7 @@ openapi_schema = {
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"tags": ["items"],
|
||||
"summary": "Read Items Get",
|
||||
"operationId": "read_items_items__get",
|
||||
}
|
||||
@@ -93,6 +97,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"tags": ["items"],
|
||||
"summary": "Read Item Get",
|
||||
"operationId": "read_item_items__item_id__get",
|
||||
"parameters": [
|
||||
90
tests/test_tutorial/test_handling_errors/test_tutorial001.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from handling_errors.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/{item_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Create Item Get",
|
||||
"operationId": "create_item_items__item_id__get",
|
||||
"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_get_item():
|
||||
response = client.get("/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item": "The Foo Wrestlers"}
|
||||
|
||||
|
||||
def test_get_item_not_found():
|
||||
response = client.get("/items/bar")
|
||||
assert response.status_code == 404
|
||||
assert response.headers.get("x-error") is None
|
||||
assert response.json() == {"detail": "Item not found"}
|
||||
90
tests/test_tutorial/test_handling_errors/test_tutorial002.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from handling_errors.tutorial002 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items-header/{item_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Create Item Header Get",
|
||||
"operationId": "create_item_header_items-header__item_id__get",
|
||||
"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_get_item_header():
|
||||
response = client.get("/items-header/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item": "The Foo Wrestlers"}
|
||||
|
||||
|
||||
def test_get_item_not_found_header():
|
||||
response = client.get("/items-header/bar")
|
||||
assert response.status_code == 404
|
||||
assert response.headers.get("x-error") == "There goes my error"
|
||||
assert response.json() == {"detail": "Item not found"}
|
||||
0
tests/test_tutorial/test_sql_databases/__init__.py
Normal file
88
tests/test_tutorial/test_sql_databases/test_tutorial001.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from sql_databases.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/{user_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read User Get",
|
||||
"operationId": "read_user_users__user_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "User_Id", "type": "integer"},
|
||||
"name": "user_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_first_user():
|
||||
response = client.get("/users/1")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"is_active": True,
|
||||
"hashed_password": "notreallyhashed",
|
||||
"email": "johndoe@example.com",
|
||||
"id": 1,
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from sub_applications.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema_main = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/app": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Main Get",
|
||||
"operationId": "read_main_app_get",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
openapi_schema_sub = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/subapi/sub": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Sub Get",
|
||||
"operationId": "read_sub_sub_get",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema_main():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema_main
|
||||
|
||||
|
||||
def test_main():
|
||||
response = client.get("/app")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Hello World from main app"}
|
||||
|
||||
|
||||
def test_openapi_schema_sub():
|
||||
response = client.get("/subapi/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema_sub
|
||||
|
||||
|
||||
def test_sub():
|
||||
response = client.get("/subapi/sub")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Hello World from sub API"}
|
||||