Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8880c4cb03 | ||
|
|
6324be684f | ||
|
|
c705685394 | ||
|
|
945f401d8e | ||
|
|
f216d340ec | ||
|
|
a4558e7053 | ||
|
|
298f8478e2 | ||
|
|
b86d163470 | ||
|
|
9e2d37b89c | ||
|
|
97adadd9e1 | ||
|
|
26e3dffb37 | ||
|
|
aa7b4bd101 | ||
|
|
ffc4c716c0 | ||
|
|
ef7b6e8eaf | ||
|
|
596243f4a5 | ||
|
|
766bf1c5aa | ||
|
|
9e748dbca4 | ||
|
|
cefe6cf92c | ||
|
|
be3953499f | ||
|
|
546d233dec | ||
|
|
61dd36a945 | ||
|
|
27f9d55c3e | ||
|
|
906cc60f65 | ||
|
|
69afaf256f | ||
|
|
4ab349a2a8 | ||
|
|
9c258107b4 | ||
|
|
29a4f90bcd | ||
|
|
361fd00777 | ||
|
|
4c3cf31730 | ||
|
|
aad6b123f7 | ||
|
|
e40e87c662 | ||
|
|
84de980977 | ||
|
|
08484603ee | ||
|
|
ab6dd60997 | ||
|
|
c9ef7bd6dc | ||
|
|
88ece95a30 | ||
|
|
366c5db0bb | ||
|
|
46e3811f8d | ||
|
|
613e211d20 | ||
|
|
500f2b2ad4 | ||
|
|
5cf7718657 | ||
|
|
727b656c8d | ||
|
|
cc7102e9b8 | ||
|
|
5123915fe4 | ||
|
|
1b8bbd51d8 | ||
|
|
1e4f86db6d | ||
|
|
7391056daf | ||
|
|
6a274d18b4 | ||
|
|
62626b0175 | ||
|
|
c8df3ae57c | ||
|
|
6f7f9268f6 | ||
|
|
50653e205f | ||
|
|
50a280b17b | ||
|
|
c1da3b38a3 | ||
|
|
e68a68c97c | ||
|
|
907e613ff2 | ||
|
|
f0fc2fad2c | ||
|
|
ad471307e2 | ||
|
|
2bd775988f | ||
|
|
c59ddc8a24 | ||
|
|
378b39bbbc | ||
|
|
37e0306517 | ||
|
|
fad3a9e1dc | ||
|
|
b35b0a9a90 | ||
|
|
1426b6200a | ||
|
|
40e5f3764e | ||
|
|
e5c75807ce | ||
|
|
deff2b6678 | ||
|
|
7c572fdb3a | ||
|
|
ae970638cf | ||
|
|
deae92bba1 | ||
|
|
f806ba642a | ||
|
|
5a3cf863da | ||
|
|
dd6ab23b62 | ||
|
|
0449499188 | ||
|
|
4dc7b32861 | ||
|
|
08d849d5c5 | ||
|
|
714e68b5f0 | ||
|
|
3d4f59f35a | ||
|
|
3ce2920fef | ||
|
|
eda9b28338 | ||
|
|
7514ac6fb0 | ||
|
|
25fb4239cc | ||
|
|
65568065e0 | ||
|
|
95679ca5e6 | ||
|
|
84a300ef84 | ||
|
|
c6d28c8209 | ||
|
|
3984e9b8ac | ||
|
|
aa0bca7bb2 |
11
.travis.yml
@@ -1,14 +1,16 @@
|
||||
dist: xenial
|
||||
|
||||
language: python
|
||||
|
||||
cache: pip
|
||||
|
||||
python:
|
||||
- "3.6"
|
||||
- "3.7-dev"
|
||||
- "3.7"
|
||||
|
||||
install:
|
||||
- pip install flit
|
||||
- flit install
|
||||
- flit install --symlink
|
||||
|
||||
script:
|
||||
- bash scripts/test.sh
|
||||
@@ -18,6 +20,7 @@ after_script:
|
||||
|
||||
deploy:
|
||||
provider: script
|
||||
script: bash scripts/trigger-docker.sh
|
||||
script: bash scripts/deploy.sh
|
||||
on:
|
||||
branch: master
|
||||
tags: true
|
||||
python: "3.6"
|
||||
|
||||
2
Pipfile
@@ -26,7 +26,7 @@ uvicorn = "*"
|
||||
|
||||
[packages]
|
||||
starlette = "==0.11.1"
|
||||
pydantic = "==0.21.0"
|
||||
pydantic = "==0.23.0"
|
||||
databases = {extras = ["sqlite"],version = "*"}
|
||||
|
||||
[requires]
|
||||
|
||||
198
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "24b3b7b88d3cbe671ddbe296e64c15f8558f0e5d5df977200119872a363aac13"
|
||||
"sha256": "02367d250c6327eac80dfcd8e5ccfa49bcdca0332bc757d527c3db27643baa0d"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -18,34 +18,33 @@
|
||||
"default": {
|
||||
"aiocontextvars": {
|
||||
"hashes": [
|
||||
"sha256:1e0ff5837c8b01c36a1107acdd0baf7853ebdf6c9fc43e8e311f4be37ac2038a",
|
||||
"sha256:6ff7aee14f549d52f0446cbb84d0deddcd3fc677bcf8fbc2ce13f5756d2064dc"
|
||||
"sha256:885daf8261818767d8f7cbd79f9d4482d118f024b6586ef6e67980236a27bfa3",
|
||||
"sha256:f027372dc48641f683c559f247bd84962becaacdc9ba711d583c3871fb5652aa"
|
||||
],
|
||||
"markers": "python_version < '3.7'",
|
||||
"version": "==0.2.1"
|
||||
"version": "==0.2.2"
|
||||
},
|
||||
"aiosqlite": {
|
||||
"hashes": [
|
||||
"sha256:af4fed9e778756fa0ffffc7a8b14c4d7b1a57155dc5669f18e45107313f6019e"
|
||||
"sha256:ad84fbd7516ca7065d799504fc41d6845c938e5306d1b7dd960caaeda12e22a9"
|
||||
],
|
||||
"version": "==0.9.0"
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"contextvars": {
|
||||
"hashes": [
|
||||
"sha256:2341042e1c03a271813e07dba29b6b60fa85c1005ea5ed1638a076cf50b4d625"
|
||||
"sha256:f38c908aaa59c14335eeea12abea5f443646216c4e29380d7bf34d2018e2c39e"
|
||||
],
|
||||
"markers": "python_version < '3.7'",
|
||||
"version": "==2.3"
|
||||
"version": "==2.4"
|
||||
},
|
||||
"databases": {
|
||||
"extras": [
|
||||
"sqlite"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:4a0f15669c390a04b439972426350c0ae921ddc08c42bd54f125eb2fb86ee728"
|
||||
"sha256:d365cff2035c5177ef5fd8c5abf6671da01189521da64848a01251c870daf48f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.2.0"
|
||||
"version": "==0.2.2"
|
||||
},
|
||||
"dataclasses": {
|
||||
"hashes": [
|
||||
@@ -57,33 +56,38 @@
|
||||
},
|
||||
"immutables": {
|
||||
"hashes": [
|
||||
"sha256:1e4f4513254ef11e0230a558ee0dcb4551b914993c330005d15338da595d3750",
|
||||
"sha256:228e38dc7a810ba4ff88909908ac47f840e5dc6c4c0da6b25009c626a9ae771c",
|
||||
"sha256:2ae88fbfe1d04f4e5859c924e97313edf70e72b4f19871bf329b96a67ede9ba0",
|
||||
"sha256:2d32b61c222cba1dd11f0faff67c7fb6204ef1982454e1b5b001d4b79966ef17",
|
||||
"sha256:35af186bfac5b62522fdf2cab11120d7b0547f405aa399b6a1e443cf5f5e318c",
|
||||
"sha256:63023fa0cceedc62e0d1535cd4ca7a1f6df3120a6d8e5c34e89037402a6fd809",
|
||||
"sha256:6bf5857f42a96331fd0929c357dc0b36a72f339f3b6acaf870b149c96b141f69",
|
||||
"sha256:7bb1590024a032c7a57f79faf8c8ff5e91340662550d2980e0177f67e66e9c9c",
|
||||
"sha256:7c090687d7e623d4eca22962635b5e1a1ee2d6f9a9aca2f3fb5a184a1ffef1f2",
|
||||
"sha256:bc36a0a8749881eebd753f696b081bd51145e4d77291d671d2e2f622e5b65d2f",
|
||||
"sha256:d9fc6a236018d99af6453ead945a6bb55f98d14b1801a2c229dd993edc753a00"
|
||||
"sha256:10861f2a2b86139f0c91d5073392d76117f37e84f912dc47c943c23a64008cc7",
|
||||
"sha256:3e23eeb4bc55d57b2a97bef4c1a2891bbb731050b4167c855545797d45e84e45",
|
||||
"sha256:4373876879f147986808f71e6ca02380192a279e8b8d45832f6fed4e7f717562",
|
||||
"sha256:46f9122da033fecf84d7f4c6257aec780f370b20f3ce6bc521702b63ee3d99f7",
|
||||
"sha256:5104db6102e53702af45c6b0af36e45a80970123b11a80c14e0fce48444cdbe3",
|
||||
"sha256:59274bcb631f4fdc9731e9a4a96d16d96b3a17e29fd5e46516518f38406f678f",
|
||||
"sha256:65a9c624e50ca5c50464dbf432996b5c4f056a411bcff5690ef4cab59f913f99",
|
||||
"sha256:b64e0672497b884d21170ca61c693da8488d77f043650efa7911378cbbad0f2c",
|
||||
"sha256:b70655dba00742b033310933066a2202e1cfbbb0f63841b4597cd8787974b242",
|
||||
"sha256:c3d8c238a6f9b60355578579563773348674b6da63c1a0d7394384ed341f3d41",
|
||||
"sha256:cd66bcd11b6a1c1a80fb8d90e25870ff2d5c705ab5eb9666355a33d3fef6ac70",
|
||||
"sha256:d59310fc4f97c1ff8c3660cb98032db266ac0c285a86ca7a512e8e84a95f44c9",
|
||||
"sha256:d71d1c822498646143270580dd6f743bb31ab89ae0ded8b2307c356d3a00f1c0",
|
||||
"sha256:f53da698b42db83cfb1f5073560838051430798c8d8e34a57a27031edbc3041d",
|
||||
"sha256:f958ba15745e30d3a38e3c9fcead8496037135bb21c78c0f925c104abba3a6fa",
|
||||
"sha256:ff95e2aa618eed1a0ef4479938f18f3522c89562b9bbb59d677597c0337569dd"
|
||||
],
|
||||
"version": "==0.6"
|
||||
"version": "==0.9"
|
||||
},
|
||||
"pydantic": {
|
||||
"hashes": [
|
||||
"sha256:93fa585402e7c8c01623ea8af6ca23363e8b4c6a020b7a2de9e99fa29d642d50",
|
||||
"sha256:eb441dd50779347a450494c437db3ecbb13c1f3854497df879662782af516c5c"
|
||||
"sha256:1205cd1213e8acee40a9ad7160b24de74484fd79ec3f09150b255896a3f506ab",
|
||||
"sha256:58b71804e9a6b4e1ccf8b3dbbca8c0f9cf4b494e5bea219a96e2e2ecb5af688e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.21.0"
|
||||
"version": "==0.23.0"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:781fb7b9d194ed3fc596b8f0dd4623ff160e3e825dd8c15472376a438c19598b"
|
||||
"sha256:91c54ca8345008fceaec987e10924bf07dcab36c442925357e5a467b36a38319"
|
||||
],
|
||||
"version": "==1.3.1"
|
||||
"version": "==1.3.3"
|
||||
},
|
||||
"starlette": {
|
||||
"hashes": [
|
||||
@@ -216,10 +220,10 @@
|
||||
},
|
||||
"defusedxml": {
|
||||
"hashes": [
|
||||
"sha256:24d7f2f94f7f3cb6061acb215685e5125fbcdc40a857eff9de22518820b0a4f4",
|
||||
"sha256:702a91ade2968a82beb0db1e0766a6a273f33d4616a6ce8cde475d8e09853b20"
|
||||
"sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93",
|
||||
"sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"
|
||||
],
|
||||
"version": "==0.5.0"
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"dnspython": {
|
||||
"hashes": [
|
||||
@@ -277,6 +281,7 @@
|
||||
"hashes": [
|
||||
"sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc"
|
||||
],
|
||||
"markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'pypy'",
|
||||
"version": "==0.0.13"
|
||||
},
|
||||
"idna": {
|
||||
@@ -317,11 +322,11 @@
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
"sha256:08f8e3f0f0b7249e9fad7e5c41e2113aba44969798a26452ee790c06f155d4ec",
|
||||
"sha256:4e9e9c4bd1acd66cf6c36973f29b031ec752cbfd991c69695e4e259f9a756927"
|
||||
"sha256:01cb7e1ca5e6c5b3f235f0385057f70558b70d2f00320208825fa62887292f43",
|
||||
"sha256:268067462aed7eb2a1e237fcb287852f22077de3fb07964e87e00f829eea2d1a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.3.16"
|
||||
"version": "==4.3.17"
|
||||
},
|
||||
"jedi": {
|
||||
"hashes": [
|
||||
@@ -332,10 +337,10 @@
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||
"sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
|
||||
"sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
|
||||
],
|
||||
"version": "==2.10"
|
||||
"version": "==2.10.1"
|
||||
},
|
||||
"jsonschema": {
|
||||
"hashes": [
|
||||
@@ -383,10 +388,10 @@
|
||||
},
|
||||
"markdown": {
|
||||
"hashes": [
|
||||
"sha256:c00429bd503a47ec88d5e30a751e147dcb4c6889663cd3e2ba0afe858e009baa",
|
||||
"sha256:d02e0f9b04c500cde6637c11ad7c72671f359b87b9fe924b2383649d8841db7c"
|
||||
"sha256:fc4a6f69a656b8d858d7503bda633f4dd63c2d70cf80abdc6eafa64c4ae8c250",
|
||||
"sha256:fe463ff51e679377e3624984c829022e2cfb3be5518726b06f608a07a3aad680"
|
||||
],
|
||||
"version": "==3.0.1"
|
||||
"version": "==3.1"
|
||||
},
|
||||
"markdown-include": {
|
||||
"hashes": [
|
||||
@@ -452,27 +457,36 @@
|
||||
},
|
||||
"mkdocs-material": {
|
||||
"hashes": [
|
||||
"sha256:0b394aa034b25a09a5874ae2a6ccc426fd81f5764e0991217b169e31cb0c1c0e",
|
||||
"sha256:f5bb80a2c16d045d380edb2c5b05636af1bb709cb859bfaa9d01063a11df803f"
|
||||
"sha256:8a572f4b3358b9c0e11af8ae319ba4f3747ebb61e2393734d875133b0d2f7891",
|
||||
"sha256:91210776db541283dd4b7beb5339c190aa69de78ad661aa116a8aa97dd73c803"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.1.0"
|
||||
"version": "==4.1.2"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40",
|
||||
"sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"
|
||||
"sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7",
|
||||
"sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a"
|
||||
],
|
||||
"markers": "python_version > '2.7'",
|
||||
"version": "==6.0.0"
|
||||
"version": "==7.0.0"
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:308c274eb8482fbf16006f549137ddc0d69e5a589465e37b99c4564414363ca7",
|
||||
"sha256:e80fd6af34614a0e898a57f14296d0dacb584648f0339c2e000ddbf0f4cc2f8d"
|
||||
"sha256:2afe51527b1f6cdc4a5f34fc90473109b22bf7f21086ba3e9451857cf11489e6",
|
||||
"sha256:56a16df3e0abb145d8accd5dbb70eba6c4bd26e2f89042b491faa78c9635d1e2",
|
||||
"sha256:5764f10d27b2e93c84f70af5778941b8f4aa1379b2430f85c827e0f5464e8714",
|
||||
"sha256:5bbc86374f04a3aa817622f98e40375ccb28c4836f36b66706cf3c6ccce86eda",
|
||||
"sha256:6a9343089f6377e71e20ca734cd8e7ac25d36478a9df580efabfe9059819bf82",
|
||||
"sha256:6c9851bc4a23dc1d854d3f5dfd5f20a016f8da86bcdbb42687879bb5f86434b0",
|
||||
"sha256:b8e85956af3fcf043d6f87c91cbe8705073fc67029ba6e22d3468bfee42c4823",
|
||||
"sha256:b9a0af8fae490306bc112229000aa0c2ccc837b49d29a5c42e088c132a2334dd",
|
||||
"sha256:bbf643528e2a55df2c1587008d6e3bda5c0445f1240dfa85129af22ae16d7a9a",
|
||||
"sha256:c46ab3438bd21511db0f2c612d89d8344154c0c9494afc7fbc932de514cf8d15",
|
||||
"sha256:f7a83d6bd805855ef83ec605eb01ab4fa42bcef254b13631e451cbb44914a9b0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.670"
|
||||
"version": "==0.701"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
@@ -497,10 +511,10 @@
|
||||
},
|
||||
"notebook": {
|
||||
"hashes": [
|
||||
"sha256:18a98858c0331fb65a60f2ebb6439f8c0c4defd14ca363731b6cabc7f61624b4",
|
||||
"sha256:cc027a62be0f7756e0ef3d2d98458c4d7f4b3566449fb1a05891207f5bd9a1bf"
|
||||
"sha256:573e0ae650c5d76b18b6e564ba6d21bf321d00847de1d215b418acb64f056eb8",
|
||||
"sha256:f64fa6624d2323fbef6210a621817d6505a45d0d4a9367f1843b20a38a4666ee"
|
||||
],
|
||||
"version": "==5.7.6"
|
||||
"version": "==5.7.8"
|
||||
},
|
||||
"pandocfilters": {
|
||||
"hashes": [
|
||||
@@ -510,18 +524,18 @@
|
||||
},
|
||||
"parso": {
|
||||
"hashes": [
|
||||
"sha256:4580328ae3f548b358f4901e38c0578229186835f0fa0846e47369796dd5bcc9",
|
||||
"sha256:68406ebd7eafe17f8e40e15a84b56848eccbf27d7c1feb89e93d8fca395706db"
|
||||
"sha256:17cc2d7a945eb42c3569d4564cdf49bde221bc2b552af3eca9c1aad517dcdd33",
|
||||
"sha256:2e9574cb12e7112a87253e14e2c380ce312060269d04bd018478a3c92ea9a376"
|
||||
],
|
||||
"version": "==0.3.4"
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"pexpect": {
|
||||
"hashes": [
|
||||
"sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba",
|
||||
"sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b"
|
||||
"sha256:2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1",
|
||||
"sha256:9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb"
|
||||
],
|
||||
"markers": "sys_platform != 'win32'",
|
||||
"version": "==4.6.0"
|
||||
"version": "==4.7.0"
|
||||
},
|
||||
"pickleshare": {
|
||||
"hashes": [
|
||||
@@ -602,11 +616,11 @@
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:592eaa2c33fae68c7d75aacf042efc9f77b27c08a6224a4f59beab8d9a420523",
|
||||
"sha256:ad3ad5c450284819ecde191a654c09b0ec72257a2c711b9633d677c71c9850c4"
|
||||
"sha256:3773f4c235918987d51daf1db66d51c99fac654c81d6f2f709a046ab446d5e5d",
|
||||
"sha256:b7802283b70ca24d7119b32915efa7c409982f59913c1a6c0640aacf118b95f5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.3.1"
|
||||
"version": "==4.4.1"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
@@ -713,16 +727,16 @@
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:781fb7b9d194ed3fc596b8f0dd4623ff160e3e825dd8c15472376a438c19598b"
|
||||
"sha256:91c54ca8345008fceaec987e10924bf07dcab36c442925357e5a467b36a38319"
|
||||
],
|
||||
"version": "==1.3.1"
|
||||
"version": "==1.3.3"
|
||||
},
|
||||
"terminado": {
|
||||
"hashes": [
|
||||
"sha256:55abf9ade563b8f9be1f34e4233c7b7bde726059947a593322e8a553cc4c067a",
|
||||
"sha256:65011551baff97f5414c67018e908110693143cfbaeb16831b743fe7cad8b927"
|
||||
"sha256:d9d012de63acb8223ac969c17c3043337c2fcfd28f3aea1ee429b345d01ef460",
|
||||
"sha256:de08e141f83c3a0798b050ecb097ab6259c3f0331b2f7b7750c9075ced2c20c2"
|
||||
],
|
||||
"version": "==0.8.1"
|
||||
"version": "==0.8.2"
|
||||
},
|
||||
"testpath": {
|
||||
"hashes": [
|
||||
@@ -759,27 +773,28 @@
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"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"
|
||||
"sha256:04894d268ba6eab7e093d43107869ad49e7b5ef40d1a94243ea49b352061b200",
|
||||
"sha256:16616ece19daddc586e499a3d2f560302c11f122b9c692bc216e821ae32aa0d0",
|
||||
"sha256:252fdae740964b2d3cdfb3f84dcb4d6247a48a6abe2579e8029ab3be3cdc026c",
|
||||
"sha256:2af80a373af123d0b9f44941a46df67ef0ff7a60f95872412a145f4500a7fc99",
|
||||
"sha256:2c88d0a913229a06282b285f42a31e063c3bf9071ff65c5ea4c12acb6977c6a7",
|
||||
"sha256:2ea99c029ebd4b5a308d915cc7fb95b8e1201d60b065450d5d26deb65d3f2bc1",
|
||||
"sha256:3d2e3ab175fc097d2a51c7a0d3fda442f35ebcc93bb1d7bd9b95ad893e44c04d",
|
||||
"sha256:4766dd695548a15ee766927bf883fb90c6ac8321be5a60c141f18628fb7f8da8",
|
||||
"sha256:56b6978798502ef66625a2e0f80cf923da64e328da8bbe16c1ff928c70c873de",
|
||||
"sha256:5cddb6f8bce14325b2863f9d5ac5c51e07b71b462361fd815d1d7706d3a9d682",
|
||||
"sha256:644ee788222d81555af543b70a1098f2025db38eaa99226f3a75a6854924d4db",
|
||||
"sha256:64cf762049fc4775efe6b27161467e76d0ba145862802a65eefc8879086fc6f8",
|
||||
"sha256:68c362848d9fb71d3c3e5f43c09974a0ae319144634e7a47db62f0f2a54a7fa7",
|
||||
"sha256:6c1f3c6f6635e611d58e467bf4371883568f0de9ccc4606f17048142dec14a1f",
|
||||
"sha256:b213d4a02eec4ddf622f4d2fbc539f062af3788d1f332f028a2e19c42da53f15",
|
||||
"sha256:bb27d4e7805a7de0e35bd0cb1411bc85f807968b2b0539597a49a23b00a622ae",
|
||||
"sha256:c9d414512eaa417aadae7758bc118868cd2396b0e6138c1dd4fda96679c079d3",
|
||||
"sha256:f0937165d1e25477b01081c4763d2d9cdc3b18af69cb259dd4f640c9b900fe5e",
|
||||
"sha256:fb96a6e2c11059ecf84e6741a319f93f683e440e341d4489c9b161eca251cf2a",
|
||||
"sha256:fc71d2d6ae56a091a8d94f33ec9d0f2001d1cb1db423d8b4355debfe9ce689b7"
|
||||
],
|
||||
"version": "==1.3.1"
|
||||
"version": "==1.3.4"
|
||||
},
|
||||
"ujson": {
|
||||
"hashes": [
|
||||
@@ -790,17 +805,17 @@
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
|
||||
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
|
||||
"sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0",
|
||||
"sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3"
|
||||
],
|
||||
"version": "==1.24.1"
|
||||
"version": "==1.24.2"
|
||||
},
|
||||
"uvicorn": {
|
||||
"hashes": [
|
||||
"sha256:d700b65169820fc260f39402b7f966c178691daaa40cb376cad99d7cd737f772"
|
||||
"sha256:181d47abddedd0f6e23eaeed97976bdce9ea1dbff0ec12385309cf4835783f6a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.7.0b1"
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"uvloop": {
|
||||
"hashes": [
|
||||
@@ -815,6 +830,7 @@
|
||||
"sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f",
|
||||
"sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe"
|
||||
],
|
||||
"markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'pypy'",
|
||||
"version": "==0.12.2"
|
||||
},
|
||||
"wcwidth": {
|
||||
|
||||
27
README.md
@@ -14,6 +14,9 @@
|
||||
<a href="https://pypi.org/project/fastapi" target="_blank">
|
||||
<img src="https://badge.fury.io/py/fastapi.svg" alt="Package version">
|
||||
</a>
|
||||
<a href="https://gitter.im/tiangolo/fastapi?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge" target="_blank">
|
||||
<img src="https://badges.gitter.im/tiangolo/fastapi.svg" alt="Join the chat at https://gitter.im/tiangolo/fastapi">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
@@ -40,6 +43,28 @@ The key features are:
|
||||
|
||||
<small>* estimation based on tests on an internal development team, building production applications.</small>
|
||||
|
||||
## Opinions
|
||||
|
||||
"*[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products.*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Kabir Khan - <strong>Microsoft</strong> <a href="https://github.com/tiangolo/fastapi/pull/26" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
"*I’m over the moon excited about **FastAPI**. It’s so fun!*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Brian Okken - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">Python Bytes</a> podcast host</strong> <a href="https://twitter.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
"*Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that.*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="http://www.hug.rest/" target="_blank">Hug</a> creator</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -195,7 +220,7 @@ def read_item(item_id: int, q: str = None):
|
||||
|
||||
|
||||
@app.put("/items/{item_id}")
|
||||
def create_item(item_id: int, item: Item):
|
||||
def update_item(item_id: int, item: Item):
|
||||
return {"item_name": item.name, "item_id": item_id}
|
||||
```
|
||||
|
||||
|
||||
@@ -236,6 +236,19 @@ It was one of the first extremely fast Python frameworks based on `asyncio`. It
|
||||
|
||||
That's why **FastAPI** is based on Starlette, as it is the fastest framework available (tested by third-party benchmarks).
|
||||
|
||||
### <a href="https://falconframework.org/" target="_blank">Falcon</a>
|
||||
|
||||
Falcon is another high performance Python framework, it is designed to be minimal, and work as the foundation of other frameworks like Hug.
|
||||
|
||||
It uses the previous standard for Python web frameworks (WSGI) which is synchronous, so it can't handle Websockets and other use cases. Nevertheless, it also has a very good performance.
|
||||
|
||||
It is designed to have functions that receive two parameters, one "request" and one "response". Then you "read" parts from the request, and "write" parts to the response. Because of this design, it is not possible to declare request parameters and bodies with standard Python type hints as function parameters.
|
||||
|
||||
So, data validation, serialization, and documentation, have to be done in code, not automatically. Or they have to be implemented as a framework on top of Falcon, like Hug. This same distinction happens in other frameworks that are inspired by Falcon's design, of having one request object and one response object as parameters.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Find ways to get great performance.
|
||||
|
||||
### <a href="https://moltenframework.com/" target="_blank">Molten</a>
|
||||
|
||||
I discovered Molten in the first stages of building **FastAPI**. And it has quite similar ideas:
|
||||
@@ -257,11 +270,34 @@ Routes are declared in a single place, using functions declared in other places
|
||||
|
||||
This actually inspired updating parts of Pydantic, to support the same validation declaration style (all this functionality is now already available in Pydantic).
|
||||
|
||||
### <a href="http://www.hug.rest/" target="_blank">Hug</a>
|
||||
|
||||
Hug was one of the first frameworks to implement the declaration of API parameter types using Python type hints. This was a great idea that inspired other tools to do the same.
|
||||
|
||||
It used custom types in its declarations instead of standard Python types, but it was still a huge step forward.
|
||||
|
||||
It also was one of the first frameworks to generate a custom schema declaring the whole API in JSON.
|
||||
|
||||
It was not based on a standard like OpenAPI and JSON Schema. So it wouldn't be straightforward to integrate it with other tools, like Swagger UI. But again, it was a very innovative idea.
|
||||
|
||||
It has an interesting, uncommon feature: using the same framework, it's possible to create APIs and also CLIs.
|
||||
|
||||
As it is based on the previous standard for synchronous Python web frameworks (WSGI), it can't handle Websockets and other things, although it still has high performance too.
|
||||
|
||||
!!! info
|
||||
Hug was created by Timothy Crosley, the same creator of <a href="https://github.com/timothycrosley/isort" target="_blank">`isort`</a>, a great tool to automatically sort imports in Python files.
|
||||
|
||||
!!! check "Ideas inspired in **FastAPI**"
|
||||
Hug inspired parts of APIStar, and was one of the tools I found most promising, alongside APIStar.
|
||||
|
||||
Hug helped inspiring **FastAPI** to use Python type hints to declare parameters, and to generate a schema defining the API automatically.
|
||||
|
||||
|
||||
### <a href="https://github.com/encode/apistar" target="_blank">APIStar</a> (<= 0.5)
|
||||
|
||||
Right before deciding to build **FastAPI** I found **APIStar** server. It had almost everything I was looking for and had a great design.
|
||||
|
||||
It was actually the first implementation of a framework using Python type hints to declare parameters and requests that I ever saw (before NestJS and Molten).
|
||||
It was one of the first implementations of a framework using Python type hints to declare parameters and requests that I ever saw (before NestJS and Molten). I found it more or less at the same time as Hug. But APIStar used the OpenAPI standard.
|
||||
|
||||
It had automatic data validation, data serialization and OpenAPI schema generation based on the same type hints in several places.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=a979de55-980d-4721-a46f-77298b3f3923&hw=ph&test=fortune&l=zijzen-7" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
|
||||
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
|
||||
|
||||
But when checking benchmarks and comparisons you should have the following in mind.
|
||||
|
||||
|
||||
@@ -24,6 +24,15 @@ 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.
|
||||
|
||||
## Join the chat
|
||||
|
||||
<a href="https://gitter.im/tiangolo/fastapi?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge" target="_blank">
|
||||
<img src="https://badges.gitter.im/tiangolo/fastapi.svg" alt="Join the chat at https://gitter.im/tiangolo/fastapi">
|
||||
</a>
|
||||
|
||||
Join the chat on Gitter: <a href="https://gitter.im/tiangolo/fastapi" target="_blank">https://gitter.im/tiangolo/fastapi</a>.
|
||||
|
||||
There you can ask quick questions, help others, share ideas, etc.
|
||||
|
||||
## Connect with the author
|
||||
|
||||
|
||||
BIN
docs/img/tutorial/additional-responses/image01.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 76 KiB |
BIN
docs/img/tutorial/extending-openapi/image01.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 85 KiB |
BIN
docs/img/tutorial/security/image11.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
docs/img/tutorial/security/image12.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
@@ -14,6 +14,9 @@
|
||||
<a href="https://pypi.org/project/fastapi" target="_blank">
|
||||
<img src="https://badge.fury.io/py/fastapi.svg" alt="Package version">
|
||||
</a>
|
||||
<a href="https://gitter.im/tiangolo/fastapi?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge" target="_blank">
|
||||
<img src="https://badges.gitter.im/tiangolo/fastapi.svg" alt="Join the chat at https://gitter.im/tiangolo/fastapi">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
@@ -40,6 +43,28 @@ The key features are:
|
||||
|
||||
<small>* estimation based on tests on an internal development team, building production applications.</small>
|
||||
|
||||
## Opinions
|
||||
|
||||
"*[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products.*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Kabir Khan - <strong>Microsoft</strong> <a href="https://github.com/tiangolo/fastapi/pull/26" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
"*I’m over the moon excited about **FastAPI**. It’s so fun!*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Brian Okken - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">Python Bytes</a> podcast host</strong> <a href="https://twitter.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
"*Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that.*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="http://www.hug.rest/" target="_blank">Hug</a> creator</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -195,7 +220,7 @@ def read_item(item_id: int, q: str = None):
|
||||
|
||||
|
||||
@app.put("/items/{item_id}")
|
||||
def create_item(item_id: int, item: Item):
|
||||
def update_item(item_id: int, item: Item):
|
||||
return {"item_name": item.name, "item_id": item_id}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +1,104 @@
|
||||
## Next release
|
||||
|
||||
## 0.18.0
|
||||
|
||||
* Add docs for <a href="https://fastapi.tiangolo.com/tutorial/security/http-basic-auth/" target="_blank">HTTP Basic Auth</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/177" target="_blank">#177</a>.
|
||||
|
||||
* Upgrade HTTP Basic Auth handling with automatic headers (automatic browser login prompt). PR <a href="https://github.com/tiangolo/fastapi/pull/175" target="_blank">#175</a>.
|
||||
|
||||
* Update dependencies for security. PR <a href="https://github.com/tiangolo/fastapi/pull/174" target="_blank">#174</a>.
|
||||
|
||||
* Add docs for <a href="https://fastapi.tiangolo.com/tutorial/middleware/" target="_blank">Middleware</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/173" target="_blank">#173</a>.
|
||||
|
||||
## 0.17.0
|
||||
|
||||
* Make Flit publish from CI. PR <a href="https://github.com/tiangolo/fastapi/pull/170" target="_blank">#170</a>.
|
||||
|
||||
* Add documentation about handling <a href="https://fastapi.tiangolo.com/tutorial/cors/" target="_blank">CORS (Cross-Origin Resource Sharing)</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/169" target="_blank">#169</a>.
|
||||
|
||||
* By default, encode by alias. This allows using Pydantic `alias` parameters working by default. PR <a href="https://github.com/tiangolo/fastapi/pull/168" target="_blank">#168</a>.
|
||||
|
||||
## 0.16.0
|
||||
|
||||
* Upgrade *path operation* `doctsring` parsing to support proper Markdown descriptions. New documentation at <a href="https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#description-from-docstring" target="_blank">Path Operation Configuration</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/163" target="_blank">#163</a>.
|
||||
|
||||
* Refactor internal usage of Pydantic to use correct data types. PR <a href="https://github.com/tiangolo/fastapi/pull/164" target="_blank">#164</a>.
|
||||
|
||||
* Upgrade Pydantic to version `0.23`. PR <a href="https://github.com/tiangolo/fastapi/pull/160" target="_blank">#160</a> by <a href="https://github.com/euri10" target="_blank">@euri10</a>.
|
||||
|
||||
* Fix typo in Tutorial about Extra Models. PR <a href="https://github.com/tiangolo/fastapi/pull/159" target="_blank">#159</a> by <a href="https://github.com/danielmichaels" target="_blank">@danielmichaels</a>.
|
||||
|
||||
* Fix <a href="https://fastapi.tiangolo.com/tutorial/query-params/" target="_blank">Query Parameters</a> URL examples in docs. PR <a href="https://github.com/tiangolo/fastapi/pull/157" target="_blank">#157</a> by <a href="https://github.com/hayata-yamamoto" target="_blank">@hayata-yamamoto</a>.
|
||||
|
||||
## 0.15.0
|
||||
|
||||
* Add support for multiple file uploads (as a single form field). New docs at: <a href="https://fastapi.tiangolo.com/tutorial/request-files/#multiple-file-uploads" target="_blank">Multiple file uploads</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/158" target="_blank">#158</a>.
|
||||
|
||||
* Add docs for: <a href="https://fastapi.tiangolo.com/tutorial/additional-status-codes/" target="_blank">Additional Status Codes</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/156" target="_blank">#156</a>.
|
||||
|
||||
## 0.14.0
|
||||
|
||||
* Improve automatically generated names of path operations in OpenAPI (in API docs). A function `read_items` instead of having a generated name "Read Items Get" will have "Read Items". PR <a href="https://github.com/tiangolo/fastapi/pull/155" target="_blank">#155</a>.
|
||||
|
||||
* Add docs for: <a href="https://fastapi.tiangolo.com/tutorial/testing/" target="_blank">Testing **FastAPI**</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/151" target="_blank">#151</a>.
|
||||
|
||||
* Update `/docs` Swagger UI to enable deep linking. This allows sharing the URL pointing directly to the path operation documentation in the docs. PR <a href="https://github.com/tiangolo/fastapi/pull/148" target="_blank">#148</a> by <a href="https://github.com/wshayes" target="_blank">@wshayes</a>.
|
||||
|
||||
* Update development dependencies, `Pipfile.lock`. PR <a href="https://github.com/tiangolo/fastapi/pull/150" target="_blank">#150</a>.
|
||||
|
||||
* Include Falcon and Hug in: <a href="https://fastapi.tiangolo.com/alternatives/" target="_blank">Alternatives, Inspiration and Comparisons</a>.
|
||||
|
||||
## 0.13.0
|
||||
|
||||
* Improve/upgrade OAuth2 scopes support with `SecurityScopes`:
|
||||
* `SecurityScopes` can be declared as a parameter like `Request`, to get the scopes of all super-dependencies/dependants.
|
||||
* Improve `Security` handling, merging scopes when declaring `SecurityScopes`.
|
||||
* Allow using `SecurityBase` (like `OAuth2`) classes with `Depends` and still document them. `Security` now is needed only to declare `scopes`.
|
||||
* Updated docs about: <a href="https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/" target="_blank">OAuth2 with Password (and hashing), Bearer with JWT tokens</a>.
|
||||
* New docs about: <a href="https://fastapi.tiangolo.com/tutorial/security/oauth2-scopes/" target="_blank">OAuth2 scopes</a>.
|
||||
* PR <a href="https://github.com/tiangolo/fastapi/pull/141" target="_blank">#141</a>.
|
||||
|
||||
## 0.12.1
|
||||
|
||||
* Fix bug: handling additional `responses` in `APIRouter.include_router()`. PR <a href="https://github.com/tiangolo/fastapi/pull/140" target="_blank">#140</a>.
|
||||
|
||||
* Fix typo in SQL tutorial. PR <a href="https://github.com/tiangolo/fastapi/pull/138" target="_blank">#138</a> by <a href="https://github.com/mostaphaRoudsari" target="_blank">@mostaphaRoudsari</a>.
|
||||
|
||||
* Fix typos in section about nested models and OAuth2 with JWT. PR <a href="https://github.com/tiangolo/fastapi/pull/127" target="_blank">#127</a> by <a href="https://github.com/mmcloud" target="_blank">@mmcloud</a>.
|
||||
|
||||
## 0.12.0
|
||||
|
||||
* Add additional `responses` parameter to *path operation decorators* to extend responses in OpenAPI (and API docs).
|
||||
* It also allows extending existing responses generated from `response_model`, declare other media types (like images), etc.
|
||||
* The new documentation is here: <a href="https://fastapi.tiangolo.com/tutorial/additional-responses/" target="_blank">Additional Responses</a>.
|
||||
* `responses` can also be added to `.include_router()`, the updated docs are here: <a href="https://fastapi.tiangolo.com/tutorial/bigger-applications/#add-some-custom-tags-and-responses" target="_blank">Bigger Applications</a>.
|
||||
* PR <a href="https://github.com/tiangolo/fastapi/pull/97" target="_blank">#97</a> originally initiated by <a href="https://github.com/barsi" target="_blank">@barsi</a>.
|
||||
|
||||
* Update `scripts/test-cov-html.sh` to allow passing extra parameters like `-vv`, for development.
|
||||
|
||||
## 0.11.0
|
||||
|
||||
* Add `auto_error` parameter to security utility functions. Allowing them to be optional. Also allowing to have multiple alternative security schemes that are then checked in a single dependency instead of each one verifying and returning the error to the client automatically when not satisfied. PR <a href="https://github.com/tiangolo/fastapi/pull/134" target="_blank">#134</a>.
|
||||
|
||||
* Update <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/#create-a-middleware-to-handle-sessions" target="_blank">SQL Tutorial</a> to close database sessions even when there are exceptions. PR <a href="https://github.com/tiangolo/fastapi/pull/89" target="_blank">#89</a> by <a href="https://github.com/alexiri" target="_blank">@alexiri</a>.
|
||||
|
||||
* Fix duplicate dependency in `pyproject.toml`. PR <a href="https://github.com/tiangolo/fastapi/pull/128" target="_blank">#128</a> by <a href="https://github.com/zxalif" target="_blank">@zxalif</a>.
|
||||
|
||||
## 0.10.3
|
||||
|
||||
* Add Gitter chat, badge, links, etc. <a href="https://gitter.im/tiangolo/fastapi" target="_blank">https://gitter.im/tiangolo/fastapi
|
||||
</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/117" target="_blank">#117</a>.
|
||||
|
||||
* Add docs about <a href="https://fastapi.tiangolo.com/tutorial/extending-openapi/" target="_blank">Extending OpenAPI</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/126" target="_blank">#126</a>.
|
||||
|
||||
* Make Travis run Ubuntu Xenial (newer version) and Python 3.7 instead of Python 3.7-dev. PR <a href="https://github.com/tiangolo/fastapi/pull/92" target="_blank">#92</a> by <a href="https://github.com/blueyed" target="_blank">@blueyed</a>.
|
||||
|
||||
* Fix duplicated param variable creation. PR <a href="https://github.com/tiangolo/fastapi/pull/123" target="_blank">#123</a> by <a href="https://github.com/yihuang" target="_blank">@yihuang</a>.
|
||||
|
||||
* Add note in <a href="https://fastapi.tiangolo.com/tutorial/response-model/" target="_blank">Response Model docs</a> about why using a function parameter instead of a function return type annotation. PR <a href="https://github.com/tiangolo/fastapi/pull/109" target="_blank">#109</a> by <a href="https://github.com/JHSaunders" target="_blank">@JHSaunders</a>.
|
||||
|
||||
* Fix event docs (startup/shutdown) function name. PR <a href="https://github.com/tiangolo/fastapi/pull/105" target="_blank">#105</a> by <a href="https://github.com/stratosgear" target="_blank">@stratosgear</a>.
|
||||
|
||||
## 0.10.2
|
||||
|
||||
* Fix OpenAPI (JSON Schema) for declarations of Python `Union` (JSON Schema `additionalProperties`). PR <a href="https://github.com/tiangolo/fastapi/pull/121" target="_blank">#121</a>.
|
||||
|
||||
23
docs/src/additional_responses/tutorial001.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
id: str
|
||||
value: str
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/{item_id}", response_model=Item, responses={404: {"model": Message}})
|
||||
async def read_item(item_id: str):
|
||||
if item_id == "foo":
|
||||
return {"id": "foo", "value": "there goes my hero"}
|
||||
else:
|
||||
return JSONResponse(status_code=404, content={"message": "Item not found"})
|
||||
28
docs/src/additional_responses/tutorial002.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
id: str
|
||||
value: str
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get(
|
||||
"/items/{item_id}",
|
||||
response_model=Item,
|
||||
responses={
|
||||
200: {
|
||||
"content": {"image/png": {}},
|
||||
"description": "Return the JSON item or an image.",
|
||||
}
|
||||
},
|
||||
)
|
||||
async def read_item(item_id: str, img: bool = None):
|
||||
if img:
|
||||
return FileResponse("image.png", media_type="image/png")
|
||||
else:
|
||||
return {"id": "foo", "value": "there goes my hero"}
|
||||
37
docs/src/additional_responses/tutorial003.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
id: str
|
||||
value: str
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get(
|
||||
"/items/{item_id}",
|
||||
response_model=Item,
|
||||
responses={
|
||||
404: {"model": Message, "description": "The item was not found"},
|
||||
200: {
|
||||
"description": "Item requested by ID",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {"id": "bar", "value": "The bar tenders"}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
async def read_item(item_id: str):
|
||||
if item_id == "foo":
|
||||
return {"id": "foo", "value": "there goes my hero"}
|
||||
else:
|
||||
return JSONResponse(status_code=404, content={"message": "Item not found"})
|
||||
30
docs/src/additional_responses/tutorial004.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
id: str
|
||||
value: str
|
||||
|
||||
|
||||
responses = {
|
||||
404: {"description": "Item not found"},
|
||||
302: {"description": "The item was moved"},
|
||||
403: {"description": "Not enough privileges"},
|
||||
}
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get(
|
||||
"/items/{item_id}",
|
||||
response_model=Item,
|
||||
responses={**responses, 200: {"content": {"image/png": {}}}},
|
||||
)
|
||||
async def read_item(item_id: str, img: bool = None):
|
||||
if img:
|
||||
return FileResponse("image.png", media_type="image/png")
|
||||
else:
|
||||
return {"id": "foo", "value": "there goes my hero"}
|
||||
20
docs/src/additional_status_codes/tutorial001.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from fastapi import Body, FastAPI
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.status import HTTP_201_CREATED
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}}
|
||||
|
||||
|
||||
@app.put("/items/{item_id}")
|
||||
async def upsert_item(item_id: str, name: str = Body(None), size: int = Body(None)):
|
||||
if item_id in items:
|
||||
item = items[item_id]
|
||||
item["name"] = name
|
||||
item["size"] = size
|
||||
return item
|
||||
else:
|
||||
item = {"name": name, "size": size}
|
||||
items[item_id] = item
|
||||
return JSONResponse(status_code=HTTP_201_CREATED, content=item)
|
||||
0
docs/src/app_testing/__init__.py
Normal file
8
docs/src/app_testing/main.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def read_main():
|
||||
return {"msg": "Hello World"}
|
||||
11
docs/src/app_testing/test_main.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from .main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_read_main():
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"msg": "Hello World"}
|
||||
18
docs/src/app_testing/tutorial001.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def read_main():
|
||||
return {"msg": "Hello World"}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_read_main():
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"msg": "Hello World"}
|
||||
31
docs/src/app_testing/tutorial002.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
from starlette.websockets import WebSocket
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def read_main():
|
||||
return {"msg": "Hello World"}
|
||||
|
||||
|
||||
@app.websocket_route("/ws")
|
||||
async def websocket(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
await websocket.send_json({"msg": "Hello WebSocket"})
|
||||
await websocket.close()
|
||||
|
||||
|
||||
def test_read_main():
|
||||
client = TestClient(app)
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"msg": "Hello World"}
|
||||
|
||||
|
||||
def test_websocket():
|
||||
client = TestClient(app)
|
||||
with client.websocket_connect("/ws") as websocket:
|
||||
data = websocket.receive_json()
|
||||
assert data == {"msg": "Hello WebSocket"}
|
||||
24
docs/src/app_testing/tutorial003.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
items = {}
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
items["foo"] = {"name": "Fighters"}
|
||||
items["bar"] = {"name": "Tenders"}
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def read_items(item_id: str):
|
||||
return items[item_id]
|
||||
|
||||
|
||||
def test_read_items():
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "Fighters"}
|
||||
@@ -5,4 +5,9 @@ from .routers import items, users
|
||||
app = FastAPI()
|
||||
|
||||
app.include_router(users.router)
|
||||
app.include_router(items.router, prefix="/items", tags=["items"])
|
||||
app.include_router(
|
||||
items.router,
|
||||
prefix="/items",
|
||||
tags=["items"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -11,3 +11,14 @@ async def read_items():
|
||||
@router.get("/{item_id}")
|
||||
async def read_item(item_id: str):
|
||||
return {"name": "Fake Specific Item", "item_id": item_id}
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{item_id}",
|
||||
tags=["custom"],
|
||||
responses={403: {"description": "Operation forbidden"}},
|
||||
)
|
||||
async def update_item(item_id: str):
|
||||
if item_id != "foo":
|
||||
raise HTTPException(status_code=403, detail="You can only update the item: foo")
|
||||
return {"item_id": item_id, "name": "The Fighters"}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from typing import Set
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from pydantic.types import UrlStr
|
||||
from pydantic import BaseModel, UrlStr
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from typing import List, Set
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from pydantic.types import UrlStr
|
||||
from pydantic import BaseModel, UrlStr
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -18,7 +17,7 @@ class Item(BaseModel):
|
||||
price: float
|
||||
tax: float = None
|
||||
tags: Set[str] = []
|
||||
image: List[Image] = None
|
||||
images: List[Image] = None
|
||||
|
||||
|
||||
@app.put("/items/{item_id}")
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from typing import List, Set
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from pydantic.types import UrlStr
|
||||
from pydantic import BaseModel, UrlStr
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -18,7 +17,7 @@ class Item(BaseModel):
|
||||
price: float
|
||||
tax: float = None
|
||||
tags: Set[str] = []
|
||||
image: List[Image] = None
|
||||
images: List[Image] = None
|
||||
|
||||
|
||||
class Offer(BaseModel):
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from pydantic.types import UrlStr
|
||||
from pydantic import BaseModel, UrlStr
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
19
docs/src/cors/tutorial001.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
origins = [
|
||||
"http://localhost.tiangolo.com",
|
||||
"https://localhost.tiangolo.com",
|
||||
"http:localhost",
|
||||
"http:localhost:8080",
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
@@ -12,5 +12,5 @@ async def startup_event():
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def read_item(item_id: str):
|
||||
async def read_items(item_id: str):
|
||||
return items[item_id]
|
||||
|
||||
@@ -4,7 +4,7 @@ app = FastAPI()
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
def startup_event():
|
||||
def shutdown_event():
|
||||
with open("log.txt", mode="a") as log:
|
||||
log.write("Application shutdown")
|
||||
|
||||
|
||||
28
docs/src/extending_openapi/tutorial001.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items():
|
||||
return [{"name": "Foo"}]
|
||||
|
||||
|
||||
def custom_openapi():
|
||||
if app.openapi_schema:
|
||||
return app.openapi_schema
|
||||
openapi_schema = get_openapi(
|
||||
title="Custom title",
|
||||
version="2.5.0",
|
||||
description="This is a very custom OpenAPI schema",
|
||||
routes=app.routes,
|
||||
)
|
||||
openapi_schema["info"]["x-logo"] = {
|
||||
"url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png"
|
||||
}
|
||||
app.openapi_schema = openapi_schema
|
||||
return app.openapi_schema
|
||||
|
||||
|
||||
app.openapi = custom_openapi
|
||||
@@ -6,7 +6,7 @@ items = {"foo": "The Foo Wrestlers"}
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def create_item(item_id: str):
|
||||
async def read_item(item_id: str):
|
||||
if item_id not in items:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
return {"item": items[item_id]}
|
||||
|
||||
@@ -6,7 +6,7 @@ items = {"foo": "The Foo Wrestlers"}
|
||||
|
||||
|
||||
@app.get("/items-header/{item_id}")
|
||||
async def create_item_header(item_id: str):
|
||||
async def read_item_header(item_id: str):
|
||||
if item_id not in items:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
|
||||
15
docs/src/middleware/tutorial001.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import time
|
||||
|
||||
from fastapi import FastAPI
|
||||
from starlette.requests import Request
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def add_process_time_header(request: Request, call_next):
|
||||
start_time = time.time()
|
||||
response = await call_next(request)
|
||||
process_time = time.time() - start_time
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
return response
|
||||
@@ -18,11 +18,11 @@ class Item(BaseModel):
|
||||
async def create_item(*, item: Item):
|
||||
"""
|
||||
Create an item with all the information:
|
||||
|
||||
* name: each item must have a name
|
||||
* description: a long description
|
||||
* price: required
|
||||
* tax: if the item doesn't have tax, you can omit this
|
||||
* tags: a set of unique tag strings for this item
|
||||
|
||||
- **name**: each item must have a name
|
||||
- **description**: a long description
|
||||
- **price**: required
|
||||
- **tax**: if the item doesn't have tax, you can omit this
|
||||
- **tags**: a set of unique tag strings for this item
|
||||
"""
|
||||
return item
|
||||
|
||||
@@ -23,11 +23,11 @@ class Item(BaseModel):
|
||||
async def create_item(*, item: Item):
|
||||
"""
|
||||
Create an item with all the information:
|
||||
|
||||
* name: each item must have a name
|
||||
* description: a long description
|
||||
* price: required
|
||||
* tax: if the item doesn't have tax, you can omit this
|
||||
* tags: a set of unique tag strings for this item
|
||||
|
||||
- **name**: each item must have a name
|
||||
- **description**: a long description
|
||||
- **price**: required
|
||||
- **tax**: if the item doesn't have tax, you can omit this
|
||||
- **tags**: a set of unique tag strings for this item
|
||||
"""
|
||||
return item
|
||||
|
||||
33
docs/src/request_files/tutorial002.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI, File, UploadFile
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/files/")
|
||||
async def create_files(files: List[bytes] = File(...)):
|
||||
return {"file_sizes": [len(file) for file in files]}
|
||||
|
||||
|
||||
@app.post("/uploadfiles/")
|
||||
async def create_upload_files(files: List[UploadFile] = File(...)):
|
||||
return {"filenames": [file.filename for file in files]}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def main():
|
||||
content = """
|
||||
<body>
|
||||
<form action="/files/" enctype="multipart/form-data" method="post">
|
||||
<input name="files" type="file" multiple>
|
||||
<input type="submit">
|
||||
</form>
|
||||
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
|
||||
<input name="files" type="file" multiple>
|
||||
<input type="submit">
|
||||
</form>
|
||||
</body>
|
||||
"""
|
||||
return HTMLResponse(content=content)
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import jwt
|
||||
from fastapi import Depends, FastAPI, HTTPException, Security
|
||||
from fastapi import Depends, FastAPI, HTTPException
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from jwt import PyJWTError
|
||||
from passlib.context import CryptContext
|
||||
@@ -12,7 +12,6 @@ from starlette.status import HTTP_403_FORBIDDEN
|
||||
# openssl rand -hex 32
|
||||
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
|
||||
ALGORITHM = "HS256"
|
||||
TOKEN_SUBJECT = "access"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||
|
||||
|
||||
@@ -32,7 +31,7 @@ class Token(BaseModel):
|
||||
token_type: str
|
||||
|
||||
|
||||
class TokenPayload(BaseModel):
|
||||
class TokenData(BaseModel):
|
||||
username: str = None
|
||||
|
||||
|
||||
@@ -83,20 +82,26 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None):
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire, "sub": TOKEN_SUBJECT})
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
async def get_current_user(token: str = Security(oauth2_scheme)):
|
||||
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
token_data = TokenPayload(**payload)
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
token_data = TokenData(username=username)
|
||||
except PyJWTError:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
|
||||
)
|
||||
raise credentials_exception
|
||||
user = get_user(fake_users_db, username=token_data.username)
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
|
||||
@@ -107,18 +112,18 @@ async def get_current_active_user(current_user: User = Depends(get_current_user)
|
||||
|
||||
|
||||
@app.post("/token", response_model=Token)
|
||||
async def route_login_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Incorrect email or password")
|
||||
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"username": form_data.username}, expires_delta=access_token_expires
|
||||
data={"sub": user.username}, expires_delta=access_token_expires
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@app.get("/users/me", response_model=User)
|
||||
@app.get("/users/me/", response_model=User)
|
||||
async def read_users_me(current_user: User = Depends(get_current_active_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
162
docs/src/security/tutorial005.py
Normal file
@@ -0,0 +1,162 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List
|
||||
|
||||
import jwt
|
||||
from fastapi import Depends, FastAPI, HTTPException, Security
|
||||
from fastapi.security import (
|
||||
OAuth2PasswordBearer,
|
||||
OAuth2PasswordRequestForm,
|
||||
SecurityScopes,
|
||||
)
|
||||
from jwt import PyJWTError
|
||||
from passlib.context import CryptContext
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
# to get a string like this run:
|
||||
# openssl rand -hex 32
|
||||
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||
|
||||
|
||||
fake_users_db = {
|
||||
"johndoe": {
|
||||
"username": "johndoe",
|
||||
"full_name": "John Doe",
|
||||
"email": "johndoe@example.com",
|
||||
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
|
||||
"disabled": False,
|
||||
},
|
||||
"alice": {
|
||||
"username": "alice",
|
||||
"full_name": "Alice Chains",
|
||||
"email": "alicechains@example.com",
|
||||
"hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm",
|
||||
"disabled": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: str = None
|
||||
scopes: List[str] = []
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: str = None
|
||||
full_name: str = None
|
||||
disabled: bool = None
|
||||
|
||||
|
||||
class UserInDB(User):
|
||||
hashed_password: str
|
||||
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(
|
||||
tokenUrl="/token",
|
||||
scopes={"me": "Read information about the current user.", "items": "Read items."},
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def get_user(db, username: str):
|
||||
if username in db:
|
||||
user_dict = db[username]
|
||||
return UserInDB(**user_dict)
|
||||
|
||||
|
||||
def authenticate_user(fake_db, username: str, password: str):
|
||||
user = get_user(fake_db, username)
|
||||
if not user:
|
||||
return False
|
||||
if not verify_password(password, user.hashed_password):
|
||||
return False
|
||||
return user
|
||||
|
||||
|
||||
def create_access_token(*, data: dict, expires_delta: timedelta = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme)
|
||||
):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
token_scopes = payload.get("scopes", [])
|
||||
token_data = TokenData(scopes=token_scopes, username=username)
|
||||
except (PyJWTError, ValidationError):
|
||||
raise credentials_exception
|
||||
user = get_user(fake_users_db, username=token_data.username)
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
for scope in security_scopes.scopes:
|
||||
if scope not in token_data.scopes:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not enough permissions"
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_active_user(
|
||||
current_user: User = Security(get_current_user, scopes=["me"])
|
||||
):
|
||||
if current_user.disabled:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
||||
|
||||
@app.post("/token", response_model=Token)
|
||||
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.username, "scopes": form_data.scopes},
|
||||
expires_delta=access_token_expires,
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@app.get("/users/me/", response_model=User)
|
||||
async def read_users_me(current_user: User = Depends(get_current_active_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
@app.get("/users/me/items/")
|
||||
async def read_own_items(
|
||||
current_user: User = Security(get_current_active_user, scopes=["items"])
|
||||
):
|
||||
return [{"item_id": "Foo", "owner": current_user.username}]
|
||||
11
docs/src/security/tutorial006.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPBasic()
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(credentials: HTTPBasicCredentials = Depends(security)):
|
||||
return {"username": credentials.username, "password": credentials.password}
|
||||
22
docs/src/security/tutorial007.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from fastapi import Depends, FastAPI, HTTPException
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPBasic()
|
||||
|
||||
|
||||
def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
|
||||
if credentials.username != "foo" or credentials.password != "password":
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
return credentials.username
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(username: str = Depends(get_current_username)):
|
||||
return {"username": username}
|
||||
@@ -3,6 +3,7 @@ from sqlalchemy import Boolean, Column, Integer, String, create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base, declared_attr
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
# SQLAlchemy specific code, as with any other app
|
||||
SQLALCHEMY_DATABASE_URI = "sqlite:///./test.db"
|
||||
@@ -66,7 +67,10 @@ def read_user(user_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
@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()
|
||||
response = Response("Internal server error", status_code=500)
|
||||
try:
|
||||
request.state.db = SessionLocal()
|
||||
response = await call_next(request)
|
||||
finally:
|
||||
request.state.db.close()
|
||||
return response
|
||||
|
||||
235
docs/tutorial/additional-responses.md
Normal file
@@ -0,0 +1,235 @@
|
||||
!!! warning
|
||||
This is a rather advanced topic.
|
||||
|
||||
If you are starting with **FastAPI**, you might not need this.
|
||||
|
||||
You can declare additional responses, with additional status codes, media types, descriptions, etc.
|
||||
|
||||
Those additional responses will be included in the OpenAPI schema, so they will also appear in the API docs.
|
||||
|
||||
But for those additional responses you have to make sure you return a `Response` like `JSONResponse` directly, with your status code and content.
|
||||
|
||||
## Additional Response with `model`
|
||||
|
||||
You can pass to your *path operation decorators* a parameter `responses`.
|
||||
|
||||
It receives a `dict`, the keys are status codes for each response, like `200`, and the values are other `dict`s with the information for each of them.
|
||||
|
||||
Each of those response `dict`s can have a key `model`, containing a Pydantic model, just like `response_model`.
|
||||
|
||||
**FastAPI** will take that model, generate its JSON Schema and include it in the correct place in OpenAPI.
|
||||
|
||||
For example, to declare another response with a status code `404` and a Pydantic model `Message`, you can write:
|
||||
|
||||
|
||||
```Python hl_lines="18 23"
|
||||
{!./src/additional_responses/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! note
|
||||
Have in mind that you have to return the `JSONResponse` directly.
|
||||
|
||||
!!! info
|
||||
The `model` key is not part of OpenAPI.
|
||||
|
||||
**FastAPI** will take the Pydantic model from there, generate the `JSON Schema`, and put it in the correct place.
|
||||
|
||||
The correct place is:
|
||||
|
||||
* In the key `content`, that has as value another JSON object (`dict`) that contains:
|
||||
* A key with the media type, e.g. `application/json`, that contains as value another JSON object, that contains:
|
||||
* A key `schema`, that has as the value the JSON Schema from the model, here's the correct place.
|
||||
* **FastAPI** adds a reference here to the global JSON Schemas in another place in your OpenAPI instead of including it directly. This way, other applications and clients can use those JSON Schemas directly, provide better code generation tools, etc.
|
||||
|
||||
The generated responses in the OpenAPI for this *path operation* will be:
|
||||
|
||||
```JSON hl_lines="3 4 5 6 7 8 9 10 11 12"
|
||||
{
|
||||
"responses": {
|
||||
"404": {
|
||||
"description": "Additional Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Item"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The schemas are referenced to another place inside the OpenAPI schema:
|
||||
|
||||
```JSON hl_lines="4 5 6 7 8 9 10 11 12 13 14 15 16"
|
||||
{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Message": {
|
||||
"title": "Message",
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"title": "Message",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Item": {
|
||||
"title": "Item",
|
||||
"required": [
|
||||
"id",
|
||||
"value"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"title": "Id",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"title": "Value",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": [
|
||||
"loc",
|
||||
"msg",
|
||||
"type"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"msg": {
|
||||
"title": "Message",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"title": "Error Type",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Additional media types for the main response
|
||||
|
||||
You can use this same `responses` parameter to add different media types for the same main response.
|
||||
|
||||
For example, you can add an additional media type of `image/png`, declaring that your *path operation* can return a JSON object (with media type `application/json`) or a PNG image:
|
||||
|
||||
```Python hl_lines="17 18 19 20 21 22 23 24 28"
|
||||
{!./src/additional_responses/tutorial002.py!}
|
||||
```
|
||||
|
||||
!!! note
|
||||
Notice that you have to return the image using a `FileResponse` directly.
|
||||
|
||||
## Combining information
|
||||
|
||||
You can also combine response information from multiple places, including the `response_model`, `status_code`, and `responses` parameters.
|
||||
|
||||
You can declare a `response_model`, using the default status code `200` (or a custom one if you need), and then declare additional information for that same response in `responses`, directly in the OpenAPI schema.
|
||||
|
||||
**FastAPI** will keep the additional information from `responses`, and combine it with the JSON Schema from your model.
|
||||
|
||||
For example, you can declare a response with a status code `404` that uses a Pydantic model and has a custom `description`.
|
||||
|
||||
And a response with a status code `200` that uses your `response_model`, but includes a custom `example`:
|
||||
|
||||
```Python hl_lines="20 21 22 23 24 25 26 27 28 29 30 31"
|
||||
{!./src/additional_responses/tutorial003.py!}
|
||||
```
|
||||
|
||||
It will all be combined and included in your OpenAPI, and shown in the API docs:
|
||||
|
||||
<img src="/img/tutorial/additional-responses/image01.png">
|
||||
|
||||
|
||||
## Combine predefined responses and custom ones
|
||||
|
||||
You might want to have some predefined responses that apply to many *path operations*, but you want to combine them with custom responses needed by each *path operation*.
|
||||
|
||||
For those cases, you can use the Python technique of "unpacking" a `dict` with `**dict_to_unpack`:
|
||||
|
||||
```Python
|
||||
old_dict = {
|
||||
"old key": "old value",
|
||||
"second old key": "second old value",
|
||||
}
|
||||
new_dict = {**old_dict, "new key": "new value"}
|
||||
```
|
||||
|
||||
Here, `new_dict` will contain all the key-value pairs from `old_dict` plus the new key-value pair:
|
||||
|
||||
```Python
|
||||
{
|
||||
"old key": "old value",
|
||||
"second old key": "second old value",
|
||||
"new key": "new value",
|
||||
}
|
||||
```
|
||||
|
||||
You can use that technique to re-use some predefined responses in your *path operations* and combine them with additional custom ones.
|
||||
|
||||
For example:
|
||||
|
||||
```Python hl_lines="11 12 13 14 15 24"
|
||||
{!./src/additional_responses/tutorial004.py!}
|
||||
```
|
||||
|
||||
## More information about OpenAPI responses
|
||||
|
||||
To see what exactly you can include in the responses, you can check these sections in the OpenAPI specification:
|
||||
|
||||
* <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responsesObject" target="_blank">OpenAPI Responses Object</a>, it includes the `Response Object`.
|
||||
* <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject" target="_blank">OpenAPI Response Object</a>, you can include anything from this directly in each response inside your `responses` parameter. Including `description`, `headers`, `content` (inside of this is that you declare different media types and JSON Schemas), and `links`.
|
||||
30
docs/tutorial/additional-status-codes.md
Normal file
@@ -0,0 +1,30 @@
|
||||
By default, **FastAPI** will return the responses using Starlette's `JSONResponse`, putting the content you return from your *path operation* inside of that `JSONResponse`.
|
||||
|
||||
It will use the default status code or the one you set in your *path operation*.
|
||||
|
||||
## Additional status codes
|
||||
|
||||
If you want to return additional status codes apart from the main one, you can do that by returning a `Response` directly, like a `JSONResponse`, and set the additional status code directly.
|
||||
|
||||
For example, let's say that you want to have a *path operation* that allows to update items, and returns HTTP status codes of 200 "OK" when successful.
|
||||
|
||||
But you also want it to accept new items. And when the items didn't exist before, it creates them, and returns an HTTP status code of 201 "Created".
|
||||
|
||||
To achieve that, import `JSONResponse`, and return your content there directly, setting the `status_code` that you want:
|
||||
|
||||
```Python hl_lines="2 20"
|
||||
{!./src/additional_status_codes/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! warning
|
||||
When you return a `Response` directly, like in the example above, it will be returned directly.
|
||||
|
||||
It won't be serialized with a model, etc.
|
||||
|
||||
Make sure it has the data you want it to have, and that the values are valid JSON (if you are using `JSONResponse`).
|
||||
|
||||
## OpenAPI and API docs
|
||||
|
||||
If you return additional status codes and responses directly, they won't be included in the OpenAPI schema (the API docs), because FastAPI doesn't have a way to know before hand what you are going to return.
|
||||
|
||||
But you can document that in your code, using: <a href="https://fastapi.tiangolo.com/tutorial/additional-responses/" target="_blank">Additional Responses</a>.
|
||||
@@ -103,7 +103,17 @@ But let's say that this time we are more lazy.
|
||||
|
||||
And we don't want to have to explicitly type `/items/` and `tags=["items"]` in every *path operation* (we will be able to do it later):
|
||||
|
||||
```Python hl_lines="6 11 16"
|
||||
```Python hl_lines="6 11"
|
||||
{!./src/bigger_applications/app/routers/items.py!}
|
||||
```
|
||||
|
||||
### Add some custom `tags` and `responses`
|
||||
|
||||
We are not adding the prefix `/items/` nor the `tags=["items"]` to add them later.
|
||||
|
||||
But we can add custom `tags` and `responses` that will be applied to a specific *path operation*:
|
||||
|
||||
```Python hl_lines="18 19"
|
||||
{!./src/bigger_applications/app/routers/items.py!}
|
||||
```
|
||||
|
||||
@@ -192,7 +202,7 @@ So, to be able to use both of them in the same file, we import the submodules di
|
||||
|
||||
Now, let's include the `router` from the submodule `users`:
|
||||
|
||||
```Python hl_lines="8"
|
||||
```Python hl_lines="7"
|
||||
{!./src/bigger_applications/app/main.py!}
|
||||
```
|
||||
|
||||
@@ -217,7 +227,7 @@ It will include all the routes from that router as part of it.
|
||||
So it won't affect performance.
|
||||
|
||||
|
||||
### Include an `APIRouter` with a prefix
|
||||
### Include an `APIRouter` with a `prefix`, `tags`, and `responses`
|
||||
|
||||
Now, let's include the router form the `items` submodule.
|
||||
|
||||
@@ -237,9 +247,11 @@ async def read_item(item_id: str):
|
||||
|
||||
So, the prefix in this case would be `/items`.
|
||||
|
||||
And we can also add a list of `tags` that will be applied to all the *path operations* included in this router:
|
||||
We can also add a list of `tags` that will be applied to all the *path operations* included in this router.
|
||||
|
||||
```Python hl_lines="9"
|
||||
And we can add predefined `responses` that will be included in all the *path operations* too.
|
||||
|
||||
```Python hl_lines="8 9 10 11 12 13"
|
||||
{!./src/bigger_applications/app/main.py!}
|
||||
```
|
||||
|
||||
@@ -250,12 +262,18 @@ The end result is that the item paths are now:
|
||||
|
||||
...as we intended.
|
||||
|
||||
And they are marked with a list of tags that contain a single string `"items"`.
|
||||
They will be marked with a list of tags that contain a single string `"items"`.
|
||||
|
||||
The *path operation* that declared a `"custom"` tag will have both tags, `items` and `custom`.
|
||||
|
||||
These "tags" are especially useful for the automatic interactive documentation systems (using OpenAPI).
|
||||
|
||||
And all of them will include the the predefined `responses`.
|
||||
|
||||
The *path operation* that declared a custom `403` response will have both the predefined responses (`404`) and the `403` declared in it directly.
|
||||
|
||||
!!! check
|
||||
The `prefix` and `tags` parameters are (as in many other cases) just a feature from **FastAPI** to help you avoid code duplication.
|
||||
The `prefix`, `tags`, and `responses` parameters are (as in many other cases) just a feature from **FastAPI** to help you avoid code duplication.
|
||||
|
||||
|
||||
!!! tip
|
||||
|
||||
@@ -120,7 +120,7 @@ To see all the options you have, checkout the docs for <a href="https://pydantic
|
||||
|
||||
For example, as in the `Image` model we have a `url` field, we can declare it to be instead of a `str`, a Pydantic's `UrlStr`:
|
||||
|
||||
```Python hl_lines="5 11"
|
||||
```Python hl_lines="4 10"
|
||||
{!./src/body_nested_models/tutorial005.py!}
|
||||
```
|
||||
|
||||
@@ -130,7 +130,7 @@ The string will be checked to be a valid URL, and documented in JSON Schema / Op
|
||||
|
||||
You can also use Pydantic models as subtypes of `list`, `set`, etc:
|
||||
|
||||
```Python hl_lines="21"
|
||||
```Python hl_lines="20"
|
||||
{!./src/body_nested_models/tutorial006.py!}
|
||||
```
|
||||
|
||||
@@ -167,7 +167,7 @@ This will expect (convert, validate, document, etc) a JSON body like:
|
||||
|
||||
You can define arbitrarily deeply nested models:
|
||||
|
||||
```Python hl_lines="10 15 21 24 28"
|
||||
```Python hl_lines="9 14 20 23 27"
|
||||
{!./src/body_nested_models/tutorial007.py!}
|
||||
```
|
||||
|
||||
@@ -184,7 +184,7 @@ images: List[Image]
|
||||
|
||||
as in:
|
||||
|
||||
```Python hl_lines="16"
|
||||
```Python hl_lines="15"
|
||||
{!./src/body_nested_models/tutorial008.py!}
|
||||
```
|
||||
|
||||
|
||||
55
docs/tutorial/cors.md
Normal file
@@ -0,0 +1,55 @@
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank">CORS or "Cross-Origin Resource Sharing"</a> refers to the situations when a frontend running in a browser has JavaScript code that communicates with a backend, and the backend is in a different "origin" than the frontend.
|
||||
|
||||
## Origin
|
||||
|
||||
An origin is the combination of protocol (`http`, `https`), domain (`myapp.com`, `localhost`, `localhost.tiangolo.com`), and port (`80`, `443`, `8080`).
|
||||
|
||||
So, all these are different origins:
|
||||
|
||||
* `http://localhost`
|
||||
* `https://localhost`
|
||||
* `http://localhost:8080`
|
||||
|
||||
Even if they are all in `localhost`, they use different protocols or ports, so, they are different "origins".
|
||||
|
||||
## Steps
|
||||
|
||||
So, let's say you have a frontend running in your browser at `http://localhost:8080`, and its JavaScript is trying to communicate with a backend running at `http://localhost` (because we don't specify a port, the browser will assume the default port `80`).
|
||||
|
||||
Then, the browser will send an HTTP `OPTIONS` request to the backend, and if the backend sends the appropriate headers authorizing the communication from this different origin (`http://localhost:8080`) then the browser will let the JavaScript in the frontend send its request to the backend.
|
||||
|
||||
To achieve this, the backend must have a list of "allowed origins".
|
||||
|
||||
In this case, it would have to include `http://localhost:8080` for the frontend to work correctly.
|
||||
|
||||
## Wildcards
|
||||
|
||||
It's also possible to declare the list as `"*"` (a "wildcard") to say that all are allowed.
|
||||
|
||||
But that will only allow certain types of communication, excluding everything that involves credentials: Cookies, Authorization headers like those used with Bearer Tokens, etc.
|
||||
|
||||
So, for everything to work correctly, it's better to specify explicitly the allowed origins.
|
||||
|
||||
## Use `CORSMiddleware`
|
||||
|
||||
You can configure it in your **FastAPI** application using Starlette's <a href="https://www.starlette.io/middleware/#corsmiddleware" target="_blank">`CORSMiddleware`</a>.
|
||||
|
||||
* Import it form Starlette.
|
||||
* Create a list of allowed origins (as strings).
|
||||
* Add it as a "middleware" to your **FastAPI** application.
|
||||
|
||||
You can also specify if your backend allows:
|
||||
|
||||
* Credentials (Authorization headers, Cookies, etc).
|
||||
* Specific HTTP methods (`POST`, `PUT`) or all of them with the wildcard `"*"`.
|
||||
* Specific HTTP headers or all of them with the wildcard `"*"`.
|
||||
|
||||
```Python hl_lines="2 6 7 8 9 10 11 13 14 15 16 17 18 19"
|
||||
{!./src/cors/tutorial001.py!}
|
||||
```
|
||||
|
||||
## More info
|
||||
|
||||
For more details of what you can specify in `CORSMiddleware`, check <a href="https://www.starlette.io/middleware/#corsmiddleware" target="_blank">Starlette's `CORSMiddleware` docs</a>.
|
||||
|
||||
For more info about <abbr title="Cross-Origin Resource Sharing">CORS</abbr>, check the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank">Mozilla CORS documentation</a>.
|
||||
90
docs/tutorial/extending-openapi.md
Normal file
@@ -0,0 +1,90 @@
|
||||
!!! warning
|
||||
This is a rather advanced feature. You probably can skip it.
|
||||
|
||||
If you are just following the tutorial - user guide, you can probably skip this section.
|
||||
|
||||
If you already know that you need to modify the generated OpenAPI schema, continue reading.
|
||||
|
||||
|
||||
There are some cases where you might need to modify the generated OpenAPI schema.
|
||||
|
||||
In this section you will see how.
|
||||
|
||||
## The normal process
|
||||
|
||||
The normal (default) process, is as follows.
|
||||
|
||||
A `FastAPI` application (instance) has an `.openapi()` method that is expected to return the OpenAPI schema.
|
||||
|
||||
As part of the application object creation, a *path operation* for `/openapi.json` (or for whatever you set your `openapi_url`) is registered.
|
||||
|
||||
It just returns a JSON response with the result of the application's `.openapi()` method.
|
||||
|
||||
By default, what the method `.openapi()` does is check the property `.openapi_schema` to see if it has contents and return them.
|
||||
|
||||
If it doesn't, it generates them using the utility function at `fastapi.openapi.utils.get_openapi`.
|
||||
|
||||
And that function `get_openapi()` receives as parameters:
|
||||
|
||||
* `title`: The OpenAPI title, shown in the docs.
|
||||
* `version`: The version of your API, e.g. `2.5.0`.
|
||||
* `openapi_version`: The version of the OpenAPI specification used. By default, the latest: `3.0.2`.
|
||||
* `description`: The description of your API.
|
||||
* `routes`: A list of routes, these are each of the registered *path operations*. They are taken from `app.routes`.
|
||||
* `openapi_prefix`: The URL prefix to be used in your OpenAPI.
|
||||
|
||||
## Overriding the defaults
|
||||
|
||||
Using the information above, you can use the same utility function to generate the OpenAPI schema and override each part that you need.
|
||||
|
||||
For example, let's add <a href="https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md#x-logo" target="_blank">ReDoc's OpenAPI extension to include a custom logo</a>.
|
||||
|
||||
### Normal **FastAPI**
|
||||
|
||||
First, write all your **FastAPI** application as normally:
|
||||
|
||||
```Python hl_lines="1 4 7 8 9"
|
||||
{!./src/extending_openapi/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Generate the OpenAPI schema
|
||||
|
||||
Then, use the same utility function to generate the OpenAPI schema, inside a `custom_openapi()` function:
|
||||
|
||||
```Python hl_lines="2 15 16 17 18 19 20"
|
||||
{!./src/extending_openapi/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Modify the OpenAPI schema
|
||||
|
||||
Now you can add the ReDoc extension, adding a custom `x-logo` to the `info` "object" in the OpenAPI schema:
|
||||
|
||||
```Python hl_lines="21 22 23"
|
||||
{!./src/extending_openapi/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Cache the OpenAPI schema
|
||||
|
||||
You can use the property `.openapi_schema` as a "cache", to store your generated schema.
|
||||
|
||||
That way, your application won't have to generate the schema every time a user opens your API docs.
|
||||
|
||||
It will be generated only once, and then the same cached schema will be used for the next requests.
|
||||
|
||||
```Python hl_lines="13 14 24 25"
|
||||
{!./src/extending_openapi/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Override the method
|
||||
|
||||
Now you can replace the `.openapi()` method with your new function.
|
||||
|
||||
```Python hl_lines="28"
|
||||
{!./src/extending_openapi/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Check it
|
||||
|
||||
Once you go to <a href="http://127.0.0.1:8000/redoc" target="_blank">http://127.0.0.1:8000/redoc</a> you will see that you are using your custom logo (in this example, **FastAPI**'s logo):
|
||||
|
||||
<img src="/img/tutorial/extending-openapi/image01.png">
|
||||
@@ -3,7 +3,7 @@ Continuing with the previous example, it will be common to have more than one re
|
||||
This is especially the case for user models, because:
|
||||
|
||||
* The **input model** needs to be able to have a password.
|
||||
* The **output model** should do not have a password.
|
||||
* The **output model** should not have a password.
|
||||
* The **database model** would probably need to have a hashed password.
|
||||
|
||||
!!! danger
|
||||
|
||||
54
docs/tutorial/middleware.md
Normal file
@@ -0,0 +1,54 @@
|
||||
You can add middleware to **FastAPI** applications.
|
||||
|
||||
A "middleware" is a function that works with every **request** before it is processed by any specific *path operation*. And also with every **response** before returning it.
|
||||
|
||||
* It takes each **request** that comes to your application.
|
||||
* It can then do something to that **request** or run any needed code.
|
||||
* Then it passes the **request** to be processed by the rest of the application (by some *path operation*).
|
||||
* It then takes the **response** generated by the application (by some *path operation*).
|
||||
* It can do something to that **response** or run any needed code.
|
||||
* Then it returns the **response**.
|
||||
|
||||
## Create a middleware
|
||||
|
||||
To create a middleware you use the decorator `@app.middleware("http")` on top of a function.
|
||||
|
||||
The middleware function receives:
|
||||
|
||||
* The `request`.
|
||||
* A function `call_next` that will receive the `request` as a parameter.
|
||||
* This function will pass the `request` to the corresponding *path operation*.
|
||||
* Then it returns the `response` generated by the corresponding *path operation*.
|
||||
* You can then modify further the `response` before returning it.
|
||||
|
||||
```Python hl_lines="9 10 12 15"
|
||||
{!./src/middleware/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
This technique is used in the tutorial about <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">SQL (Relational) Databases</a>.
|
||||
|
||||
### Before and after the `response`
|
||||
|
||||
You can add code to be run with the `request`, before any *path operation* receives it.
|
||||
|
||||
And also after the `response` is generated, before returning it.
|
||||
|
||||
For example, you could add a custom header `X-Process-Time` containing the time in seconds that it took to process the request and generate a response:
|
||||
|
||||
```Python hl_lines="11 13 14"
|
||||
{!./src/middleware/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Starlette's Middleware
|
||||
|
||||
You can also add any other <a href="https://www.starlette.io/middleware/" target="_blank">Starlette Middleware</a>.
|
||||
|
||||
These are classes instead of plain functions.
|
||||
|
||||
Including:
|
||||
|
||||
* `CORSMiddleware` (described in the next section).
|
||||
* `GZipMiddleware`.
|
||||
* `SentryMiddleware`.
|
||||
* ...and others.
|
||||
@@ -42,6 +42,8 @@ You can add a `summary` and `description`:
|
||||
|
||||
As descriptions tend to be long and cover multiple lines, you can declare the path operation description in the function <abbr title="a multi-line string as the first expression inside a function (not assigned to any variable) used for documentation">docstring</abbr> and **FastAPI** will read it from there.
|
||||
|
||||
You can write <a href="https://en.wikipedia.org/wiki/Markdown" target="_blank">Markdown</a> in the docstring, it will be interpreted and displayed correctly (taking into account docstring indentation).
|
||||
|
||||
```Python hl_lines="19 20 21 22 23 24 25 26 27"
|
||||
{!./src/path_operation_configuration/tutorial004.py!}
|
||||
```
|
||||
@@ -50,9 +52,6 @@ It will be used in the interactive docs:
|
||||
|
||||
<img src="/img/tutorial/path-operation-configuration/image02.png">
|
||||
|
||||
!!! info
|
||||
OpenAPI specifies that descriptions can be written in Markdown syntax, but the interactive documentation systems included still don't support it at the time of writing this, although they have it in their plans.
|
||||
|
||||
## Response description
|
||||
|
||||
You can specify the response description with the parameter `response_description`:
|
||||
|
||||
@@ -81,31 +81,31 @@ You can also declare `bool` types, and they will be converted:
|
||||
In this case, if you go to:
|
||||
|
||||
```
|
||||
http://127.0.0.1:8000/items/?short=1
|
||||
http://127.0.0.1:8000/items/foo?short=1
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
http://127.0.0.1:8000/items/?short=True
|
||||
http://127.0.0.1:8000/items/foo?short=True
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
http://127.0.0.1:8000/items/?short=true
|
||||
http://127.0.0.1:8000/items/foo?short=true
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
http://127.0.0.1:8000/items/?short=on
|
||||
http://127.0.0.1:8000/items/foo?short=on
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
http://127.0.0.1:8000/items/?short=yes
|
||||
http://127.0.0.1:8000/items/foo?short=yes
|
||||
```
|
||||
|
||||
or any other case variation (uppercase, first letter in uppercase, etc), your function will see the parameter `short` with a `bool` value of `True`. Otherwise as `False`.
|
||||
|
||||
@@ -43,7 +43,7 @@ Using `UploadFile` has several advantages over `bytes`:
|
||||
|
||||
* It uses a "spooled" file:
|
||||
* A file stored in memory up to a maximum size limit, and after passing this limit it will be stored in disk.
|
||||
* This means that it will work well for large files like images, videos, large binaries, etc. All without consuming all the memory.
|
||||
* This means that it will work well for large files like images, videos, large binaries, etc. without consuming all the memory.
|
||||
* You can get metadata from the uploaded file.
|
||||
* It has a <a href="https://docs.python.org/3/glossary.html#term-file-like-object" target="_blank">file-like</a> `async` interface.
|
||||
* It exposes an actual Python <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" target="_blank">`SpooledTemporaryFile`</a> object that you can pass directly to other libraries that expect a file-like object.
|
||||
@@ -107,6 +107,27 @@ The way HTML forms (`<form></form>`) sends the data to the server normally uses
|
||||
|
||||
This is not a limitation of **FastAPI**, it's part of the HTTP protocol.
|
||||
|
||||
## Multiple file uploads
|
||||
|
||||
It's possible to upload several files at the same time.
|
||||
|
||||
They would be associated to the same "form field" sent using "form data".
|
||||
|
||||
To use that, declare a `List` of `bytes` or `UploadFile`:
|
||||
|
||||
```Python hl_lines="10 15"
|
||||
{!./src/request_files/tutorial002.py!}
|
||||
```
|
||||
|
||||
You will receive, as declared, a `list` of `bytes` or `UploadFile`s.
|
||||
|
||||
!!! note
|
||||
Notice that, as of 2019-04-14, Swagger UI doesn't support multiple file uploads in the same form field. For more information, check <a href="https://github.com/swagger-api/swagger-ui/issues/4276" target="_blank">#4276</a> and <a href="https://github.com/swagger-api/swagger-ui/issues/3641" target="_blank">#3641</a>.
|
||||
|
||||
Nevertheless, **FastAPI** is already compatible with it, using the standard OpenAPI.
|
||||
|
||||
So, whenever Swagger UI supports multi-file uploads, or any other tools that supports OpenAPI, they will be compatible with **FastAPI**.
|
||||
|
||||
## Recap
|
||||
|
||||
Use `File` to declare files to be uploaded as input parameters (as form data).
|
||||
|
||||
@@ -24,6 +24,9 @@ But most importantly:
|
||||
|
||||
* Will limit the output data to that of the model. We'll see how that's important below.
|
||||
|
||||
!!! note "Technical Details"
|
||||
The response model is declared in this parameter instead of as a function return type annotation, because the path function may not actually return that response model but rather return a `dict`, database object or some other model, and then use the `response_model` to perform the field limiting and serialization.
|
||||
|
||||
## Return the same input data
|
||||
|
||||
Here we are declaring a `UserIn` model, it will contain a plaintext password:
|
||||
|
||||
40
docs/tutorial/security/http-basic-auth.md
Normal file
@@ -0,0 +1,40 @@
|
||||
For the simplest cases, you can use HTTP Basic Auth.
|
||||
|
||||
In HTTP Basic Auth, the application expects a header that contains a username and a password.
|
||||
|
||||
If it doesn't receive it, it returns an HTTP 401 "Unauthorized" error.
|
||||
|
||||
And returns a header `WWW-Authenticate` with a value of `Basic`, and an optional `realm` parameter.
|
||||
|
||||
That tells the browser to show the integrated prompt for a username and password.
|
||||
|
||||
Then, when you type that username and password, the browser sends them in the header automatically.
|
||||
|
||||
## Simple HTTP Basic Auth
|
||||
|
||||
* Import `HTTPBAsic` and `HTTPBasicCredentials`.
|
||||
* Create a "`security` scheme" using `HTTPBAsic`.
|
||||
* Use that `security` with a dependency in your *path operation*.
|
||||
* It returns an object of type `HTTPBasicCredentials`:
|
||||
* It contains the `username` and `password` sent.
|
||||
|
||||
|
||||
```Python hl_lines="2 6 10"
|
||||
{!./src/security/tutorial006.py!}
|
||||
```
|
||||
|
||||
When you try to open the URL for the first time (or click the "Execute" button in the docs) the browser will ask you for your username and password:
|
||||
|
||||
<img src="/img/tutorial/security/image12.png">
|
||||
|
||||
## Check the username
|
||||
|
||||
Here's a more complete example.
|
||||
|
||||
Use a dependency to check if the username and password are correct.
|
||||
|
||||
If the credentials are incorrect, return an `HTTPException` with a status code 401 (the same returned when no credentials are provided) and add the header `WWW-Authenticate` to make the browser show the login prompt again:
|
||||
|
||||
```Python hl_lines="10 11 12 13 14 15 16 17 21"
|
||||
{!./src/security/tutorial007.py!}
|
||||
```
|
||||
@@ -20,7 +20,7 @@ It is quite an extensive specification and covers several complex use cases.
|
||||
|
||||
It includes ways to authenticate using a "third party".
|
||||
|
||||
That's what all the system with "login with Facebook, Google, Twitter, GitHub" use underneath.
|
||||
That's what all the systems with "login with Facebook, Google, Twitter, GitHub" use underneath.
|
||||
|
||||
### OAuth 1
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ If you want to play with JWT tokens and see how they work, check <a href="https:
|
||||
|
||||
## Install `PyJWT`
|
||||
|
||||
We need to install `PyJWT` to generate and verity the JWT tokens in Python:
|
||||
We need to install `PyJWT` to generate and verify the JWT tokens in Python:
|
||||
|
||||
```bash
|
||||
pip install pyjwt
|
||||
@@ -46,7 +46,7 @@ So, the thief won't be able to try to use that password in another system (as ma
|
||||
|
||||
PassLib is a great Python package to handle password hashes.
|
||||
|
||||
It supports many secure hashing algorithms, and utilities to work with them.
|
||||
It supports many secure hashing algorithms and utilities to work with them.
|
||||
|
||||
The recommended algorithm is "Bcrypt".
|
||||
|
||||
@@ -69,7 +69,7 @@ Import the tools we need from `passlib`.
|
||||
Create a PassLib "context". This is what will be used to hash and verify passwords.
|
||||
|
||||
!!! tip
|
||||
The PassLib context also has functionality to use different hashing algorithms, deprecate old ones, but allow verifying them, etc.
|
||||
The PassLib context also has functionality to use different hashing algorithms, including deprecate old ones only to allow verifying them, etc.
|
||||
|
||||
For example, you could use it to read and verify passwords generated by another system (like Django) but hash any new passwords with a different algorithm like Bcrypt.
|
||||
|
||||
@@ -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 50 57 58 61 62 71 72 73 74 75 76 77"
|
||||
```Python hl_lines="7 39 56 57 60 61 70 71 72 73 74 75 76"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
@@ -94,7 +94,7 @@ Import the modules installed.
|
||||
|
||||
Create a random secret key that will be used to sign the JWT tokens.
|
||||
|
||||
To generate a secure random secret, key use the command:
|
||||
To generate a secure random secret key use the command:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
@@ -104,15 +104,13 @@ And copy the output to the variable `SECRET_KEY` (don't use the one in the examp
|
||||
|
||||
Create a variable `ALGORITHM` with the algorithm used to sign the JWT token and set it to `"HS256"`.
|
||||
|
||||
And another one for the `TOKEN_SUBJECT`, and set it to, for example, `"access"`.
|
||||
|
||||
Create a variable for the expiration of the token.
|
||||
|
||||
Define a Pydantic Model that will be used in the token endpoint for the response.
|
||||
|
||||
Create a utility function to generate a new access token.
|
||||
|
||||
```Python hl_lines="3 6 13 14 15 16 30 31 32 80 81 82 83 84 85 86 87 88"
|
||||
```Python hl_lines="3 6 13 14 15 29 30 31 79 80 81 82 83 84 85 86 87"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
@@ -124,7 +122,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="91 92 93 94 95 96 97 98 99 100"
|
||||
```Python hl_lines="90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
@@ -134,10 +132,33 @@ Create a `timedelta` with the expiration time of the token.
|
||||
|
||||
Create a real JWT access token and return it.
|
||||
|
||||
```Python hl_lines="114 115 116 117 118"
|
||||
```Python hl_lines="114 115 116 117 118 119 120 121 122 123"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
|
||||
### Technical details about the JWT "subject" `sub`
|
||||
|
||||
The JWT specification says that there's a key `sub`, with the subject of the token.
|
||||
|
||||
It's optional to use it, but that's where you would put the user's identification, so we are using it here.
|
||||
|
||||
JWT might be used for other things apart from identifying a user and allowing him to perform operations directly on your API.
|
||||
|
||||
For example, you could identify a "car" or a "blog post".
|
||||
|
||||
Then you could add permissions about that entity, like "drive" (for the car) or "edit" (for the blog).
|
||||
|
||||
And then, you could give that JWT token to a user (or bot), and he could use it to perform those actions (drive the car, or edit the blog post) without even needing to have an account, just with the JWT token your API generated for that.
|
||||
|
||||
Using these ideas, JWT can be used for way more sophisticate scenarios.
|
||||
|
||||
In those cases, several of those entities could have the same ID, let's say `foo` (a user `foo`, a car `foo`, and a blog post `foo`).
|
||||
|
||||
So, to avoid ID collisions, when creating the JWT token for the user, you could prefix the value of the `sub` key, e.g. with `username:`.
|
||||
|
||||
The important thing to have in mind is that the `sub` key should have a unique identifier across the entire application.
|
||||
|
||||
## Check it
|
||||
|
||||
Run the server and go to the docs: <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
@@ -158,7 +179,7 @@ Password: `secret`
|
||||
|
||||
<img src="/img/tutorial/security/image08.png">
|
||||
|
||||
Call the endpoint `/users/me`, you will get the response as:
|
||||
Call the endpoint `/users/me/`, you will get the response as:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -180,15 +201,17 @@ If you open the developer tools, you could see how the data sent and received is
|
||||
|
||||
## Advanced usage with `scopes`
|
||||
|
||||
We didn't use it in this example, but `Security` can receive a parameter `scopes`, as a list of strings.
|
||||
OAuth2 has the notion of "scopes".
|
||||
|
||||
It would describe the scopes required for a specific path operation, as different path operations might require different security scopes, even while using the same `OAuth2PasswordBearer` (or any of the other tools).
|
||||
You can use them to add a specific set of permissions to a JWT token.
|
||||
|
||||
This only applies to OAuth2, and it might be, more or less, an advanced feature, but it is there, if you need to use it.
|
||||
Then you can give this token to a user directly or a third party, to interact with your API with a set of restrictions.
|
||||
|
||||
You can learn how to use them and how they are integrated into **FastAPI** in the next section.
|
||||
|
||||
## Recap
|
||||
|
||||
This concludes our tour for the security features of **FastAPI**.
|
||||
With what you have seen up to now, you can set up a secure **FastAPI** application using standards like OAuth2 and JWT.
|
||||
|
||||
In almost any framework handling the security becomes a rather complex subject quite quickly.
|
||||
|
||||
@@ -198,10 +221,14 @@ Many packages that simplify it a lot have to make many compromises with the data
|
||||
|
||||
**FastAPI** doesn't make any compromise with any database, data model or tool.
|
||||
|
||||
It gives you all the flexibility to chose the ones that fit your project the best.
|
||||
It gives you all the flexibility to choose the ones that fit your project the best.
|
||||
|
||||
And you can use directly many well maintained and widely used packages like `passlib` and `pyjwt`, because **FastAPI** doesn't require any complex mechanisms to integrate external packages.
|
||||
|
||||
But it provides you the tools to simplify the process as much as possible without compromising flexibility, robustness or security.
|
||||
|
||||
And you can use secure, standard protocols like OAuth2 in a relatively simple way.
|
||||
|
||||
In the next (optional) section you can see how to extend this even further, using OAuth2 "scopes", for a more fine-grained permission system following standards.
|
||||
|
||||
OAuth2 with scopes (explained in the next section) is the mechanism used by many big authentication providers, like Facebook, Google, GitHub, Microsoft, Twitter, etc.
|
||||
|
||||
191
docs/tutorial/security/oauth2-scopes.md
Normal file
@@ -0,0 +1,191 @@
|
||||
You can use OAuth2 scopes directly with **FastAPI**, they are integrated to work seamlessly.
|
||||
|
||||
This would allow you to have a more fine-grained permission system, following the OAuth2 standard, integrated into your OpenAPI application (and the API docs).
|
||||
|
||||
OAuth2 with scopes is the mechanism used by many big authentication providers, like Facebook, Google, GitHub, Microsoft, Twitter, etc. They use it to provide specific permissions to users and applications.
|
||||
|
||||
Every time you "log in with" Facebook, Google, GitHub, Microsoft, Twitter, that application is using OAuth2 with scopes.
|
||||
|
||||
In this section you will see how to manage authentication and authorization with the same OAuth2 with scopes in your **FastAPI** application.
|
||||
|
||||
!!! warning
|
||||
This is a more or less advanced section. If you are just starting, you can skip it.
|
||||
|
||||
You don't necessarily need OAuth2 scopes, you can handle authentication and authorization however you want.
|
||||
|
||||
But OAuth2 with scopes can be nicely integrated into your API (with OpenAPI) and your API docs.
|
||||
|
||||
Nevertheless, you still enforce those scopes or any other security/authorization requirement however you need in your code.
|
||||
|
||||
In many cases, OAuth2 with scopes can be an overkill.
|
||||
|
||||
But if you know you need it, or you are curious, keep reading.
|
||||
|
||||
## OAuth2 scopes and OpenAPI
|
||||
|
||||
The OAuth2 specification defines "scopes" as a list of strings separated by spaces.
|
||||
|
||||
The content of each of these strings can have any format, but should not contain spaces.
|
||||
|
||||
These scopes represent "permissions".
|
||||
|
||||
In OpenAPI (e.g. the API docs), you can define "security schemes", the same as you saw in the previous sections.
|
||||
|
||||
When one of these security schemes uses OAuth2, you can also declare and use scopes.
|
||||
|
||||
## Global view
|
||||
|
||||
First, let's quickly see the parts that change from the previous section about OAuth2 and JWT. Now using OAuth2 scopes:
|
||||
|
||||
```Python hl_lines="2 5 9 13 48 66 106 115 116 117 122 123 124 125 126 131 145 158"
|
||||
{!./src/security/tutorial005.py!}
|
||||
```
|
||||
|
||||
Now let's review those changes step by step.
|
||||
|
||||
## OAuth2 Security scheme
|
||||
|
||||
The first change is that now we are declaring the OAuth2 security scheme with two available scopes, `me` and `items`.
|
||||
|
||||
The `scopes` parameter receives a `dict` with each scope as a key and the description as the value:
|
||||
|
||||
```Python hl_lines="64 65 66 67"
|
||||
{!./src/security/tutorial005.py!}
|
||||
```
|
||||
|
||||
Because we are now declaring those scopes,they will show up in the API docs when you log-in/authorize.
|
||||
|
||||
And you will be able to select which scopes you want to give access to: `me` and `items`.
|
||||
|
||||
This is the same mechanism used when you give permissions while logging in with Facebook, Google, GitHub, etc:
|
||||
|
||||
<img src="/img/tutorial/security/image11.png">
|
||||
|
||||
## JWT token with scopes
|
||||
|
||||
Now, modify the token *path operation* to return the scopes requested.
|
||||
|
||||
We are still using the same `OAuth2PasswordRequestForm`. It includes a property `scopes` with each scope it received.
|
||||
|
||||
And we return the scopes as part of the JWT token.
|
||||
|
||||
!!! danger
|
||||
For simplicity, here we are just adding the scopes received directly to the token.
|
||||
|
||||
But in your application, for security, you should make sure you only add the scopes that the user is actually able to have, or the ones you have predefined.
|
||||
|
||||
```Python hl_lines="145"
|
||||
{!./src/security/tutorial005.py!}
|
||||
```
|
||||
|
||||
## Declare scopes in *path operations* and dependencies
|
||||
|
||||
Now we declare that the *path operation* for `/users/me/items/` requires the scope `items`.
|
||||
|
||||
For this, we import and use `Security` from `fastapi`.
|
||||
|
||||
You can use `Security` to declare dependencies (just like `Depends`), but `Security` also receives a parameter `scopes` with a list of scopes (strings).
|
||||
|
||||
In this case, we pass a dependency function `get_current_active_user` to `Security` (the same way we would do with `Depends`).
|
||||
|
||||
But we also pass a `list` of scopes, in this case with just one scope: `items` (it could have more).
|
||||
|
||||
And the dependency function `get_current_active_user` can also declare sub-dependencies, not only with `Depends` but also with `Security`. Declaring its own sub-dependency function (`get_current_user`), and more scope requirements.
|
||||
|
||||
In this case, it requires the scope `me` (it could require more than one scope).
|
||||
|
||||
!!! note
|
||||
You don't necessarily need to add different scopes in different places.
|
||||
|
||||
We are doing it here to demonstrate how **FastAPI** handles scopes declared at different levels.
|
||||
|
||||
```Python hl_lines="5 131 158"
|
||||
{!./src/security/tutorial005.py!}
|
||||
```
|
||||
|
||||
## Use `SecurityScopes`
|
||||
|
||||
Now update the dependency `get_current_user`.
|
||||
|
||||
This is the one used by the dependencies above.
|
||||
|
||||
Here's were we are declaring the same OAuth2 scheme we created above as a dependency: `oauth2_scheme`.
|
||||
|
||||
Because this dependency function doesn't have any scope requirements itself, we can use `Depends` with `oauth2_scheme`, we don't have to use `Security`.
|
||||
|
||||
We also declare a special parameter of type `SecurityScopes`, imported from `fastapi.security`.
|
||||
|
||||
This `SecurityScopes` class is similar to `Request` (`Request` was used to get the request object directly).
|
||||
|
||||
The parameter `security_scopes` will be of type `SecurityScopes`. It will have a property `scopes` with a list containing all the scopes required by itself and all the dependencies that use this as a sub-dependency. That means, all the "dependants" or all the super-dependencies (the contrary of sub-dependencies).
|
||||
|
||||
We verify that all the scopes required, by this dependency and all the dependants (including *path operations*), are included in the scopes provided in the token received, otherwise raise an `HTTPException`.
|
||||
|
||||
We also check that the token data is validated with the Pydantic model (catching the `ValidationError` exception), and if we get an error reading the JWT token or validating the data with Pydantic, we also raise an `HTTPException`.
|
||||
|
||||
By validating the data with Pydantic we can make sure that we have, for example, exactly a `list` of `str` with the scopes and a `str` with the `username`. Instead of, for example, a `dict`, or something else, as it could break the application at some point later.
|
||||
|
||||
|
||||
```Python hl_lines="9 13 106 48 106 115 116 117 122 123"
|
||||
{!./src/security/tutorial005.py!}
|
||||
```
|
||||
|
||||
So, as the other dependency `get_current_active_user` has as a sub-dependency this `get_current_user`, the scope `"me"` declared at `get_current_active_user` will be included in the `security_scopes.scopes` `list` inside of `get_current_user`.
|
||||
|
||||
And as the *path operation* itself also declares a scope `"items"`, it will also be part of this `list` `security_scopes.scopes` in `get_current_user`.
|
||||
|
||||
Here's how the hierarchy of dependencies and scopes looks like:
|
||||
|
||||
* The *path operation* `read_own_items` has:
|
||||
* Required scopes `["items"]` with the dependency:
|
||||
* `get_current_active_user`:
|
||||
* The dependency function `get_current_active_user` has:
|
||||
* Required scopes `["me"]` with the dependency:
|
||||
* `get_current_user`:
|
||||
* The dependency function `get_current_user` has:
|
||||
* No scopes required by itself.
|
||||
* A dependency using `oauth2_scheme`.
|
||||
* A `security_scopes` parameter of type `SecurityScopes`:
|
||||
* This `security_scopes` parameter has a property `scopes` with a `list` containing all these scopes declared above, so:
|
||||
* `security_scopes.scopes` will contain `["me", "items"]`
|
||||
|
||||
## More details about `SecurityScopes`
|
||||
|
||||
You can use `SecurityScopes` at any point, and in multiple places, it doesn't have to be at the "root" dependency.
|
||||
|
||||
It will always have the security scopes declared in the current `Security` dependencies and all the super-dependencies/dependants.
|
||||
|
||||
Because the `SecurityScopes` will have all the scopes declared by super-dependencies/dependants, you can use it to verify that a token has the required scopes in a central dependency function, and then declare different scope requirements in different *path operations*.
|
||||
|
||||
## Check it
|
||||
|
||||
If you open the API docs, you can authenticate and specify which scopes you want to authorize.
|
||||
|
||||
<img src="/img/tutorial/security/image11.png">
|
||||
|
||||
If you don't select any scope, you will be "authenticated", but when you try to access `/users/me/` or `/users/me/items/` you will get an error saying that you don't have enough permissions.
|
||||
|
||||
And if you select the scope `me` but not the scope `items`, you will be able to access `/users/me/` but not `/users/me/items/`.
|
||||
|
||||
That's what would happen to a third party application that tried to access one of these *path operations* with a token provided by a user, depending on how many permissions the user gave the application.
|
||||
|
||||
## About third party integrations
|
||||
|
||||
In this example we are using the OAuth2 "password" flow.
|
||||
|
||||
This is appropriate when we are logging in to our own application, probably with our own frontend.
|
||||
|
||||
Because we can trust it to receive the `username` and `password`, as we control it.
|
||||
|
||||
But if you are building an OAuth2 application that others would connect to (i.e., if you are building an authentication provider equivalent to Facebook, Google, GitHub, etc.) you should use one of the other flows.
|
||||
|
||||
The most common is the implicit flow.
|
||||
|
||||
The most secure is the code flow, but is more complex to implement as it requires more steps. As it is more cumbersome, many providers end up suggesting the implicit flow.
|
||||
|
||||
!!! note
|
||||
It's common that each authentication provider names their flows in a different way, to make it part of their brand.
|
||||
|
||||
But in the end, they are implementing the same OAuth2 standard.
|
||||
|
||||
**FastAPI** includes utilities for all these OAuth2 authentication flows in `fastapi.security.oauth2`.
|
||||
@@ -37,7 +37,7 @@ For now, don't pay attention to the rest, only the imports:
|
||||
|
||||
Define the database that SQLAlchemy should "connect" to:
|
||||
|
||||
```Python hl_lines="8"
|
||||
```Python hl_lines="9"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -59,7 +59,7 @@ SQLALCHEMY_DATABASE_URI = "postgresql://user:password@postgresserver/db"
|
||||
|
||||
## Create the SQLAlchemy `engine`
|
||||
|
||||
```Python hl_lines="11 12 13"
|
||||
```Python hl_lines="12 13 14"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -86,11 +86,11 @@ This object (class) is not a connection to the database yet, but once we create
|
||||
|
||||
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.
|
||||
We will use `Session` to declare types later and to get better editor support and completion.
|
||||
|
||||
For now, create the `SessionLocal`:
|
||||
|
||||
```Python hl_lines="14"
|
||||
```Python hl_lines="15"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -108,10 +108,17 @@ A "middleware" is a function that is always executed for each request, and have
|
||||
|
||||
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"
|
||||
```Python hl_lines="68 69 70 71 72 73 74 75 76"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
We put the creation of the `SessionLocal()` and handling of the requests in a `try` block.
|
||||
|
||||
And then we close it in the `finally` block.
|
||||
|
||||
This way we make sure the database session is always closed after the request. Even if there was an exception in the middle.
|
||||
|
||||
### 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.
|
||||
@@ -126,7 +133,7 @@ And when using the dependency in a path operation function, we declare it with t
|
||||
|
||||
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"
|
||||
```Python hl_lines="54 55 69"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -145,13 +152,13 @@ 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="17 18 19 20 21"
|
||||
```Python hl_lines="18 19 20 21 22"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Create the SQLAlchemy `Base` model
|
||||
|
||||
```Python hl_lines="24"
|
||||
```Python hl_lines="25"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -161,7 +168,7 @@ Now this is finally code specific to your app.
|
||||
|
||||
Here's a user model that will be a table in the database:
|
||||
|
||||
```Python hl_lines="27 28 29 30 31"
|
||||
```Python hl_lines="28 29 30 31 32"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -169,7 +176,7 @@ Here's a user model that will be a table in the database:
|
||||
|
||||
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"
|
||||
```Python hl_lines="35 37 39 40 41 42 43 45"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -197,7 +204,7 @@ Also, as all the functionality is self-contained in the same code, you can copy
|
||||
|
||||
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="48 49"
|
||||
```Python hl_lines="49 50"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -207,7 +214,7 @@ Now, finally, here's the standard **FastAPI** code.
|
||||
|
||||
Create your app and path operation function:
|
||||
|
||||
```Python hl_lines="58 61 62 63 64"
|
||||
```Python hl_lines="59 62 63 64 65"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -243,7 +250,7 @@ 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="62"
|
||||
```Python hl_lines="63"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
|
||||
85
docs/tutorial/testing.md
Normal file
@@ -0,0 +1,85 @@
|
||||
Thanks to <a href="https://www.starlette.io/testclient/" target="_blank">Starlette's TestClient</a>, testing **FastAPI** applications is easy and enjoyable.
|
||||
|
||||
It is based on <a href="http://docs.python-requests.org" target="_blank">Requests</a>, so it's very familiar and intuitive.
|
||||
|
||||
With it, you can use <a href="https://docs.pytest.org/" target="_blank">pytest</a> directly with **FastAPI**.
|
||||
|
||||
## Using `TestClient`
|
||||
|
||||
Import `TestClient` from `starlette.testclient`.
|
||||
|
||||
Create a `TestClient` passing to it your **FastAPI**.
|
||||
|
||||
Create functions with a name that starts with `test_` (this is standard `pytest` conventions).
|
||||
|
||||
Use the `TestClient` object the same way as you do with `requests`.
|
||||
|
||||
Write simple `assert` statements with the standard Python expressions that you need to check (again, standard `pytest`).
|
||||
|
||||
```Python hl_lines="2 12 15 16 17 18"
|
||||
{!./src/app_testing/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Notice that the testing functions are normal `def`, not `async def`.
|
||||
|
||||
And the calls to the client are also normal calls, not using `await`.
|
||||
|
||||
This allows you to use `pytest` directly without complications.
|
||||
|
||||
|
||||
## Separating tests
|
||||
|
||||
In a real application, you probably would have your tests in a different file.
|
||||
|
||||
And your **FastAPI** application might also be composed of several files/modules, etc.
|
||||
|
||||
### **FastAPI** app file
|
||||
|
||||
Let's say you have a file `main.py` with your **FastAPI** app:
|
||||
|
||||
```Python
|
||||
{!./src/app_testing/main.py!}
|
||||
```
|
||||
|
||||
### Testing file
|
||||
|
||||
Then you could have a file `test_main.py` with your tests, and import your `app` from the `main` module (`main.py`):
|
||||
|
||||
```Python
|
||||
{!./src/app_testing/test_main.py!}
|
||||
```
|
||||
|
||||
## Testing WebSockets
|
||||
|
||||
You can use the same `TestClient` to test WebSockets.
|
||||
|
||||
For this, you use the `TestClient` in a `with` statement, connecting to the WebSocket:
|
||||
|
||||
```Python hl_lines="27 28 29 30 31"
|
||||
{!./src/app_testing/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Testing Events, `startup` and `shutdown`
|
||||
|
||||
When you need your event handlers (`startup` and `shutdown`) to run in your tests, you can use the `TestClient` with a `with` statement:
|
||||
|
||||
```Python hl_lines="9 10 11 12 20 21 22 23 24"
|
||||
{!./src/app_testing/tutorial003.py!}
|
||||
```
|
||||
|
||||
## Run it
|
||||
|
||||
After that, you just need to install `pytest`:
|
||||
|
||||
```bash
|
||||
pip install pytest
|
||||
```
|
||||
|
||||
It will detect the files and tests automatically, execute them, and report the results back to you.
|
||||
|
||||
Run the tests with:
|
||||
|
||||
```bash
|
||||
pytest
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.10.2"
|
||||
__version__ = "0.18.0"
|
||||
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Callable, Dict, List, Optional, Type
|
||||
from typing import Any, Callable, Dict, List, Optional, Type, Union
|
||||
|
||||
from fastapi import routing
|
||||
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
||||
@@ -114,6 +114,7 @@ class FastAPI(Starlette):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
@@ -130,6 +131,7 @@ class FastAPI(Starlette):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
methods=methods,
|
||||
operation_id=operation_id,
|
||||
@@ -148,6 +150,7 @@ class FastAPI(Starlette):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
@@ -165,6 +168,7 @@ class FastAPI(Starlette):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
methods=methods,
|
||||
operation_id=operation_id,
|
||||
@@ -177,9 +181,16 @@ class FastAPI(Starlette):
|
||||
return decorator
|
||||
|
||||
def include_router(
|
||||
self, router: routing.APIRouter, *, prefix: str = "", tags: List[str] = None
|
||||
self,
|
||||
router: routing.APIRouter,
|
||||
*,
|
||||
prefix: str = "",
|
||||
tags: List[str] = None,
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
self.router.include_router(router, prefix=prefix, tags=tags)
|
||||
self.router.include_router(
|
||||
router, prefix=prefix, tags=tags, responses=responses or {}
|
||||
)
|
||||
|
||||
def get(
|
||||
self,
|
||||
@@ -191,6 +202,7 @@ class FastAPI(Starlette):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -205,6 +217,7 @@ class FastAPI(Starlette):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
@@ -222,6 +235,7 @@ class FastAPI(Starlette):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -236,6 +250,7 @@ class FastAPI(Starlette):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
@@ -253,6 +268,7 @@ class FastAPI(Starlette):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -267,6 +283,7 @@ class FastAPI(Starlette):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
@@ -284,6 +301,7 @@ class FastAPI(Starlette):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -298,6 +316,7 @@ class FastAPI(Starlette):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
@@ -315,6 +334,7 @@ class FastAPI(Starlette):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -329,6 +349,7 @@ class FastAPI(Starlette):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
@@ -346,6 +367,7 @@ class FastAPI(Starlette):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -360,6 +382,7 @@ class FastAPI(Starlette):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
@@ -377,6 +400,7 @@ class FastAPI(Starlette):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -391,6 +415,7 @@ class FastAPI(Starlette):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
@@ -408,6 +433,7 @@ class FastAPI(Starlette):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -422,6 +448,7 @@ class FastAPI(Starlette):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
|
||||
@@ -27,6 +27,8 @@ class Dependant:
|
||||
call: Callable = None,
|
||||
request_param_name: str = None,
|
||||
background_tasks_param_name: str = None,
|
||||
security_scopes_param_name: str = None,
|
||||
security_scopes: List[str] = None,
|
||||
) -> None:
|
||||
self.path_params = path_params or []
|
||||
self.query_params = query_params or []
|
||||
@@ -37,5 +39,7 @@ class Dependant:
|
||||
self.security_requirements = security_schemes or []
|
||||
self.request_param_name = request_param_name
|
||||
self.background_tasks_param_name = background_tasks_param_name
|
||||
self.security_scopes = security_scopes
|
||||
self.security_scopes_param_name = security_scopes_param_name
|
||||
self.name = name
|
||||
self.call = call
|
||||
|
||||
@@ -20,8 +20,10 @@ from uuid import UUID
|
||||
from fastapi import params
|
||||
from fastapi.dependencies.models import Dependant, SecurityRequirement
|
||||
from fastapi.security.base import SecurityBase
|
||||
from fastapi.utils import UnconstrainedConfig, get_path_param_names
|
||||
from pydantic import Schema, create_model
|
||||
from fastapi.security.oauth2 import OAuth2, SecurityScopes
|
||||
from fastapi.security.open_id_connect_url import OpenIdConnect
|
||||
from fastapi.utils import get_path_param_names
|
||||
from pydantic import BaseConfig, Schema, create_model
|
||||
from pydantic.error_wrappers import ErrorWrapper
|
||||
from pydantic.errors import MissingError
|
||||
from pydantic.fields import Field, Required, Shape
|
||||
@@ -29,8 +31,8 @@ from pydantic.schema import get_annotation_from_schema
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from starlette.background import BackgroundTasks
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.datastructures import UploadFile
|
||||
from starlette.requests import Headers, QueryParams, Request
|
||||
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
|
||||
from starlette.requests import Request
|
||||
|
||||
param_supported_types = (
|
||||
str,
|
||||
@@ -45,19 +47,37 @@ param_supported_types = (
|
||||
Decimal,
|
||||
)
|
||||
|
||||
sequence_shapes = {Shape.LIST, Shape.SET, Shape.TUPLE}
|
||||
sequence_types = (list, set, tuple)
|
||||
sequence_shape_to_type = {Shape.LIST: list, Shape.SET: set, Shape.TUPLE: tuple}
|
||||
|
||||
def get_sub_dependant(*, param: inspect.Parameter, path: str) -> Dependant:
|
||||
|
||||
def get_sub_dependant(
|
||||
*, param: inspect.Parameter, path: str, security_scopes: List[str] = None
|
||||
) -> Dependant:
|
||||
depends: params.Depends = param.default
|
||||
if depends.dependency:
|
||||
dependency = depends.dependency
|
||||
else:
|
||||
dependency = param.annotation
|
||||
sub_dependant = get_dependant(path=path, call=dependency, name=param.name)
|
||||
if isinstance(depends, params.Security) and isinstance(dependency, SecurityBase):
|
||||
security_requirement = None
|
||||
security_scopes = security_scopes or []
|
||||
if isinstance(depends, params.Security):
|
||||
dependency_scopes = depends.scopes
|
||||
security_scopes.extend(dependency_scopes)
|
||||
if isinstance(dependency, SecurityBase):
|
||||
use_scopes = []
|
||||
if isinstance(dependency, (OAuth2, OpenIdConnect)):
|
||||
use_scopes = security_scopes
|
||||
security_requirement = SecurityRequirement(
|
||||
security_scheme=dependency, scopes=depends.scopes
|
||||
security_scheme=dependency, scopes=use_scopes
|
||||
)
|
||||
sub_dependant = get_dependant(
|
||||
path=path, call=dependency, name=param.name, security_scopes=security_scopes
|
||||
)
|
||||
if security_requirement:
|
||||
sub_dependant.security_requirements.append(security_requirement)
|
||||
sub_dependant.security_scopes = security_scopes
|
||||
return sub_dependant
|
||||
|
||||
|
||||
@@ -81,7 +101,9 @@ def get_flat_dependant(dependant: Dependant) -> Dependant:
|
||||
return flat_dependant
|
||||
|
||||
|
||||
def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant:
|
||||
def get_dependant(
|
||||
*, path: str, call: Callable, name: str = None, security_scopes: List[str] = None
|
||||
) -> Dependant:
|
||||
path_param_names = get_path_param_names(path)
|
||||
endpoint_signature = inspect.signature(call)
|
||||
signature_params = endpoint_signature.parameters
|
||||
@@ -89,7 +111,9 @@ def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant:
|
||||
for param_name in signature_params:
|
||||
param = signature_params[param_name]
|
||||
if isinstance(param.default, params.Depends):
|
||||
sub_dependant = get_sub_dependant(param=param, path=path)
|
||||
sub_dependant = get_sub_dependant(
|
||||
param=param, path=path, security_scopes=security_scopes
|
||||
)
|
||||
dependant.dependencies.append(sub_dependant)
|
||||
for param_name in signature_params:
|
||||
param = signature_params[param_name]
|
||||
@@ -100,7 +124,6 @@ def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant:
|
||||
lenient_issubclass(param.annotation, param_supported_types)
|
||||
or param.annotation == param.empty
|
||||
), f"Path params must be of one of the supported types"
|
||||
param = signature_params[param_name]
|
||||
add_param_to_fields(
|
||||
param=param,
|
||||
dependant=dependant,
|
||||
@@ -139,6 +162,8 @@ def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant:
|
||||
dependant.request_param_name = param_name
|
||||
elif lenient_issubclass(param.annotation, BackgroundTasks):
|
||||
dependant.background_tasks_param_name = param_name
|
||||
elif lenient_issubclass(param.annotation, SecurityScopes):
|
||||
dependant.security_scopes_param_name = param_name
|
||||
elif not isinstance(param.default, params.Depends):
|
||||
add_param_to_body_fields(param=param, dependant=dependant)
|
||||
return dependant
|
||||
@@ -178,8 +203,8 @@ def add_param_to_fields(
|
||||
default=None if required else default_value,
|
||||
alias=alias,
|
||||
required=required,
|
||||
model_config=UnconstrainedConfig,
|
||||
class_validators=[],
|
||||
model_config=BaseConfig,
|
||||
class_validators={},
|
||||
schema=schema,
|
||||
)
|
||||
if schema.in_ == params.ParamTypes.path:
|
||||
@@ -212,8 +237,8 @@ def add_param_to_body_fields(*, param: inspect.Parameter, dependant: Dependant)
|
||||
default=None if required else default_value,
|
||||
alias=schema.alias or param.name,
|
||||
required=required,
|
||||
model_config=UnconstrainedConfig,
|
||||
class_validators=[],
|
||||
model_config=BaseConfig,
|
||||
class_validators={},
|
||||
schema=schema,
|
||||
)
|
||||
dependant.body_params.append(field)
|
||||
@@ -283,6 +308,10 @@ async def solve_dependencies(
|
||||
if background_tasks is None:
|
||||
background_tasks = BackgroundTasks()
|
||||
values[dependant.background_tasks_param_name] = background_tasks
|
||||
if dependant.security_scopes_param_name:
|
||||
values[dependant.security_scopes_param_name] = SecurityScopes(
|
||||
scopes=dependant.security_scopes
|
||||
)
|
||||
return values, errors, background_tasks
|
||||
|
||||
|
||||
@@ -293,7 +322,7 @@ def request_params_to_args(
|
||||
values = {}
|
||||
errors = []
|
||||
for field in required_params:
|
||||
if field.shape in {Shape.LIST, Shape.SET, Shape.TUPLE} and isinstance(
|
||||
if field.shape in sequence_shapes and isinstance(
|
||||
received_params, (QueryParams, Headers)
|
||||
):
|
||||
value = received_params.getlist(field.alias)
|
||||
@@ -307,7 +336,7 @@ def request_params_to_args(
|
||||
ErrorWrapper(
|
||||
MissingError(),
|
||||
loc=(schema.in_.value, field.alias),
|
||||
config=UnconstrainedConfig,
|
||||
config=BaseConfig,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -333,17 +362,24 @@ async def request_body_to_args(
|
||||
embed = getattr(field.schema, "embed", None)
|
||||
if len(required_params) == 1 and not embed:
|
||||
received_body = {field.alias: received_body}
|
||||
elif received_body is None:
|
||||
received_body = {}
|
||||
for field in required_params:
|
||||
value = received_body.get(field.alias)
|
||||
if value is None or (isinstance(field.schema, params.Form) and value == ""):
|
||||
if field.shape in sequence_shapes and isinstance(received_body, FormData):
|
||||
value = received_body.getlist(field.alias)
|
||||
else:
|
||||
value = received_body.get(field.alias)
|
||||
if (
|
||||
value is None
|
||||
or (isinstance(field.schema, params.Form) and value == "")
|
||||
or (
|
||||
isinstance(field.schema, params.Form)
|
||||
and field.shape in sequence_shapes
|
||||
and len(value) == 0
|
||||
)
|
||||
):
|
||||
if field.required:
|
||||
errors.append(
|
||||
ErrorWrapper(
|
||||
MissingError(),
|
||||
loc=("body", field.alias),
|
||||
config=UnconstrainedConfig,
|
||||
MissingError(), loc=("body", field.alias), config=BaseConfig
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -355,6 +391,15 @@ async def request_body_to_args(
|
||||
and isinstance(value, UploadFile)
|
||||
):
|
||||
value = await value.read()
|
||||
elif (
|
||||
field.shape in sequence_shapes
|
||||
and isinstance(field.schema, params.File)
|
||||
and lenient_issubclass(field.type_, bytes)
|
||||
and isinstance(value, sequence_types)
|
||||
):
|
||||
awaitables = [sub_value.read() for sub_value in value]
|
||||
contents = await asyncio.gather(*awaitables)
|
||||
value = sequence_shape_to_type[field.shape](contents)
|
||||
v_, errors_ = field.validate(value, values, loc=("body", field.alias))
|
||||
if isinstance(errors_, ErrorWrapper):
|
||||
errors.append(errors_)
|
||||
@@ -366,10 +411,14 @@ async def request_body_to_args(
|
||||
|
||||
|
||||
def get_schema_compatible_field(*, field: Field) -> Field:
|
||||
out_field = field
|
||||
if lenient_issubclass(field.type_, UploadFile):
|
||||
return Field(
|
||||
use_type: type = bytes
|
||||
if field.shape in sequence_shapes:
|
||||
use_type = List[bytes]
|
||||
out_field = Field(
|
||||
name=field.name,
|
||||
type_=bytes,
|
||||
type_=use_type,
|
||||
class_validators=field.class_validators,
|
||||
model_config=field.model_config,
|
||||
default=field.default,
|
||||
@@ -377,10 +426,10 @@ def get_schema_compatible_field(*, field: Field) -> Field:
|
||||
alias=field.alias,
|
||||
schema=field.schema,
|
||||
)
|
||||
return field
|
||||
return out_field
|
||||
|
||||
|
||||
def get_body_field(*, dependant: Dependant, name: str) -> Field:
|
||||
def get_body_field(*, dependant: Dependant, name: str) -> Optional[Field]:
|
||||
flat_dependant = get_flat_dependant(dependant)
|
||||
if not flat_dependant.body_params:
|
||||
return None
|
||||
@@ -405,8 +454,8 @@ def get_body_field(*, dependant: Dependant, name: str) -> Field:
|
||||
type_=BodyModel,
|
||||
default=None,
|
||||
required=required,
|
||||
model_config=UnconstrainedConfig,
|
||||
class_validators=[],
|
||||
model_config=BaseConfig,
|
||||
class_validators={},
|
||||
alias="body",
|
||||
schema=BodySchema(None),
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ def jsonable_encoder(
|
||||
obj: Any,
|
||||
include: Set[str] = None,
|
||||
exclude: Set[str] = set(),
|
||||
by_alias: bool = False,
|
||||
by_alias: bool = True,
|
||||
include_none: bool = True,
|
||||
custom_encoder: dict = {},
|
||||
sqlalchemy_safe: bool = True,
|
||||
|
||||
@@ -31,7 +31,8 @@ def get_swagger_ui_html(*, openapi_url: str, title: str) -> HTMLResponse:
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "BaseLayout"
|
||||
layout: "BaseLayout",
|
||||
deepLinking: true
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -124,7 +124,7 @@ def generate_operation_id(*, route: routing.APIRoute, method: str) -> str:
|
||||
def generate_operation_summary(*, route: routing.APIRoute, method: str) -> str:
|
||||
if route.summary:
|
||||
return route.summary
|
||||
return route.name.replace("_", " ").title() + " " + method.title()
|
||||
return route.name.replace("_", " ").title()
|
||||
|
||||
|
||||
def get_openapi_operation_metadata(*, route: routing.APIRoute, method: str) -> Dict:
|
||||
@@ -178,6 +178,23 @@ def get_openapi_path(
|
||||
definitions[
|
||||
"HTTPValidationError"
|
||||
] = validation_error_response_definition
|
||||
if route.responses:
|
||||
for (additional_status_code, response) in route.responses.items():
|
||||
assert isinstance(
|
||||
response, dict
|
||||
), "An additional response must be a dict"
|
||||
field = route.response_fields.get(additional_status_code)
|
||||
if field:
|
||||
response_schema, _ = field_schema(
|
||||
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)
|
||||
response.setdefault("content", {}).setdefault(
|
||||
"application/json", {}
|
||||
)["schema"] = response_schema
|
||||
response.setdefault("description", "Additional Response")
|
||||
operation.setdefault("responses", {})[
|
||||
str(additional_status_code)
|
||||
] = response
|
||||
status_code = str(route.status_code)
|
||||
response_schema = {"type": "string"}
|
||||
if lenient_issubclass(route.content_type, JSONResponse):
|
||||
@@ -189,13 +206,14 @@ def get_openapi_path(
|
||||
)
|
||||
else:
|
||||
response_schema = {}
|
||||
content = {route.content_type.media_type: {"schema": response_schema}}
|
||||
operation["responses"] = {
|
||||
status_code: {
|
||||
"description": route.response_description,
|
||||
"content": content,
|
||||
}
|
||||
}
|
||||
operation.setdefault("responses", {}).setdefault(status_code, {})[
|
||||
"description"
|
||||
] = route.response_description
|
||||
operation.setdefault("responses", {}).setdefault(
|
||||
status_code, {}
|
||||
).setdefault("content", {}).setdefault(route.content_type.media_type, {})[
|
||||
"schema"
|
||||
] = response_schema
|
||||
if all_route_params or route.body_field:
|
||||
operation["responses"][str(HTTP_422_UNPROCESSABLE_ENTITY)] = {
|
||||
"description": "Validation Error",
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import logging
|
||||
from typing import Any, Callable, List, Optional, Type
|
||||
from typing import Any, Callable, Dict, List, Optional, Type, Union
|
||||
|
||||
from fastapi import params
|
||||
from fastapi.dependencies.models import Dependant
|
||||
from fastapi.dependencies.utils import get_body_field, get_dependant, solve_dependencies
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.utils import UnconstrainedConfig
|
||||
from pydantic import BaseModel, Schema
|
||||
from pydantic import BaseConfig, BaseModel, Schema
|
||||
from pydantic.error_wrappers import ErrorWrapper, ValidationError
|
||||
from pydantic.fields import Field
|
||||
from pydantic.utils import lenient_issubclass
|
||||
@@ -53,18 +52,13 @@ def get_app(
|
||||
body = None
|
||||
if body_field:
|
||||
if is_body_form:
|
||||
raw_body = await request.form()
|
||||
form_fields = {}
|
||||
for field, value in raw_body.items():
|
||||
form_fields[field] = value
|
||||
if form_fields:
|
||||
body = form_fields
|
||||
body = await request.form()
|
||||
else:
|
||||
body_bytes = await request.body()
|
||||
if body_bytes:
|
||||
body = await request.json()
|
||||
except Exception as e:
|
||||
logging.error("Error getting request body", e)
|
||||
logging.error(f"Error getting request body: {e}")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="There was an error parsing the body"
|
||||
)
|
||||
@@ -110,6 +104,7 @@ class APIRoute(routing.Route):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
name: str = None,
|
||||
methods: List[str] = None,
|
||||
@@ -130,10 +125,10 @@ class APIRoute(routing.Route):
|
||||
self.response_field: Optional[Field] = Field(
|
||||
name=response_name,
|
||||
type_=self.response_model,
|
||||
class_validators=[],
|
||||
class_validators={},
|
||||
default=None,
|
||||
required=False,
|
||||
model_config=UnconstrainedConfig,
|
||||
model_config=BaseConfig,
|
||||
schema=Schema(None),
|
||||
)
|
||||
else:
|
||||
@@ -141,8 +136,32 @@ class APIRoute(routing.Route):
|
||||
self.status_code = status_code
|
||||
self.tags = tags or []
|
||||
self.summary = summary
|
||||
self.description = description or self.endpoint.__doc__
|
||||
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
|
||||
self.response_description = response_description
|
||||
self.responses = responses or {}
|
||||
response_fields = {}
|
||||
for additional_status_code, response in self.responses.items():
|
||||
assert isinstance(response, dict), "An additional response must be a dict"
|
||||
model = response.get("model")
|
||||
if model:
|
||||
assert lenient_issubclass(
|
||||
model, BaseModel
|
||||
), "A response model must be a Pydantic model"
|
||||
response_name = f"Response_{additional_status_code}_{self.name}"
|
||||
response_field = Field(
|
||||
name=response_name,
|
||||
type_=model,
|
||||
class_validators=None,
|
||||
default=None,
|
||||
required=False,
|
||||
model_config=BaseConfig,
|
||||
schema=Schema(None),
|
||||
)
|
||||
response_fields[additional_status_code] = response_field
|
||||
if response_fields:
|
||||
self.response_fields: Dict[Union[int, str], Field] = response_fields
|
||||
else:
|
||||
self.response_fields = {}
|
||||
self.deprecated = deprecated
|
||||
if methods is None:
|
||||
methods = ["GET"]
|
||||
@@ -180,6 +199,7 @@ class APIRouter(routing.Router):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
@@ -196,6 +216,7 @@ class APIRouter(routing.Router):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
methods=methods,
|
||||
operation_id=operation_id,
|
||||
@@ -215,6 +236,7 @@ class APIRouter(routing.Router):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
@@ -232,6 +254,7 @@ class APIRouter(routing.Router):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
methods=methods,
|
||||
operation_id=operation_id,
|
||||
@@ -244,15 +267,23 @@ class APIRouter(routing.Router):
|
||||
return decorator
|
||||
|
||||
def include_router(
|
||||
self, router: "APIRouter", *, prefix: str = "", tags: List[str] = None
|
||||
self,
|
||||
router: "APIRouter",
|
||||
*,
|
||||
prefix: str = "",
|
||||
tags: List[str] = None,
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
if prefix:
|
||||
assert prefix.startswith("/"), "A path prefix must start with '/'"
|
||||
assert not prefix.endswith(
|
||||
"/"
|
||||
), "A path prefix must not end with '/', as the routes will start with '/'"
|
||||
if responses is None:
|
||||
responses = {}
|
||||
for route in router.routes:
|
||||
if isinstance(route, APIRoute):
|
||||
combined_responses = {**responses, **route.responses}
|
||||
self.add_api_route(
|
||||
prefix + route.path,
|
||||
route.endpoint,
|
||||
@@ -262,6 +293,7 @@ class APIRouter(routing.Router):
|
||||
summary=route.summary,
|
||||
description=route.description,
|
||||
response_description=route.response_description,
|
||||
responses=combined_responses,
|
||||
deprecated=route.deprecated,
|
||||
methods=route.methods,
|
||||
operation_id=route.operation_id,
|
||||
@@ -292,6 +324,7 @@ class APIRouter(routing.Router):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -306,6 +339,7 @@ class APIRouter(routing.Router):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
methods=["GET"],
|
||||
operation_id=operation_id,
|
||||
@@ -324,6 +358,7 @@ class APIRouter(routing.Router):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -338,6 +373,7 @@ class APIRouter(routing.Router):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
methods=["PUT"],
|
||||
operation_id=operation_id,
|
||||
@@ -356,6 +392,7 @@ class APIRouter(routing.Router):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -370,6 +407,7 @@ class APIRouter(routing.Router):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
methods=["POST"],
|
||||
operation_id=operation_id,
|
||||
@@ -388,6 +426,7 @@ class APIRouter(routing.Router):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -402,6 +441,7 @@ class APIRouter(routing.Router):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
methods=["DELETE"],
|
||||
operation_id=operation_id,
|
||||
@@ -420,6 +460,7 @@ class APIRouter(routing.Router):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -434,6 +475,7 @@ class APIRouter(routing.Router):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
methods=["OPTIONS"],
|
||||
operation_id=operation_id,
|
||||
@@ -452,6 +494,7 @@ class APIRouter(routing.Router):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -466,6 +509,7 @@ class APIRouter(routing.Router):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
methods=["HEAD"],
|
||||
operation_id=operation_id,
|
||||
@@ -484,6 +528,7 @@ class APIRouter(routing.Router):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -498,6 +543,7 @@ class APIRouter(routing.Router):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
methods=["PATCH"],
|
||||
operation_id=operation_id,
|
||||
@@ -516,6 +562,7 @@ class APIRouter(routing.Router):
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
@@ -530,6 +577,7 @@ class APIRouter(routing.Router):
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
responses=responses or {},
|
||||
deprecated=deprecated,
|
||||
methods=["TRACE"],
|
||||
operation_id=operation_id,
|
||||
|
||||
@@ -6,5 +6,10 @@ from .http import (
|
||||
HTTPBearer,
|
||||
HTTPDigest,
|
||||
)
|
||||
from .oauth2 import OAuth2, OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from .oauth2 import (
|
||||
OAuth2,
|
||||
OAuth2PasswordBearer,
|
||||
OAuth2PasswordRequestForm,
|
||||
SecurityScopes,
|
||||
)
|
||||
from .open_id_connect_url import OpenIdConnect
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.openapi.models import APIKey, APIKeyIn
|
||||
from fastapi.security.base import SecurityBase
|
||||
from starlette.exceptions import HTTPException
|
||||
@@ -10,42 +12,54 @@ class APIKeyBase(SecurityBase):
|
||||
|
||||
|
||||
class APIKeyQuery(APIKeyBase):
|
||||
def __init__(self, *, name: str, scheme_name: str = None):
|
||||
self.model = APIKey(**{"in": APIKeyIn.query}, name=name)
|
||||
def __init__(self, *, name: str, scheme_name: str = None, auto_error: bool = True):
|
||||
self.model: APIKey = APIKey(**{"in": APIKeyIn.query}, name=name)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
api_key: str = request.query_params.get(self.model.name)
|
||||
if not api_key:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return api_key
|
||||
|
||||
|
||||
class APIKeyHeader(APIKeyBase):
|
||||
def __init__(self, *, name: str, scheme_name: str = None):
|
||||
self.model = APIKey(**{"in": APIKeyIn.header}, name=name)
|
||||
def __init__(self, *, name: str, scheme_name: str = None, auto_error: bool = True):
|
||||
self.model: APIKey = APIKey(**{"in": APIKeyIn.header}, name=name)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
api_key: str = request.headers.get(self.model.name)
|
||||
if not api_key:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return api_key
|
||||
|
||||
|
||||
class APIKeyCookie(APIKeyBase):
|
||||
def __init__(self, *, name: str, scheme_name: str = None):
|
||||
self.model = APIKey(**{"in": APIKeyIn.cookie}, name=name)
|
||||
def __init__(self, *, name: str, scheme_name: str = None, auto_error: bool = True):
|
||||
self.model: APIKey = APIKey(**{"in": APIKeyIn.cookie}, name=name)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
api_key: str = request.cookies.get(self.model.name)
|
||||
if not api_key:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return api_key
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import binascii
|
||||
from base64 import b64decode
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.openapi.models import (
|
||||
HTTPBase as HTTPBaseModel,
|
||||
HTTPBearer as HTTPBearerModel,
|
||||
@@ -8,9 +10,8 @@ from fastapi.openapi.models import (
|
||||
from fastapi.security.base import SecurityBase
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from pydantic import BaseModel
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
class HTTPBasicCredentials(BaseModel):
|
||||
@@ -24,38 +25,58 @@ class HTTPAuthorizationCredentials(BaseModel):
|
||||
|
||||
|
||||
class HTTPBase(SecurityBase):
|
||||
def __init__(self, *, scheme: str, scheme_name: str = None):
|
||||
def __init__(
|
||||
self, *, scheme: str, scheme_name: str = None, auto_error: bool = True
|
||||
):
|
||||
self.model = HTTPBaseModel(scheme=scheme)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
async def __call__(
|
||||
self, request: Request
|
||||
) -> Optional[HTTPAuthorizationCredentials]:
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||
if not (authorization and scheme and credentials):
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
|
||||
|
||||
class HTTPBasic(HTTPBase):
|
||||
def __init__(self, *, scheme_name: str = None, realm: str = None):
|
||||
def __init__(
|
||||
self, *, scheme_name: str = None, realm: str = None, auto_error: bool = True
|
||||
):
|
||||
self.model = HTTPBaseModel(scheme="basic")
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.realm = realm
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
async def __call__(self, request: Request) -> Optional[HTTPBasicCredentials]:
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
# before implementing headers with 401 errors, wait for: https://github.com/encode/starlette/issues/295
|
||||
# unauthorized_headers = {"WWW-Authenticate": "Basic"}
|
||||
if self.realm:
|
||||
unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
|
||||
else:
|
||||
unauthorized_headers = {"WWW-Authenticate": "Basic"}
|
||||
invalid_user_credentials_exc = HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Invalid authentication credentials"
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials",
|
||||
headers=unauthorized_headers,
|
||||
)
|
||||
if not authorization or scheme.lower() != "basic":
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers=unauthorized_headers,
|
||||
)
|
||||
else:
|
||||
return None
|
||||
try:
|
||||
data = b64decode(param).decode("ascii")
|
||||
except (ValueError, UnicodeDecodeError, binascii.Error):
|
||||
@@ -67,17 +88,29 @@ class HTTPBasic(HTTPBase):
|
||||
|
||||
|
||||
class HTTPBearer(HTTPBase):
|
||||
def __init__(self, *, bearerFormat: str = None, scheme_name: str = None):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
bearerFormat: str = None,
|
||||
scheme_name: str = None,
|
||||
auto_error: bool = True,
|
||||
):
|
||||
self.model = HTTPBearerModel(bearerFormat=bearerFormat)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
async def __call__(
|
||||
self, request: Request
|
||||
) -> Optional[HTTPAuthorizationCredentials]:
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||
if not (authorization and scheme and credentials):
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
if scheme.lower() != "bearer":
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
@@ -87,17 +120,23 @@ class HTTPBearer(HTTPBase):
|
||||
|
||||
|
||||
class HTTPDigest(HTTPBase):
|
||||
def __init__(self, *, scheme_name: str = None):
|
||||
def __init__(self, *, scheme_name: str = None, auto_error: bool = True):
|
||||
self.model = HTTPBaseModel(scheme="digest")
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
async def __call__(
|
||||
self, request: Request
|
||||
) -> Optional[HTTPAuthorizationCredentials]:
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||
if not (authorization and scheme and credentials):
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
if scheme.lower() != "digest":
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi.openapi.models import OAuth2 as OAuth2Model, OAuthFlows as OAuthFlowsModel
|
||||
from fastapi.params import Form
|
||||
@@ -113,32 +113,54 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm):
|
||||
|
||||
class OAuth2(SecurityBase):
|
||||
def __init__(
|
||||
self, *, flows: OAuthFlowsModel = OAuthFlowsModel(), scheme_name: str = None
|
||||
self,
|
||||
*,
|
||||
flows: OAuthFlowsModel = OAuthFlowsModel(),
|
||||
scheme_name: str = None,
|
||||
auto_error: bool = True
|
||||
):
|
||||
self.model = OAuth2Model(flows=flows)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
if not authorization:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return authorization
|
||||
|
||||
|
||||
class OAuth2PasswordBearer(OAuth2):
|
||||
def __init__(self, tokenUrl: str, scheme_name: str = None, scopes: dict = None):
|
||||
def __init__(
|
||||
self,
|
||||
tokenUrl: str,
|
||||
scheme_name: str = None,
|
||||
scopes: dict = None,
|
||||
auto_error: bool = True,
|
||||
):
|
||||
if not scopes:
|
||||
scopes = {}
|
||||
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
|
||||
super().__init__(flows=flows, scheme_name=scheme_name)
|
||||
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "bearer":
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return param
|
||||
|
||||
|
||||
class SecurityScopes:
|
||||
def __init__(self, scopes: List[str] = None):
|
||||
self.scopes = scopes or []
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel
|
||||
from fastapi.security.base import SecurityBase
|
||||
from starlette.exceptions import HTTPException
|
||||
@@ -6,14 +8,20 @@ from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
class OpenIdConnect(SecurityBase):
|
||||
def __init__(self, *, openIdConnectUrl: str, scheme_name: str = None):
|
||||
def __init__(
|
||||
self, *, openIdConnectUrl: str, scheme_name: str = None, auto_error: bool = True
|
||||
):
|
||||
self.model = OpenIdConnectModel(openIdConnectUrl=openIdConnectUrl)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
if not authorization:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return authorization
|
||||
|
||||
@@ -3,17 +3,12 @@ from typing import Any, Dict, List, Sequence, Set, Type
|
||||
|
||||
from fastapi import routing
|
||||
from fastapi.openapi.constants import REF_PREFIX
|
||||
from pydantic import BaseConfig, BaseModel
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import Field
|
||||
from pydantic.schema import get_flat_models_from_fields, model_process_schema
|
||||
from starlette.routing import BaseRoute
|
||||
|
||||
|
||||
class UnconstrainedConfig(BaseConfig):
|
||||
min_anystr_length = None
|
||||
max_anystr_length = None
|
||||
|
||||
|
||||
def get_flat_models_from_routes(
|
||||
routes: Sequence[Type[BaseRoute]]
|
||||
) -> Set[Type[BaseModel]]:
|
||||
@@ -30,6 +25,8 @@ def get_flat_models_from_routes(
|
||||
body_fields_from_routes.append(route.body_field)
|
||||
if route.response_field:
|
||||
responses_from_routes.append(route.response_field)
|
||||
if route.response_fields:
|
||||
responses_from_routes.extend(route.response_fields.values())
|
||||
flat_models = get_flat_models_from_fields(
|
||||
body_fields_from_routes + responses_from_routes
|
||||
)
|
||||
|
||||
@@ -43,7 +43,9 @@ nav:
|
||||
- Handling Errors: 'tutorial/handling-errors.md'
|
||||
- Path Operation Configuration: 'tutorial/path-operation-configuration.md'
|
||||
- Path Operation Advanced Configuration: 'tutorial/path-operation-advanced-configuration.md'
|
||||
- Additional Status Codes: 'tutorial/additional-status-codes.md'
|
||||
- Custom Response: 'tutorial/custom-response.md'
|
||||
- Additional Responses in OpenAPI: 'tutorial/additional-responses.md'
|
||||
- Dependencies:
|
||||
- First Steps: 'tutorial/dependencies/first-steps.md'
|
||||
- Classes as Dependencies: 'tutorial/dependencies/classes-as-dependencies.md'
|
||||
@@ -55,6 +57,10 @@ 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'
|
||||
- OAuth2 scopes: 'tutorial/security/oauth2-scopes.md'
|
||||
- HTTP Basic Auth: 'tutorial/security/http-basic-auth.md'
|
||||
- Middleware: 'tutorial/middleware.md'
|
||||
- CORS (Cross-Origin Resource Sharing): 'tutorial/cors.md'
|
||||
- Using the Request Directly: 'tutorial/using-request-directly.md'
|
||||
- SQL (Relational) Databases: 'tutorial/sql-databases.md'
|
||||
- Async SQL (Relational) Databases: 'tutorial/async-sql-databases.md'
|
||||
@@ -66,7 +72,9 @@ nav:
|
||||
- GraphQL: 'tutorial/graphql.md'
|
||||
- WebSockets: 'tutorial/websockets.md'
|
||||
- 'Events: startup - shutdown': 'tutorial/events.md'
|
||||
- Testing: 'tutorial/testing.md'
|
||||
- Debugging: 'tutorial/debugging.md'
|
||||
- Extending OpenAPI: 'tutorial/extending-openapi.md'
|
||||
- Concurrency and async / await: 'async.md'
|
||||
- Deployment: 'deployment.md'
|
||||
- Project Generation - Template: 'project-generation.md'
|
||||
|
||||
@@ -20,7 +20,7 @@ classifiers = [
|
||||
]
|
||||
requires = [
|
||||
"starlette ==0.11.1",
|
||||
"pydantic >=0.17,<=0.21.0"
|
||||
"pydantic >=0.17,<=0.23.0"
|
||||
]
|
||||
description-file = "README.md"
|
||||
requires-python = ">=3.6"
|
||||
@@ -58,7 +58,6 @@ all = [
|
||||
"pyyaml",
|
||||
"graphene",
|
||||
"ujson",
|
||||
"ujson",
|
||||
"email_validator",
|
||||
"uvicorn",
|
||||
]
|
||||
|
||||
7
scripts/deploy.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
bash scripts/publish.sh
|
||||
|
||||
bash scripts/trigger-docker.sh
|
||||
5
scripts/publish.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
flit publish
|
||||
@@ -3,4 +3,4 @@
|
||||
set -e
|
||||
set -x
|
||||
|
||||
bash scripts/test.sh --cov-report=html
|
||||
bash scripts/test.sh --cov-report=html ${@}
|
||||
|
||||
@@ -41,7 +41,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Foo Post",
|
||||
"summary": "Foo",
|
||||
"operationId": "foo_foo_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
|
||||
52
tests/test_additional_response_extra.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
sub_router = APIRouter()
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@sub_router.get("/")
|
||||
def read_item():
|
||||
return {"id": "foo"}
|
||||
|
||||
|
||||
router.include_router(sub_router, prefix="/items")
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Item",
|
||||
"operationId": "read_item_items__get",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_path_operation():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"id": "foo"}
|
||||
95
tests/test_additional_responses_router.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/a", responses={501: {"description": "Error 1"}})
|
||||
async def a():
|
||||
return "a"
|
||||
|
||||
|
||||
@router.get("/b", responses={502: {"description": "Error 2"}})
|
||||
async def b():
|
||||
return "b"
|
||||
|
||||
|
||||
@router.get("/c", responses={501: {"description": "Error 3"}})
|
||||
async def c():
|
||||
return "c"
|
||||
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/a": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"501": {"description": "Error 1"},
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
},
|
||||
"summary": "A",
|
||||
"operationId": "a_a_get",
|
||||
}
|
||||
},
|
||||
"/b": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"502": {"description": "Error 2"},
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
},
|
||||
"summary": "B",
|
||||
"operationId": "b_b_get",
|
||||
}
|
||||
},
|
||||
"/c": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"501": {"description": "Error 3"},
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
},
|
||||
"summary": "C",
|
||||
"operationId": "c_c_get",
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_a():
|
||||
response = client.get("/a")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "a"
|
||||
|
||||
|
||||
def test_b():
|
||||
response = client.get("/b")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "b"
|
||||
|
||||
|
||||
def test_c():
|
||||
response = client.get("/c")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == "c"
|
||||
@@ -17,7 +17,7 @@ openapi_schema = {
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Non Operation Get",
|
||||
"summary": "Non Operation",
|
||||
"operationId": "non_operation_api_route_get",
|
||||
}
|
||||
},
|
||||
@@ -29,7 +29,7 @@ openapi_schema = {
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Non Decorated Route Get",
|
||||
"summary": "Non Decorated Route",
|
||||
"operationId": "non_decorated_route_non_decorated_route_get",
|
||||
}
|
||||
},
|
||||
@@ -41,7 +41,7 @@ openapi_schema = {
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Get Text Get",
|
||||
"summary": "Get Text",
|
||||
"operationId": "get_text_text_get",
|
||||
}
|
||||
},
|
||||
@@ -63,7 +63,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Id Get",
|
||||
"summary": "Get Id",
|
||||
"operationId": "get_id_path__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -93,7 +93,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Str Id Get",
|
||||
"summary": "Get Str Id",
|
||||
"operationId": "get_str_id_path_str__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -123,7 +123,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Int Id Get",
|
||||
"summary": "Get Int Id",
|
||||
"operationId": "get_int_id_path_int__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -153,7 +153,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Float Id Get",
|
||||
"summary": "Get Float Id",
|
||||
"operationId": "get_float_id_path_float__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -183,7 +183,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Bool Id Get",
|
||||
"summary": "Get Bool Id",
|
||||
"operationId": "get_bool_id_path_bool__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -213,7 +213,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Id Get",
|
||||
"summary": "Get Path Param Id",
|
||||
"operationId": "get_path_param_id_path_param__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -243,7 +243,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Required Id Get",
|
||||
"summary": "Get Path Param Required Id",
|
||||
"operationId": "get_path_param_required_id_path_param-required__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -273,7 +273,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Min Length Get",
|
||||
"summary": "Get Path Param Min Length",
|
||||
"operationId": "get_path_param_min_length_path_param-minlength__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -307,7 +307,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Max Length Get",
|
||||
"summary": "Get Path Param Max Length",
|
||||
"operationId": "get_path_param_max_length_path_param-maxlength__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -341,7 +341,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Min Max Length Get",
|
||||
"summary": "Get Path Param Min Max Length",
|
||||
"operationId": "get_path_param_min_max_length_path_param-min_maxlength__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -376,7 +376,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Gt Get",
|
||||
"summary": "Get Path Param Gt",
|
||||
"operationId": "get_path_param_gt_path_param-gt__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -410,7 +410,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Gt0 Get",
|
||||
"summary": "Get Path Param Gt0",
|
||||
"operationId": "get_path_param_gt0_path_param-gt0__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -444,7 +444,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Ge Get",
|
||||
"summary": "Get Path Param Ge",
|
||||
"operationId": "get_path_param_ge_path_param-ge__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -478,7 +478,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Lt Get",
|
||||
"summary": "Get Path Param Lt",
|
||||
"operationId": "get_path_param_lt_path_param-lt__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -512,7 +512,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Lt0 Get",
|
||||
"summary": "Get Path Param Lt0",
|
||||
"operationId": "get_path_param_lt0_path_param-lt0__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -546,7 +546,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Le Get",
|
||||
"summary": "Get Path Param Le",
|
||||
"operationId": "get_path_param_le_path_param-le__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -580,7 +580,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Lt Gt Get",
|
||||
"summary": "Get Path Param Lt Gt",
|
||||
"operationId": "get_path_param_lt_gt_path_param-lt-gt__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -615,7 +615,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Le Ge Get",
|
||||
"summary": "Get Path Param Le Ge",
|
||||
"operationId": "get_path_param_le_ge_path_param-le-ge__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -650,7 +650,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Lt Int Get",
|
||||
"summary": "Get Path Param Lt Int",
|
||||
"operationId": "get_path_param_lt_int_path_param-lt-int__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -684,7 +684,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Gt Int Get",
|
||||
"summary": "Get Path Param Gt Int",
|
||||
"operationId": "get_path_param_gt_int_path_param-gt-int__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -718,7 +718,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Le Int Get",
|
||||
"summary": "Get Path Param Le Int",
|
||||
"operationId": "get_path_param_le_int_path_param-le-int__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -752,7 +752,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Ge Int Get",
|
||||
"summary": "Get Path Param Ge Int",
|
||||
"operationId": "get_path_param_ge_int_path_param-ge-int__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -786,7 +786,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Lt Gt Int Get",
|
||||
"summary": "Get Path Param Lt Gt Int",
|
||||
"operationId": "get_path_param_lt_gt_int_path_param-lt-gt-int__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -821,7 +821,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Path Param Le Ge Int Get",
|
||||
"summary": "Get Path Param Le Ge Int",
|
||||
"operationId": "get_path_param_le_ge_int_path_param-le-ge-int__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -856,7 +856,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Query Get",
|
||||
"summary": "Get Query",
|
||||
"operationId": "get_query_query_get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -886,7 +886,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Query Optional Get",
|
||||
"summary": "Get Query Optional",
|
||||
"operationId": "get_query_optional_query_optional_get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -916,7 +916,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Query Type Get",
|
||||
"summary": "Get Query Type",
|
||||
"operationId": "get_query_type_query_int_get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -946,7 +946,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Query Type Optional Get",
|
||||
"summary": "Get Query Type Optional",
|
||||
"operationId": "get_query_type_optional_query_int_optional_get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -976,7 +976,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Query Type Optional Get",
|
||||
"summary": "Get Query Type Optional",
|
||||
"operationId": "get_query_type_optional_query_int_default_get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1006,7 +1006,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Query Param Get",
|
||||
"summary": "Get Query Param",
|
||||
"operationId": "get_query_param_query_param_get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1036,7 +1036,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Query Param Required Get",
|
||||
"summary": "Get Query Param Required",
|
||||
"operationId": "get_query_param_required_query_param-required_get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1066,7 +1066,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Query Param Required Type Get",
|
||||
"summary": "Get Query Param Required Type",
|
||||
"operationId": "get_query_param_required_type_query_param-required_int_get",
|
||||
"parameters": [
|
||||
{
|
||||
|
||||
@@ -72,7 +72,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Items Get",
|
||||
"summary": "Get Items",
|
||||
"operationId": "get_items_items__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -100,7 +100,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Delete Item Delete",
|
||||
"summary": "Delete Item",
|
||||
"operationId": "delete_item_items__item_id__delete",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -136,7 +136,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Options Item Options",
|
||||
"summary": "Options Item",
|
||||
"operationId": "options_item_items__item_id__options",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -164,7 +164,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Head Item Head",
|
||||
"summary": "Head Item",
|
||||
"operationId": "head_item_items__item_id__head",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -192,7 +192,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Patch Item Patch",
|
||||
"summary": "Patch Item",
|
||||
"operationId": "patch_item_items__item_id__patch",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -228,7 +228,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Trace Item Trace",
|
||||
"summary": "Trace Item",
|
||||
"operationId": "trace_item_items__item_id__trace",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -258,7 +258,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Get Not Decorated Get",
|
||||
"summary": "Get Not Decorated",
|
||||
"operationId": "get_not_decorated_items-not-decorated__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ from enum import Enum
|
||||
|
||||
import pytest
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Schema, ValidationError
|
||||
|
||||
|
||||
class Person:
|
||||
@@ -59,6 +59,10 @@ class ModelWithConfig(BaseModel):
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class ModelWithAlias(BaseModel):
|
||||
foo: str = Schema(..., alias="Foo")
|
||||
|
||||
|
||||
def test_encode_class():
|
||||
person = Person(name="Foo")
|
||||
pet = Pet(owner=person, name="Firulais")
|
||||
@@ -85,3 +89,13 @@ def test_encode_custom_json_encoders_model():
|
||||
def test_encode_model_with_config():
|
||||
model = ModelWithConfig(role=RoleEnum.admin)
|
||||
assert jsonable_encoder(model) == {"role": "admin"}
|
||||
|
||||
|
||||
def test_encode_model_with_alias_raises():
|
||||
with pytest.raises(ValidationError):
|
||||
model = ModelWithAlias(foo="Bar")
|
||||
|
||||
|
||||
def test_encode_model_with_alias():
|
||||
model = ModelWithAlias(Foo="Bar")
|
||||
assert jsonable_encoder(model) == {"Foo": "Bar"}
|
||||
|
||||
@@ -42,7 +42,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Save Item No Body Post",
|
||||
"summary": "Save Item No Body",
|
||||
"operationId": "save_item_no_body_items__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
|
||||
@@ -36,7 +36,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read Items Get",
|
||||
"summary": "Read Items",
|
||||
"operationId": "read_items_items__get",
|
||||
"parameters": [
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Save Item No Body Put",
|
||||
"summary": "Save Item No Body",
|
||||
"operationId": "save_item_no_body_items__item_id__put",
|
||||
"parameters": [
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@ openapi_schema = {
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"summary": "Read Current User",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"APIKeyCookie": []}],
|
||||
}
|
||||
|
||||
75
tests/test_security_api_key_cookie_optional.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import APIKeyCookie
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
api_key = APIKeyCookie(name="key", auto_error=False)
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: Optional[str] = Security(api_key)):
|
||||
if oauth_header is None:
|
||||
return None
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(current_user: User = Depends(get_current_user)):
|
||||
if current_user is None:
|
||||
return {"msg": "Create an account first"}
|
||||
else:
|
||||
return current_user
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"APIKeyCookie": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_api_key():
|
||||
response = client.get("/users/me", cookies={"key": "secret"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "secret"}
|
||||
|
||||
|
||||
def test_security_api_key_no_key():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"msg": "Create an account first"}
|
||||
@@ -36,7 +36,7 @@ openapi_schema = {
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"summary": "Read Current User",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"APIKeyHeader": []}],
|
||||
}
|
||||
|
||||
74
tests/test_security_api_key_header_optional.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import APIKeyHeader
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
api_key = APIKeyHeader(name="key", auto_error=False)
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: Optional[str] = Security(api_key)):
|
||||
if oauth_header is None:
|
||||
return None
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(current_user: Optional[User] = Depends(get_current_user)):
|
||||
if current_user is None:
|
||||
return {"msg": "Create an account first"}
|
||||
return current_user
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"APIKeyHeader": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"APIKeyHeader": {"type": "apiKey", "name": "key", "in": "header"}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_api_key():
|
||||
response = client.get("/users/me", headers={"key": "secret"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "secret"}
|
||||
|
||||
|
||||
def test_security_api_key_no_key():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"msg": "Create an account first"}
|
||||
@@ -36,7 +36,7 @@ openapi_schema = {
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"summary": "Read Current User",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"APIKeyQuery": []}],
|
||||
}
|
||||
|
||||
74
tests/test_security_api_key_query_optional.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import APIKeyQuery
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
api_key = APIKeyQuery(name="key", auto_error=False)
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: Optional[str] = Security(api_key)):
|
||||
if oauth_header is None:
|
||||
return None
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(current_user: Optional[User] = Depends(get_current_user)):
|
||||
if current_user is None:
|
||||
return {"msg": "Create an account first"}
|
||||
return current_user
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"APIKeyQuery": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_api_key():
|
||||
response = client.get("/users/me?key=secret")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "secret"}
|
||||
|
||||
|
||||
def test_security_api_key_no_key():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"msg": "Create an account first"}
|
||||
@@ -26,7 +26,7 @@ openapi_schema = {
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"summary": "Read Current User",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"HTTPBase": []}],
|
||||
}
|
||||
|
||||