mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-24 14:48:35 -05:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92c825be6a | ||
|
|
32438c85f6 | ||
|
|
02e53fde90 | ||
|
|
902cdaf010 | ||
|
|
04d77bb1c4 | ||
|
|
6d9fc08a7e | ||
|
|
5c9c088a2a | ||
|
|
014c7df142 | ||
|
|
9259dc228a | ||
|
|
de431d948d | ||
|
|
3d2c0993c1 | ||
|
|
37bc3614fd | ||
|
|
188da34529 | ||
|
|
d692c28f52 | ||
|
|
8568862a19 | ||
|
|
dfa067b061 | ||
|
|
0d1b97fb94 | ||
|
|
df1e754380 | ||
|
|
e5b341c7dd | ||
|
|
577c5a84db | ||
|
|
a5cfee434d | ||
|
|
9a8349bf96 | ||
|
|
a59408f68c | ||
|
|
3c08b05ea6 | ||
|
|
60599bad99 | ||
|
|
ccf30b5c2e | ||
|
|
ca0652aebf | ||
|
|
be957e7c99 | ||
|
|
90af868146 | ||
|
|
660f917d79 | ||
|
|
5278314f2f | ||
|
|
4a0316bcfe | ||
|
|
0393a093d3 | ||
|
|
27f530a7ff | ||
|
|
c3e5e65093 | ||
|
|
804ec460fc | ||
|
|
0125ea4f83 | ||
|
|
216770118a | ||
|
|
a935d66b10 | ||
|
|
dd2541bc97 |
9
Pipfile
9
Pipfile
@@ -5,10 +5,7 @@ verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
mypy = "*"
|
||||
jedi = "*"
|
||||
black = "*"
|
||||
prospector = "*"
|
||||
rope = "*"
|
||||
jupyter = "*"
|
||||
better-exceptions = "*"
|
||||
pytest = "*"
|
||||
@@ -22,10 +19,12 @@ markdown-include = "*"
|
||||
autoflake = "*"
|
||||
email-validator = "*"
|
||||
ujson = "*"
|
||||
flake8 = "*"
|
||||
python-multipart = "*"
|
||||
|
||||
[packages]
|
||||
starlette = "*"
|
||||
pydantic = "*"
|
||||
starlette = "==0.10.1"
|
||||
pydantic = "==0.18.2"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
|
||||
313
Pipfile.lock
generated
313
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "a0f966a95cb84845ca4aad02c44fc0e7c5e2047fc44dcf19a95a4abaa02d0197"
|
||||
"sha256": "20483e725e92e679c4c21ea3ff0043d759c74102b181f16b67908f979f854d5c"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -26,18 +26,18 @@
|
||||
},
|
||||
"pydantic": {
|
||||
"hashes": [
|
||||
"sha256:51f879ca4b1d114c9f892737a0d65233251fb00fcd2b6da2be0d277b8ba7d28d",
|
||||
"sha256:c90c9e5ae2a6a3f59efdcb1505ddfb18be6dc5648b536bf33782269460954cc2"
|
||||
"sha256:9f023811b6cefd203c5fd8fd15a4152f04e79e531b8f676ab1244dfe06ce8024",
|
||||
"sha256:edbb08b561feda505374c0f25e4b54466a0a0c702ed6b2efaabdc3890d1a82e7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.16.1"
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"starlette": {
|
||||
"hashes": [
|
||||
"sha256:01f04283b49a8cb0c8921baa90dbafe47e953f0a265f6ebb38176038e4bd9bf8"
|
||||
"sha256:7cc05c33d00db3b2ddfd7516a737544ed0a34c9dd0ced94076f29b581ce4f532"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.9.9"
|
||||
"version": "==0.10.1"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
@@ -48,13 +48,6 @@
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be",
|
||||
"sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d"
|
||||
],
|
||||
"version": "==2.0.4"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
|
||||
@@ -85,10 +78,11 @@
|
||||
},
|
||||
"better-exceptions": {
|
||||
"hashes": [
|
||||
"sha256:0a73efef96b48f867ea980227ac3b00d36a92754e6d316ad2ee472f136014580"
|
||||
"sha256:bf79c87659bc849989d726bf0e4a2100edefe7eded112d201f54fe08467fdf63",
|
||||
"sha256:c196cad849de615abb9f6eb67ca1b83f33b938818f0e2fe8fa157b22aeb7b992"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.2.1"
|
||||
"version": "==0.2.2"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
@@ -100,10 +94,10 @@
|
||||
},
|
||||
"bleach": {
|
||||
"hashes": [
|
||||
"sha256:48d39675b80a75f6d1c3bdbffec791cf0bbbab665cf01e20da701c77de278718",
|
||||
"sha256:73d26f018af5d5adcdabf5c1c974add4361a9c76af215fe32fdec8a6fc5fb9b9"
|
||||
"sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16",
|
||||
"sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa"
|
||||
],
|
||||
"version": "==3.0.2"
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
@@ -162,10 +156,10 @@
|
||||
},
|
||||
"decorator": {
|
||||
"hashes": [
|
||||
"sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82",
|
||||
"sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c"
|
||||
"sha256:33cd704aea07b4c28b3eb2c97d288a06918275dac0ecebdaf1bc8a48d98adb9e",
|
||||
"sha256:cabb249f4710888a2fc0e13e9a16c343d932033718ff62e1e9bc93a9d3a9122b"
|
||||
],
|
||||
"version": "==4.3.0"
|
||||
"version": "==4.3.2"
|
||||
},
|
||||
"defusedxml": {
|
||||
"hashes": [
|
||||
@@ -189,12 +183,6 @@
|
||||
],
|
||||
"version": "==0.14"
|
||||
},
|
||||
"dodgy": {
|
||||
"hashes": [
|
||||
"sha256:65e13cf878d7aff129f1461c13cb5fd1bb6dfe66bb5327e09379c3877763280c"
|
||||
],
|
||||
"version": "==0.1.9"
|
||||
},
|
||||
"email-validator": {
|
||||
"hashes": [
|
||||
"sha256:ddc4b5b59fa699bb10127adcf7ad4de78fde4ec539a072b104b8bb16da666ae5"
|
||||
@@ -204,10 +192,18 @@
|
||||
},
|
||||
"entrypoints": {
|
||||
"hashes": [
|
||||
"sha256:10ad569bb245e7e2ba425285b9fa3e8178a0dc92fc53b1e1c553805e15a8825b",
|
||||
"sha256:d2d587dde06f99545fb13a383d2cd336a8ff1f359c5839ce3a64c917d10c029f"
|
||||
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
|
||||
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
|
||||
],
|
||||
"version": "==0.2.3"
|
||||
"version": "==0.3"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:09b9bb539920776da542e67a570a5df96ff933c9a08b62cfae920bcc789e4383",
|
||||
"sha256:e0f8cd519cfc0072c0ee31add5def09d2b3ef6040b34dc426445c3af9b02163c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.4"
|
||||
},
|
||||
"flit": {
|
||||
"hashes": [
|
||||
@@ -267,7 +263,6 @@
|
||||
"sha256:571702b5bd167911fe9036e5039ba67f820d6502832285cde8c881ab2b2149fd",
|
||||
"sha256:c8481b5e59d34a5c7c42e98f6625e633f6ef59353abea6437472c7ec2093f191"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.13.2"
|
||||
},
|
||||
"jinja2": {
|
||||
@@ -279,10 +274,10 @@
|
||||
},
|
||||
"jsonschema": {
|
||||
"hashes": [
|
||||
"sha256:3ae8afd6f4ca6417f14bf43ef61341311598f14234cdb4174fe43d42b236a3c8",
|
||||
"sha256:dfd8426040892c8d0ef6da574085f282569f189cb24b70091a66c21c12d6705e"
|
||||
"sha256:683fe7ed58763ea0be572de5aad47cd3cc1297640916f9a8ccd222b287da7d2f",
|
||||
"sha256:b42d7a292addb57370e6260bcbadb77e00a899fe6ec998c453f45893c41c658b"
|
||||
],
|
||||
"version": "==3.0.0a3"
|
||||
"version": "==3.0.0b3"
|
||||
},
|
||||
"jupyter": {
|
||||
"hashes": [
|
||||
@@ -314,40 +309,6 @@
|
||||
],
|
||||
"version": "==4.4.0"
|
||||
},
|
||||
"lazy-object-proxy": {
|
||||
"hashes": [
|
||||
"sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33",
|
||||
"sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39",
|
||||
"sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019",
|
||||
"sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088",
|
||||
"sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b",
|
||||
"sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e",
|
||||
"sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6",
|
||||
"sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b",
|
||||
"sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5",
|
||||
"sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff",
|
||||
"sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd",
|
||||
"sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7",
|
||||
"sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff",
|
||||
"sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d",
|
||||
"sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2",
|
||||
"sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35",
|
||||
"sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4",
|
||||
"sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514",
|
||||
"sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252",
|
||||
"sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109",
|
||||
"sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f",
|
||||
"sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c",
|
||||
"sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92",
|
||||
"sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577",
|
||||
"sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d",
|
||||
"sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d",
|
||||
"sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f",
|
||||
"sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a",
|
||||
"sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b"
|
||||
],
|
||||
"version": "==1.3.1"
|
||||
},
|
||||
"livereload": {
|
||||
"hashes": [
|
||||
"sha256:29cadfabcedd12eed792e0131991235b9d4764d4474bed75cf525f57109ec0a2",
|
||||
@@ -426,27 +387,27 @@
|
||||
},
|
||||
"mkdocs-material": {
|
||||
"hashes": [
|
||||
"sha256:037712dd7e2128a9b596943bcd92ebc9ad28800906dcee447e2fc008dd9dbbff",
|
||||
"sha256:52522c8553a6d6da8fca2afe43297e8f88acdcf8ccf752a118148f1328f761e2"
|
||||
"sha256:4b4af83c704d2bab41be3a5228e800a5e1157003368fbf548d95073ce19e0f61",
|
||||
"sha256:86c0042c803586985bf79c99962ebd4644c3f0ff095d5df541f09fa48f5b62cc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1.0"
|
||||
"version": "==3.3.0"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
|
||||
"sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
|
||||
"sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
|
||||
"sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4",
|
||||
"sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc",
|
||||
"sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
|
||||
],
|
||||
"version": "==4.3.0"
|
||||
"version": "==5.0.0"
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:12d965c9c4e8a625673aec493162cf390e66de12ef176b1f4821ac00d55f3ab3",
|
||||
"sha256:38d5b5f835a81817dcc0af8d155bce4e9aefa03794fe32ed154d6612e83feafa"
|
||||
"sha256:986a7f97808a865405c5fd98fae5ebfa963c31520a56c783df159e9a81e41b3e",
|
||||
"sha256:cc5df73cc11d35655a8c364f45d07b13c8db82c000def4bd7721be13356533b4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.650"
|
||||
"version": "==0.660"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
@@ -484,17 +445,10 @@
|
||||
},
|
||||
"parso": {
|
||||
"hashes": [
|
||||
"sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2",
|
||||
"sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24"
|
||||
"sha256:4b8f9ed80c3a4a3191aa3261505d868aa552dd25649cb13a7d73b6b7315edf2d",
|
||||
"sha256:5a120be2e8863993b597f1c0437efca799e90e0793c98ae5d4e34ebd00140e31"
|
||||
],
|
||||
"version": "==0.3.1"
|
||||
},
|
||||
"pep8-naming": {
|
||||
"hashes": [
|
||||
"sha256:1b419fa45b68b61cd8c5daf4e0c96d28915ad14d3d5f35fcc1e7e95324a33a2e",
|
||||
"sha256:4eedfd4c4b05e48796f74f5d8628c068ff788b9c2b08471ad408007fc6450e5a"
|
||||
],
|
||||
"version": "==0.4.1"
|
||||
"version": "==0.3.2"
|
||||
},
|
||||
"pexpect": {
|
||||
"hashes": [
|
||||
@@ -513,10 +467,10 @@
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095",
|
||||
"sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f"
|
||||
"sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616",
|
||||
"sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"
|
||||
],
|
||||
"version": "==0.8.0"
|
||||
"version": "==0.8.1"
|
||||
},
|
||||
"prometheus-client": {
|
||||
"hashes": [
|
||||
@@ -526,18 +480,11 @@
|
||||
},
|
||||
"prompt-toolkit": {
|
||||
"hashes": [
|
||||
"sha256:c1d6aff5252ab2ef391c2fe498ed8c088066f66bc64a8d5c095bbf795d9fec34",
|
||||
"sha256:d4c47f79b635a0e70b84fdb97ebd9a274203706b1ee5ed44c10da62755cf3ec9",
|
||||
"sha256:fd17048d8335c1e6d5ee403c3569953ba3eb8555d710bfc548faf0712666ea39"
|
||||
"sha256:88002cc618cacfda8760c4539e76c3b3f148ecdb7035a3d422c7ecdc90c2a3ba",
|
||||
"sha256:c6655a12e9b08edb8cf5aeab4815fd1e1bdea4ad73d3bbf269cf2e0c4eb75d5e",
|
||||
"sha256:df5835fb8f417aa55e5cafadbaeb0cf630a1e824aad16989f9f0493e679ec010"
|
||||
],
|
||||
"version": "==2.0.7"
|
||||
},
|
||||
"prospector": {
|
||||
"hashes": [
|
||||
"sha256:877d8d361a5c0e04c8587718c22c5d671afcf814945c96b3e592836d772943fd"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.1.6.2"
|
||||
"version": "==2.0.8"
|
||||
},
|
||||
"ptyprocess": {
|
||||
"hashes": [
|
||||
@@ -556,25 +503,17 @@
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83",
|
||||
"sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a"
|
||||
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
|
||||
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
|
||||
],
|
||||
"version": "==2.4.0"
|
||||
},
|
||||
"pydocstyle": {
|
||||
"hashes": [
|
||||
"sha256:2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8",
|
||||
"sha256:5741c85e408f9e0ddf873611085e819b809fca90b619f5fd7f34bd4959da3dd4",
|
||||
"sha256:ed79d4ec5e92655eccc21eb0c6cf512e69512b4a97d215ace46d17e4990f2039"
|
||||
],
|
||||
"version": "==3.0.0"
|
||||
"version": "==2.5.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
|
||||
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
|
||||
"sha256:5e8c00e30c464c99e0b501dc160b13a14af7f27d4dffb529c556e30a159e231d",
|
||||
"sha256:f277f9ca3e55de669fba45b7393a1449009cff5a37d1af10ebb76c52765269cd"
|
||||
],
|
||||
"version": "==1.6.0"
|
||||
"version": "==2.1.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
@@ -583,38 +522,6 @@
|
||||
],
|
||||
"version": "==2.3.1"
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec",
|
||||
"sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb"
|
||||
],
|
||||
"version": "==2.1.1"
|
||||
},
|
||||
"pylint-celery": {
|
||||
"hashes": [
|
||||
"sha256:41e32094e7408d15c044178ea828dd524beedbdbe6f83f712c5e35bde1de4beb"
|
||||
],
|
||||
"version": "==0.3"
|
||||
},
|
||||
"pylint-django": {
|
||||
"hashes": [
|
||||
"sha256:5dc5f85caef2c5f9e61622b9cbd89d94edd3dcf546939b2974d18de4fa90d676",
|
||||
"sha256:bf313f10b68ed915a34f0f475cc9ff8c7f574a95302beb48b79c5993f7efd84c"
|
||||
],
|
||||
"version": "==2.0.2"
|
||||
},
|
||||
"pylint-flask": {
|
||||
"hashes": [
|
||||
"sha256:8fcdbb7cbf13d8c2ac1f2230b2aa1c1b83bb3ca2bd8b76f95561cb8757a305ec"
|
||||
],
|
||||
"version": "==0.5"
|
||||
},
|
||||
"pylint-plugin-utils": {
|
||||
"hashes": [
|
||||
"sha256:8ad25a82bcce390d1d6b7c006c123e0cb18051839c9df7b8bdb7823c53fe676e"
|
||||
],
|
||||
"version": "==0.4"
|
||||
},
|
||||
"pymdown-extensions": {
|
||||
"hashes": [
|
||||
"sha256:25b0a7967fa697b5035e23340a48594e3e93acb10b06d74574218ace3347d1df",
|
||||
@@ -624,25 +531,25 @@
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
"sha256:59880cc33ac293515892b2969aa8f4ed2cec592cbd0be4c4e20f2410468bbc62"
|
||||
"sha256:5a3827d57ad3e46820e5ee4ed5b9e0ee7bc4686df6634a7368bc1863a5c48a77"
|
||||
],
|
||||
"version": "==0.14.8"
|
||||
"version": "==0.14.9"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:f689bf2fc18c4585403348dd56f47d87780bf217c53ed9ae7a3e2d7faa45f8e9",
|
||||
"sha256:f812ea39a0153566be53d88f8de94839db1e8a05352ed8a49525d7d7f37861e9"
|
||||
"sha256:65aeaa77ae87c7fc95de56285282546cfa9c886dc8e5dc78313db1c25e21bc07",
|
||||
"sha256:6ac6d467d9f053e95aaacd79f831dbecfe730f419c6c7022cb316b365cd9199d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.2"
|
||||
"version": "==4.2.0"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
"sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7",
|
||||
"sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762"
|
||||
"sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33",
|
||||
"sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.6.0"
|
||||
"version": "==2.6.1"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
@@ -651,6 +558,13 @@
|
||||
],
|
||||
"version": "==2.7.5"
|
||||
},
|
||||
"python-multipart": {
|
||||
"hashes": [
|
||||
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.0.5"
|
||||
},
|
||||
"pytoml": {
|
||||
"hashes": [
|
||||
"sha256:ca2d0cb127c938b8b76a9a0d0f855cf930c1d50cc3a0af6d3595b566519a1013"
|
||||
@@ -712,19 +626,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.21.0"
|
||||
},
|
||||
"requirements-detector": {
|
||||
"hashes": [
|
||||
"sha256:9fbc4b24e8b7c3663aff32e3eba34596848c6b91bd425079b386973bd8d08931"
|
||||
],
|
||||
"version": "==0.6"
|
||||
},
|
||||
"rope": {
|
||||
"hashes": [
|
||||
"sha256:a108c445e1cd897fe19272ab7877d172e7faf3d4148c80e7d20faba42ea8f7b2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.11.0"
|
||||
},
|
||||
"send2trash": {
|
||||
"hashes": [
|
||||
"sha256:60001cc07d707fe247c94f74ca6ac0d3255aabcb930529690897ca2a39db28b2",
|
||||
@@ -732,12 +633,6 @@
|
||||
],
|
||||
"version": "==1.5.0"
|
||||
},
|
||||
"setoptconf": {
|
||||
"hashes": [
|
||||
"sha256:5b0b5d8e0077713f5d5152d4f63be6f048d9a1bb66be15d089a11c898c3cf49c"
|
||||
],
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||
@@ -745,13 +640,6 @@
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
"hashes": [
|
||||
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
|
||||
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
|
||||
],
|
||||
"version": "==1.2.1"
|
||||
},
|
||||
"terminado": {
|
||||
"hashes": [
|
||||
"sha256:55abf9ade563b8f9be1f34e4233c7b7bde726059947a593322e8a553cc4c067a",
|
||||
@@ -775,15 +663,9 @@
|
||||
},
|
||||
"tornado": {
|
||||
"hashes": [
|
||||
"sha256:0662d28b1ca9f67108c7e3b77afabfb9c7e87bde174fbda78186ecedc2499a9d",
|
||||
"sha256:4e5158d97583502a7e2739951553cbd88a72076f152b4b11b64b9a10c4c49409",
|
||||
"sha256:732e836008c708de2e89a31cb2fa6c0e5a70cb60492bee6f1ea1047500feaf7f",
|
||||
"sha256:8154ec22c450df4e06b35f131adc4f2f3a12ec85981a203301d310abf580500f",
|
||||
"sha256:8e9d728c4579682e837c92fdd98036bd5cdefa1da2aaf6acf26947e6dd0c01c5",
|
||||
"sha256:d4b3e5329f572f055b587efc57d29bd051589fb5a43ec8898c77a47ec2fa2bbb",
|
||||
"sha256:e5f2585afccbff22390cddac29849df463b252b711aa2ce7c5f3f342a5b3b444"
|
||||
"sha256:00ebd485a52bd7eaa3f35bdf8ab43c109aaa2edc722849b6905c1ffd8c958e82"
|
||||
],
|
||||
"version": "==5.1.1"
|
||||
"version": "==6.0a1"
|
||||
},
|
||||
"traitlets": {
|
||||
"hashes": [
|
||||
@@ -794,30 +676,29 @@
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:0555eca1671ebe09eb5f2176723826f6f44cca5060502fea259de9b0e893ab53",
|
||||
"sha256:0ca96128ea66163aea13911c9b4b661cb345eb729a20be15c034271360fc7474",
|
||||
"sha256:16ccd06d614cf81b96de42a37679af12526ea25a208bce3da2d9226f44563868",
|
||||
"sha256:1e21ae7b49a3f744958ffad1737dfbdb43e1137503ccc59f4e32c4ac33b0bd1c",
|
||||
"sha256:37670c6fd857b5eb68aa5d193e14098354783b5138de482afa401cc2644f5a7f",
|
||||
"sha256:46d84c8e3806619ece595aaf4f37743083f9454c9ea68a517f1daa05126daf1d",
|
||||
"sha256:5b972bbb3819ece283a67358103cc6671da3646397b06e7acea558444daf54b2",
|
||||
"sha256:6306ffa64922a7b58ee2e8d6f207813460ca5a90213b4a400c2e730375049246",
|
||||
"sha256:6cb25dc95078931ecbd6cbcc4178d1b8ae8f2b513ae9c3bd0b7f81c2191db4c6",
|
||||
"sha256:7e19d439fee23620dea6468d85bfe529b873dace39b7e5b0c82c7099681f8a22",
|
||||
"sha256:7f5cd83af6b3ca9757e1127d852f497d11c7b09b4716c355acfbebf783d028da",
|
||||
"sha256:81e885a713e06faeef37223a5b1167615db87f947ecc73f815b9d1bbd6b585be",
|
||||
"sha256:94af325c9fe354019a29f9016277c547ad5d8a2d98a02806f27a7436b2da6735",
|
||||
"sha256:b1e5445c6075f509d5764b84ce641a1535748801253b97f3b7ea9d948a22853a",
|
||||
"sha256:cb061a959fec9a514d243831c514b51ccb940b58a5ce572a4e209810f2507dcf",
|
||||
"sha256:cc8d0b703d573cbabe0d51c9d68ab68df42a81409e4ed6af45a04a95484b96a5",
|
||||
"sha256:da0afa955865920edb146926455ec49da20965389982f91e926389666f5cf86a",
|
||||
"sha256:dc76738331d61818ce0b90647aedde17bbba3d3f9e969d83c1d9087b4f978862",
|
||||
"sha256:e7ec9a1445d27dbd0446568035f7106fa899a36f55e52ade28020f7b3845180d",
|
||||
"sha256:f741ba03feb480061ab91a465d1a3ed2d40b52822ada5b4017770dfcb88f839f",
|
||||
"sha256:fe800a58547dd424cd286b7270b967b5b3316b993d86453ede184a17b5a6b17d"
|
||||
"sha256:023625bfa9359e29bd6e24cac2a4503495b49761d48a5f1e38333fc4ac4d93fe",
|
||||
"sha256:07591f7a5fdff50e2e566c4c1e9df545c75d21e27d98d18cb405727ed0ef329c",
|
||||
"sha256:153e526b0f4ffbfada72d0bb5ffe8574ba02803d2f3a9c605c8cf99dfedd72a2",
|
||||
"sha256:3ad2bdcd46a4a1518d7376e9f5016d17718a9ed3c6a3f09203d832f6c165de4a",
|
||||
"sha256:3ea98c84df53ada97ee1c5159bb3bc784bd734231235a1ede14c8ae0775049f7",
|
||||
"sha256:51a7141ccd076fa561af107cfb7a8b6d06a008d92451a1ac7e73149d18e9a827",
|
||||
"sha256:52c93cd10e6c24e7ac97e8615da9f224fd75c61770515cb323316c30830ddb33",
|
||||
"sha256:6344c84baeda3d7b33e157f0b292e4dd53d05ddb57a63f738178c01cac4635c9",
|
||||
"sha256:64699ca1b3bd5070bdeb043e6d43bc1d0cebe08008548f4a6bee782b0ecce032",
|
||||
"sha256:74903f2e56bbffe29282ef8a5487d207d10be0f8513b41aff787d954a4cf91c9",
|
||||
"sha256:7891710dba83c29ee2bd51ecaa82f60f6bede40271af781110c08be134207bf2",
|
||||
"sha256:91976c56224e26c256a0de0f76d2004ab885a29423737684b4f7ebdd2f46dde2",
|
||||
"sha256:9bad678a576ecc71f25eba9f1e3fd8d01c28c12a2834850b458428b3e855f062",
|
||||
"sha256:b4726339a4c180a8b6ad9d8b50d2b6dc247e1b79b38fe2290549c98e82e4fd15",
|
||||
"sha256:ba36f6aa3f8933edf94ea35826daf92cbb3ec248b89eccdc053d4a815d285357",
|
||||
"sha256:bbc96bde544fd19e9ef168e4dfa5c3dfe704bfa78128fa76f361d64d6b0f731a",
|
||||
"sha256:c0c927f1e44469056f7f2dada266c79b577da378bbde3f6d2ada726d131e4824",
|
||||
"sha256:c0f9a3708008aa59f560fa1bd22385e05b79b8e38e0721a15a8402b089243442",
|
||||
"sha256:f0bf6f36ff9c5643004171f11d2fdc745aa3953c5aacf2536a0685db9ceb3fb1",
|
||||
"sha256:f5be39a0146be663cbf210a4d95c3c58b2d7df7b043c9047c5448e358f0550a2",
|
||||
"sha256:fcd198bf19d9213e5cbf2cde2b9ef20a9856e716f76f9476157f90ae6de06cc6"
|
||||
],
|
||||
"markers": "python_version < '3.7' and implementation_name == 'cpython'",
|
||||
"version": "==1.1.1"
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"ujson": {
|
||||
"hashes": [
|
||||
@@ -853,12 +734,6 @@
|
||||
"sha256:fa618be8435447a017fd1bf2c7ae922d0428056cfc7449f7a8641edf76b48265"
|
||||
],
|
||||
"version": "==3.4.2"
|
||||
},
|
||||
"wrapt": {
|
||||
"hashes": [
|
||||
"sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6"
|
||||
],
|
||||
"version": "==1.10.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
README.md
23
README.md
@@ -24,11 +24,11 @@
|
||||
|
||||
---
|
||||
|
||||
FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+.
|
||||
FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
|
||||
|
||||
The key features are:
|
||||
|
||||
* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic).
|
||||
* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance).
|
||||
|
||||
* **Fast to code**: Increase the speed to develop features by about 200% to 300% *.
|
||||
* **Less bugs**: Reduce about 40% of human (developer) induced errors. *
|
||||
@@ -36,7 +36,7 @@ The key features are:
|
||||
* **Easy**: Designed to be easy to use and learn. Less time reading docs.
|
||||
* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Less bugs.
|
||||
* **Robust**: Get production-ready code. With automatic interactive documentation.
|
||||
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" target="_blank">OpenAPI</a> and <a href="http://json-schema.org/" target="_blank">JSON Schema</a>.
|
||||
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" target="_blank">OpenAPI</a> (previously known as Swagger) and <a href="http://json-schema.org/" target="_blank">JSON Schema</a>.
|
||||
|
||||
<small>* estimation based on tests on an internal development team, building production applications.</small>
|
||||
|
||||
@@ -166,7 +166,7 @@ You will see the alternative automatic documentation (provided by <a href="https
|
||||
|
||||
## Example upgrade
|
||||
|
||||
Now modify the file `main.py` to recive a body from a `PUT` request.
|
||||
Now modify the file `main.py` to receive a body from a `PUT` request.
|
||||
|
||||
Declare the body using standard Python types, thanks to Pydantic.
|
||||
|
||||
@@ -257,7 +257,7 @@ item: Item
|
||||
* Validation of data:
|
||||
* Automatic and clear errors when the data is invalid.
|
||||
* Validation even for deeply nested JSON objects.
|
||||
* <abbr title="also known as: serialization, parsing, marshalling">Conversion</abbr> of input data: coming from the network, to Python data and types. Reading from:
|
||||
* <abbr title="also known as: serialization, parsing, marshalling">Conversion</abbr> of input data: coming from the network to Python data and types. Reading from:
|
||||
* JSON.
|
||||
* Path parameters.
|
||||
* Query parameters.
|
||||
@@ -291,8 +291,8 @@ Coming back to the previous code example, **FastAPI** will:
|
||||
* Check that it has an optional attribute `is_offer`, that should be a `bool`, if present.
|
||||
* All this would also work for deeply nested JSON objects.
|
||||
* Convert from and to JSON automatically.
|
||||
* Document everything as an OpenAPI schema, that can be used by:
|
||||
* Interactive documentation sytems.
|
||||
* Document everything with OpenAPI, that can be used by:
|
||||
* Interactive documentation systems.
|
||||
* Automatic client code generation systems, for many languages.
|
||||
* Provide 2 interactive documentation web interfaces directly.
|
||||
|
||||
@@ -329,7 +329,7 @@ For a more complete example including more features, see the <a href="https://fa
|
||||
**Spoiler alert**: the tutorial - user guide includes:
|
||||
|
||||
* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**.
|
||||
* How to set **validation constrains** as `maximum_length` or `regex`.
|
||||
* How to set **validation constraints** as `maximum_length` or `regex`.
|
||||
* A very powerful and easy to use **<abbr title="also known as components, resources, providers, services, injectables">Dependency Injection</abbr>** system.
|
||||
* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth.
|
||||
* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic).
|
||||
@@ -342,6 +342,11 @@ For a more complete example including more features, see the <a href="https://fa
|
||||
* ...and more.
|
||||
|
||||
|
||||
## Performance
|
||||
|
||||
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=a979de55-980d-4721-a46f-77298b3f3923&hw=ph&test=fortune&l=zijzen-7" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
|
||||
|
||||
To understand more about it, see the section <a href="https://fastapi.tiangolo.com/benchmarks/" target="_blank">Benchmarks</a>.
|
||||
|
||||
## Optional Dependencies
|
||||
|
||||
@@ -366,7 +371,7 @@ Used by FastAPI / Starlette:
|
||||
|
||||
* <a href="http://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - for the server that loads and serves your application.
|
||||
|
||||
You can install all of these with `pip3 install fastapi[full]`.
|
||||
You can install all of these with `pip3 install fastapi[all]`.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
374
docs/alternatives.md
Normal file
374
docs/alternatives.md
Normal file
@@ -0,0 +1,374 @@
|
||||
What inspired **FastAPI**, how it compares to other alternatives and what it learned from them.
|
||||
|
||||
## Intro
|
||||
|
||||
**FastAPI** wouldn't exist if not for the previous work of others.
|
||||
|
||||
There have been many tools created before that have helped inspire its creation.
|
||||
|
||||
I have been avoiding the creation of a new framework for several years. First I tried to solve all the features covered by **FastAPI** using many different frameworks, plug-ins, and tools.
|
||||
|
||||
But at some point, there was no other option than creating something that provided all these features, taking the best ideas from previous tools, and combining them in the best way possible, using language features that weren't even available before (Python 3.6+ type hints).
|
||||
|
||||
## Previous tools
|
||||
|
||||
### <a href="https://www.djangoproject.com/" target="_blank">Django</a>
|
||||
|
||||
It's the most popular Python framework and is widely trusted. It is used to build systems like Instagram.
|
||||
|
||||
It's relatively tightly coupled with relational databases (like MySQL or PostgreSQL), so, having a NoSQL database (like Couchbase, MongoDB, Cassandra, etc) as the main store engine is not very easy.
|
||||
|
||||
It was created to generate the HTML in the backend, not to create APIs used by a modern frontend (like React, Vue.js and Angular) or by other systems (like <abbr title="Internet of Things">IoT</abbr> devices) communicating with it.
|
||||
|
||||
### <a href="https://www.django-rest-framework.org/" target="_blank">Django REST Framework</a>
|
||||
|
||||
Django REST framework was created to be a flexible toolkit for building Web APIs using Django underneath, to improve its API capabilities.
|
||||
|
||||
It is used by many companies including Mozilla, Red Hat and Eventbrite.
|
||||
|
||||
It was one of the first examples of **automatic API documentation**, and this was specifically one of the first ideas that inspired "the search for" **FastAPI**.
|
||||
|
||||
!!! note
|
||||
Django REST Framework was created by Tom Christie. The same creator of Starlette and Uvicorn, on which **FastAPI** is based.
|
||||
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Have an automatic API documentation web user interface.
|
||||
|
||||
### <a href="http://flask.pocoo.org/" target="_blank">Flask</a>
|
||||
|
||||
Flask is a "microframework", it doesn't include database integrations nor many of the things that come by default in Django.
|
||||
|
||||
This simplicity and flexibility allow doing things like using NoSQL databases as the main data storage system.
|
||||
|
||||
As it is very simple, it's relatively intuitive to learn, although the documentation gets somewhat technical at some points.
|
||||
|
||||
It is also commonly used for other applications that don't necessarily need a database, user management, or any of the many features that come pre-built in Django. Although many of these features can be added with plug-ins.
|
||||
|
||||
This decoupling of parts, and being a "microframework" that could be extended to cover exactly what is needed was a key feature that I wanted to keep.
|
||||
|
||||
Given the simplicity of Flask, it seemed like a good match for building APIs. The next thing to find was a "Django REST Framework" for Flask.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Be a micro-framework. Making it easy to mix and match the tools and parts needed.
|
||||
|
||||
Have a simple and easy to use routing system.
|
||||
|
||||
|
||||
### <a href="http://docs.python-requests.org" target="_blank">Requests</a>
|
||||
|
||||
**FastAPI** is not actually an alternative to **Requests**. Their scope is very different.
|
||||
|
||||
It would actually be common to use Requests *inside* of a FastAPI application.
|
||||
|
||||
But still, FastAPI got quite some inspiration from Requests.
|
||||
|
||||
**Requests** is a library to *interact* with APIs (as a client), while **FastAPI** is a library to *build* APIs (as a server).
|
||||
|
||||
They are, more or less, at opposite ends, complementing each other.
|
||||
|
||||
Requests has a very simple and intuitive design, it's very easy to use, with sensible defaults. But at the same time, it's very powerful and customizable.
|
||||
|
||||
That's why, as said in the official website:
|
||||
|
||||
> Requests is one of the most downloaded Python packages of all time
|
||||
|
||||
The way you use it is very simple. For example, to do a `GET` request, you would write:
|
||||
|
||||
```Python
|
||||
response = requests.get("http://example.com/some/url")
|
||||
```
|
||||
|
||||
The FastAPI counterpart API path operation could look like:
|
||||
|
||||
```Python hl_lines="1"
|
||||
@app.get("/some/url")
|
||||
def read_url():
|
||||
return {"message": "Hello World"}
|
||||
```
|
||||
|
||||
See the similarities in `requests.get(...)` and `@app.get(...)`.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
* Have a simple and intuitive API.
|
||||
* Use HTTP method names (operations) directly, in a straightforward and intuitive way.
|
||||
* Have sensible defaults, but powerful customizations.
|
||||
|
||||
|
||||
### <a href="https://swagger.io/" target="_blank">Swagger</a> / <a href="https://github.com/OAI/OpenAPI-Specification/" target="_blank">OpenAPI</a>
|
||||
|
||||
The main feature I wanted from Django REST Framework was the automatic API documentation.
|
||||
|
||||
Then I found that there was a standard to document APIs, using JSON (or YAML, an extension of JSON) called Swagger.
|
||||
|
||||
And there was a web user interface for Swagger APIs already created. So, being able to generate Swagger documentation for an API would allow using this web user interface automatically.
|
||||
|
||||
At some point, Swagger was given to the Linux Foundation, to be renamed OpenAPI.
|
||||
|
||||
That's why when talking about version 2.0 it's common to say "Swagger", and for version 3+ "OpenAPI".
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Adopt and use an open standard for API specifications, instead of a custom schema.
|
||||
|
||||
And integrate standards-based user interface tools:
|
||||
|
||||
* <a href="https://github.com/swagger-api/swagger-ui" target="_blank">Swagger UI</a>
|
||||
* <a href="https://github.com/Rebilly/ReDoc" target="_blank">ReDoc</a>
|
||||
|
||||
These two were chosen for being fairly popular and stable, but doing a quick search, you could find dozens of additional alternative user interfaces for OpenAPI (that you can use with **FastAPI**).
|
||||
|
||||
### Flask REST frameworks
|
||||
|
||||
There are several Flask REST frameworks, but after investing the time and work into investigating them, I found that many are discontinued or abandoned, with several standing issues that made them unfit.
|
||||
|
||||
### <a href="https://marshmallow.readthedocs.io/en/3.0/" target="_blank">Marshmallow</a>
|
||||
|
||||
One of the main features needed by API systems is data "<abbr title="also called marshalling, convertion">serialization</abbr>" which is taking data from the code (Python) and converting it into something that can be sent through the network. For example, converting an object containing data from a database into a JSON object. Converting `datetime` objects into strings, etc.
|
||||
|
||||
Another big feature needed by APIs is data validation, making sure that the data is valid, given certain parameters. For example, that some field is an `int`, and not some random string. This is especially useful for incoming data.
|
||||
|
||||
Without a data validation system, you would have to do all the checks by hand, in code.
|
||||
|
||||
These features are what Marshmallow was built to provide. It is a great library, and I have used it a lot before.
|
||||
|
||||
But it was created before there existed Python type hints. So, to define every <abbr title="the definition of how data should be formed">schema</abbr> you need to use specific utils and classes provided by Marshmallow.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Use code to define "schemas" that provide data types and validation, automatically.
|
||||
|
||||
### <a href="https://webargs.readthedocs.io/en/latest/" target="_blank">Webargs</a>
|
||||
|
||||
Another big feature required by APIs is <abbr title="reading and converting to Python data">parsing</abbr> data from incoming requests.
|
||||
|
||||
Webargs is a tool that was made to provide that on top of several frameworks, including Flask.
|
||||
|
||||
It uses Marshmallow underneath to do the data validation. And it was created by the same guys.
|
||||
|
||||
It's a great tool and I have used it a lot too, before having **FastAPI**.
|
||||
|
||||
!!! info
|
||||
Webargs was created by the same Marshmallow guys.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Have automatic validation of incoming request data.
|
||||
|
||||
### <a href="https://apispec.readthedocs.io/en/stable/" target="_blank">APISpec</a>
|
||||
|
||||
Marshmallow and Webargs provide validation, parsing and serialization as plug-ins.
|
||||
|
||||
But documentation is still missing. Then APISpec was created.
|
||||
|
||||
It is a plug-in for many frameworks (and there's a plug-in for Starlette too).
|
||||
|
||||
The way it works is that you write the definition of the schema using YAML format inside the docstring of each function handling a route.
|
||||
|
||||
And it generates OpenAPI schemas.
|
||||
|
||||
That's how it works in Flask, Starlette, Responder, etc.
|
||||
|
||||
But then, we have again the problem of having a micro-syntax, inside of a Python string (a big YAML).
|
||||
|
||||
The editor can't help much with that. And if we modify parameters or Marshmallow schemas and forget to also modify that YAML docstring, the generated schema would be obsolete.
|
||||
|
||||
!!! info
|
||||
APISpec was created by the same Marshmallow guys.
|
||||
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Support the open standard for APIs, OpenAPI.
|
||||
|
||||
### <a href="https://flask-apispec.readthedocs.io/en/latest/" target="_blank">Flask-apispec</a>
|
||||
|
||||
It's a Flask plug-in, that ties together Webargs, Marshmallow and APISpec.
|
||||
|
||||
It uses the information from Webargs and Marshmallow to automatically generate OpenAPI schemas, using APISpec.
|
||||
|
||||
It's a great tool, very under-rated. It should be way more popular than many Flask plug-ins out there. It might be due to its documentation being too concise and abstract.
|
||||
|
||||
This solved having to write YAML (another syntax) inside of Python docstrings.
|
||||
|
||||
This combination of Flask, Flask-apispec with Marshmallow and Webargs was my favorite backend stack until building **FastAPI**.
|
||||
|
||||
Using it led to the creation of several Flask full-stack generators. These are the main stack I (and several external teams) have been using up to now:
|
||||
|
||||
* <a href="https://github.com/tiangolo/full-stack" target="_blank">https://github.com/tiangolo/full-stack</a>
|
||||
* <a href="https://github.com/tiangolo/full-stack-flask-couchbase" target="_blank">https://github.com/tiangolo/full-stack-flask-couchbase</a>
|
||||
* <a href="https://github.com/tiangolo/full-stack-flask-couchdb" target="_blank">https://github.com/tiangolo/full-stack-flask-couchdb</a>
|
||||
|
||||
And these same full-stack generators were the base of the <a href="/project-generation/" target="_blank">**FastAPI** project generator</a>.
|
||||
|
||||
!!! info
|
||||
Flask-apispec was created by the same Marshmallow guys.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Generate the OpenAPI schema automatically, from the same code that defines serialization and validation.
|
||||
|
||||
### <a href="https://nestjs.com/" target="_blank">NestJS</a> (and <a href="https://angular.io/" target="_blank">Angular</a>)
|
||||
|
||||
This isn't even Python, NestJS is a JavaScript (TypeScript) NodeJS framework inspired by Angular.
|
||||
|
||||
It achieves something somewhat similar to what can be done with Flask-apispec.
|
||||
|
||||
It has an integrated dependency injection system, inspired by Angular two. It requires pre-registering the "injectables" (like all the other dependency injection systems I know), so, it adds to the verbosity and code repetition.
|
||||
|
||||
As the parameters are described with TypeScript types (similar to Python type hints), editor support is quite good.
|
||||
|
||||
But as TypeScript data is not preserved after compilation to JavaScript, it cannot rely on the types to define validation, serialization and documentation at the same time. Due to this and some design decisions, to get validation, serialization and automatic schema generation, it's needed to add decorators in many places. So, it becomes quite verbose.
|
||||
|
||||
It can't handle nested models very well. So, if the JSON body in the request is a JSON object that has inner fields that in turn are nested JSON objects, it cannot be properly documented and validated.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Use Python types to have great editor support.
|
||||
|
||||
Have a powerful dependency injection system. Find a way to minimize code repetition.
|
||||
|
||||
### <a href="https://sanic.readthedocs.io/en/latest/" target="_blank">Sanic</a>
|
||||
|
||||
It was one of the first extremely fast Python frameworks based on `asyncio`. It was made to be very similar to Flask.
|
||||
|
||||
!!! note "Technical Details"
|
||||
It used <a href="https://github.com/MagicStack/uvloop" target="_blank">`uvloop`</a> instead of the default Python `asyncio` loop. That's what made it so fast.
|
||||
|
||||
It <a href="https://github.com/huge-success/sanic/issues/761" target="_blank">still doesn't implement the ASGI spec for Python asynchronous web development</a>, but it clearly inspired Uvicorn and Starlette, that are currently faster than Sanic in open benchmarks.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Find a way to have a crazy performance.
|
||||
|
||||
That's why **FastAPI** is based on Starlette, as it is the fastest framework available (tested by third-party benchmarks).
|
||||
|
||||
### <a href="https://moltenframework.com/" target="_blank">Molten</a>
|
||||
|
||||
I discovered Molten in the first stages of building **FastAPI**. And it has quite similar ideas:
|
||||
|
||||
* Based on Python type hints.
|
||||
* Validation and documentation from these types.
|
||||
* Dependency Injection system.
|
||||
|
||||
It doesn't use a data validation, serialization and documentation third-party library like Pydantic, it has its own. So, these data type definitions would not be reusable as easily.
|
||||
|
||||
It requires a little bit more verbose configurations. And as it is based on WSGI (instead of ASGI), it is not designed to take advantage of the high-performance provided by tools like Uvicorn, Starlette and Sanic.
|
||||
|
||||
The dependency injection system requires pre-registration of the dependencies and the dependencies are solved based on the declared types. So, it's not possible to declare more than one "component" that provides a certain type.
|
||||
|
||||
Routes are declared in a single place, using functions declared in other places (instead of using decorators that can be placed right on top of the function that handles the endpoint). This is closer to how Django does it than to how Flask (and Starlette) does it. It separates in the code things that are relatively tightly coupled.
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Define extra validations for data types using the "default" value of model attributes. This improves editor support, and it was not available in Pydantic before.
|
||||
|
||||
This actually inspired updating parts of Pydantic, to support the same validation declaration style (all this functionality is now already available in Pydantic).
|
||||
|
||||
### <a href="https://github.com/encode/apistar" target="_blank">APIStar</a> (<= 0.5)
|
||||
|
||||
Right before deciding to build **FastAPI** I found **APIStar** server. It had almost everything I was looking for and had a great design.
|
||||
|
||||
It was actually the first implementation of a framework using Python type hints to declare parameters and requests that I ever saw (before NestJS and Molten).
|
||||
|
||||
It had automatic data validation, data serialization and OpenAPI schema generation based on the same type hints in several places.
|
||||
|
||||
Body schema definitions didn't use the same Python type hints like Pydantic, it was a bit more similar to Marshmallow, so, editor support wouldn't be as good, but still, APIStar was the best available option.
|
||||
|
||||
It had the best performance benchmarks at the time (only surpassed by Starlette).
|
||||
|
||||
At first, it didn't have an automatic API documentation web UI, but I knew I could add Swagger UI to it.
|
||||
|
||||
It had a dependency injection system. It required pre-registration of components, as other tools discussed above. But still, it was a great feature.
|
||||
|
||||
I was never able to use it in a full project, as it didn't have security integration, so, I couldn't replace all the features I was having with the full-stack generators based on Flask-apispec. I had in my backlog of projects to create a pull request adding that functionality.
|
||||
|
||||
But then, the project's focus shifted.
|
||||
|
||||
It was no longer an API web framework, as the creator needed to focus on Starlette.
|
||||
|
||||
Now APIStar is a set of tools to validate OpenAPI specifications, not a web framework.
|
||||
|
||||
!!! info
|
||||
APIStar was created by Tom Christie. The same guy that created:
|
||||
|
||||
* Django REST Framework
|
||||
* Starlette (in which **FastAPI** is based)
|
||||
* Uvicorn (used by Starlette and **FastAPI**)
|
||||
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Exist.
|
||||
|
||||
The idea of declaring multiple things (data validation, serialization and documentation) with the same Python types, that at the same time provided great editor support, was something I considered a brilliant idea.
|
||||
|
||||
And after searching for a long time for a similar framework and testing many different alternatives, APIStar was the best option available.
|
||||
|
||||
Then APIStar stopped to exist as a server and Starlette was created, and was a new better foundation for such a system. That was the final inspiration to build **FastAPI**.
|
||||
|
||||
I consider **FastAPI** a "spiritual successor" to APIStar, while improving and increasing the features, typing system, and other parts, based on the learnings from all these previous tools.
|
||||
|
||||
## Used by **FastAPI**
|
||||
|
||||
### <a href="https://pydantic-docs.helpmanual.io/" target="_blank">Pydantic</a>
|
||||
|
||||
Pydantic is a library to define data validation, serialization and documentation (using JSON Schema) based on Python type hints.
|
||||
|
||||
That makes it extremely intuitive.
|
||||
|
||||
It is comparable to Marshmallow. Although it's faster than Marshmallow in benchmarks. And as it is based on the same Python type hints, the editor support is great.
|
||||
|
||||
!!! check "**FastAPI** uses it to"
|
||||
Handle all the data validation, data serialization and automatic model documentation (based on JSON Schema).
|
||||
|
||||
**FastAPI** then takes that JSON Schema data and puts it in OpenAPI, apart from all the other things it does.
|
||||
|
||||
### <a href="https://www.starlette.io/" target="_blank">Starlette</a>
|
||||
|
||||
Starlette is a lightweight <abbr title="The new standard for building asynchronous Python web">ASGI</abbr> framework/toolkit, which is ideal for building high-performance asyncio services.
|
||||
|
||||
It is very simple and intuitive. It's designed to be easily extensible, and have modular components.
|
||||
|
||||
It has:
|
||||
|
||||
* Seriously impressive performance.
|
||||
* WebSocket support.
|
||||
* GraphQL support.
|
||||
* In-process background tasks.
|
||||
* Startup and shutdown events.
|
||||
* Test client built on requests.
|
||||
* CORS, GZip, Static Files, Streaming responses.
|
||||
* Session and Cookie support.
|
||||
* 100% test coverage.
|
||||
* 100% type annotated codebase.
|
||||
* Zero hard dependencies.
|
||||
|
||||
Starlette is currently the fastest Python framework tested. Only surpassed by Uvicorn, which is not a framework, but a server.
|
||||
|
||||
Starlette provides all the basic web microframework functionality.
|
||||
|
||||
But it doesn't provide automatic data validation, serialization or documentation.
|
||||
|
||||
That's one of the main things that **FastAPI** adds on top, all based on Python type hints (using Pydantic). That, plus the dependency injection system, security utilities, OpenAPI schema generation, etc.
|
||||
|
||||
!!! note "Technical Details"
|
||||
ASGI is a new "standard" being developed by Django core team members. It is still not a "Python standard" (a PEP), although they are in the process of doing that.
|
||||
|
||||
Nevertheless, it is already being used as a "standard" by several tools. This greatly improves interoperability, as you could switch Uvicorn for any other ASGI server (like Daphne or Hypercorn), or you could add ASGI compatible tools, like `python-socketio`.
|
||||
|
||||
!!! check "**FastAPI** uses it to"
|
||||
Handle all the core web parts. Adding features on top.
|
||||
|
||||
The class `FastAPI` itself inherits directly from the class `Starlette`.
|
||||
|
||||
So, anything that you can do with Starlette, you can do it directly with **FastAPI**, as it is basically Starlette on steroids.
|
||||
|
||||
### <a href="https://www.uvicorn.org/" target="_blank">Uvicorn</a>
|
||||
|
||||
Uvicorn is a lightning-fast ASGI server, built on uvloop and httptools.
|
||||
|
||||
It is not a web framework, but a server. For example, it doesn't provide tools for routing by paths. That's something that a framework like Starlette (or **FastAPI**) would provide on top.
|
||||
|
||||
It is the recommended server for Starlette and **FastAPI**.
|
||||
|
||||
!!! check "**FastAPI** recommends it as"
|
||||
The main web server to run **FastAPI** applications.
|
||||
|
||||
You can combine it with Gunicorn, to have an asynchronous multiprocess server.
|
||||
|
||||
Check more details in the <a href="/deployment/" target="_blank">Deployment</a> section.
|
||||
|
||||
## Benchmarks and speed
|
||||
|
||||
To understand, compare, and see the difference between Uvicorn, Starlette and FastAPI, check the section about [Benchmarks](/benchmarks/).
|
||||
32
docs/benchmarks.md
Normal file
32
docs/benchmarks.md
Normal file
@@ -0,0 +1,32 @@
|
||||
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=a979de55-980d-4721-a46f-77298b3f3923&hw=ph&test=fortune&l=zijzen-7" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
|
||||
|
||||
But when checking benchmarks and comparisons you should have the following in mind.
|
||||
|
||||
## Benchmarks and speed
|
||||
|
||||
When you check the benchmarks, it is common to see several tools of different types compared as equivalent.
|
||||
|
||||
Specifically, to see Uvicorn, Starlette and FastAPI compared together (among many other tools).
|
||||
|
||||
The simplest the problem solved by the tool, the better performance it will get. And most of the benchmarks don't test the additional features provided by the tool.
|
||||
|
||||
The hierarchy is like:
|
||||
|
||||
* **Uvicorn**: an ASGI server
|
||||
* **Starlette**: (uses Uvicorn) a web microframework
|
||||
* **FastAPI**: (uses Starlette) an API microframework with several additional features for building APIs, with data validation, etc.
|
||||
|
||||
* **Uvicorn**:
|
||||
* Will have the best performance, as it doesn't have much extra code apart from the server itself.
|
||||
* You wouldn't write an application in Uvicorn directly. That would mean that your code would have to include more or less, at least, all the code provided by Starlette (or **FastAPI**). And if you did that, your final application would have the same overhead as having used a framework and minimizing your app code and bugs.
|
||||
* If you are comparing Uvicorn, compare it against Daphne, Hypercorn, uWSGI, etc. Application servers.
|
||||
* **Starlette**:
|
||||
* Will have the next best performance, after Uvicorn. In fact, Starlette uses Uvicorn to run. So, it probably can only get "slower" than Uvicorn by having to execute more code.
|
||||
* But it provides you the tools to build simple web applications, with routing based on paths, etc.
|
||||
* If you are comparing Starlette, compare it against Sanic, Flask, Django, etc. Web frameworks (or microframeworks).
|
||||
* **FastAPI**:
|
||||
* The same way that Starlette uses Uvicorn and cannot be faster than it, **FastAPI** uses Starlette, so it cannot be faster than it.
|
||||
* FastAPI provides more features on top of Starlette. Features that you almost always need when building APIs, like data validation and serialization. And by using it, you get automatic documentation for free (the automatic documentation doesn't even add overhead to running applications, it is generated on startup).
|
||||
* If you didn't use FastAPI and used Starlette directly (or another tool, like Sanic, Flask, Responder, etc) you would have to implement all the data validation and serialization yourself. So, your final application would still have the same overhead as if it was built using FastAPI. And in many cases, this data validation and serialization is the biggest amount of code written in applications.
|
||||
* So, by using FastAPI you are saving development time, bugs, lines of code, and you would probably get the same performance (or better) you would if you didn't use it (as you would have to implement it all in your code).
|
||||
* If you are comparing FastAPI, compare it against a web application framework (or set of tools) that provides data validation, serialization and documentation, like Flask-apispec, NestJS, Molten, etc. Frameworks with integrated automatic data validation, serialization and documentation.
|
||||
@@ -1 +1,219 @@
|
||||
Coming soon...
|
||||
It is recommended to use <a href="https://www.docker.com/" target="_blank">**Docker**</a> for security, replicability, development simplicity, etc.
|
||||
|
||||
In this section you'll see instructions and links to guides to know how to:
|
||||
|
||||
* Make your **FastAPI** application a Docker image/container with maximum performance. In about **5 min**.
|
||||
* (Optionally) understand what you, as a developer, need to know about HTTPS.
|
||||
* Set up a Docker Swarm mode cluster with automatic HTTPS, even on a simple $5 USD/month server. In about **20 min**.
|
||||
* Generate and deploy a full **FastAPI** application, using your Docker Swarm cluster, with HTTPS, etc. In about **10 min**.
|
||||
|
||||
---
|
||||
|
||||
You can also easily use **FastAPI** in a standard server directly too (without Docker).
|
||||
|
||||
|
||||
## Docker
|
||||
|
||||
If you are using Docker, you can use the official Docker image:
|
||||
|
||||
### <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>
|
||||
|
||||
This image has an "auto-tuning" mechanism included, so that you can just add your code and get very high performance automatically. And without making sacrifices.
|
||||
|
||||
But you can still change and update all the configurations with environment variables or configuration files.
|
||||
|
||||
!!! tip
|
||||
To see all the configurations and options, go to the Docker image page: <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>.
|
||||
|
||||
|
||||
### Build your Image
|
||||
|
||||
* Go to your project directory.
|
||||
* Create a `Dockerfile` with:
|
||||
|
||||
```Dockerfile
|
||||
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
|
||||
|
||||
COPY ./app /app
|
||||
```
|
||||
|
||||
* Create an `app` directory and enter in it.
|
||||
* Create a `main.py` file with:
|
||||
|
||||
```Python
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
return {"Hello": "World"}
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
def read_item(item_id: int, q: str = None):
|
||||
return {"item_id": item_id, "q": q}
|
||||
```
|
||||
|
||||
* You should now have a directory structure like:
|
||||
|
||||
```
|
||||
.
|
||||
├── app
|
||||
│ └── main.py
|
||||
└── Dockerfile
|
||||
```
|
||||
|
||||
* Go to the project directory (in where your `Dockerfile` is, containing your `app` directory).
|
||||
* Build your FastAPI image:
|
||||
|
||||
```bash
|
||||
docker build -t myimage .
|
||||
```
|
||||
|
||||
* Run a container based on your image:
|
||||
|
||||
```bash
|
||||
docker run -d --name mycontainer -p 80:80 myimage
|
||||
```
|
||||
|
||||
Now you have an optimized FastAPI server in a Docker container. Auto-tuned for your current server (and number of CPU cores).
|
||||
|
||||
|
||||
### Check it
|
||||
|
||||
You should be able to check it in your Docker container's URL, for example: <a href="http://192.168.99.100/items/5?q=somequery" target="_blank">http://192.168.99.100/items/5?q=somequery</a> or <a href="http://127.0.0.1/items/5?q=somequery" target="_blank">http://127.0.0.1/items/5?q=somequery</a> (or equivalent, using your Docker host).
|
||||
|
||||
You will see something like:
|
||||
|
||||
```JSON
|
||||
{"item_id": 5, "q": "somequery"}
|
||||
```
|
||||
|
||||
|
||||
### Interactive API docs
|
||||
|
||||
Now you can go to <a href="http://192.168.99.100/docs" target="_blank">http://192.168.99.100/docs</a> or <a href="http://127.0.0.1/docs" target="_blank">http://127.0.0.1/docs</a> (or equivalent, using your Docker host).
|
||||
|
||||
You will see the automatic interactive API documentation (provided by <a href="https://github.com/swagger-api/swagger-ui" target="_blank">Swagger UI</a>):
|
||||
|
||||

|
||||
|
||||
|
||||
### Alternative API docs
|
||||
|
||||
And you can also go to <a href="http://192.168.99.100/redoc" target="_blank">http://192.168.99.100/redoc</a> or <a href="http://127.0.0.1/redoc" target="_blank">http://127.0.0.1/redoc</a> (or equivalent, using your Docker host).
|
||||
|
||||
You will see the alternative automatic documentation (provided by <a href="https://github.com/Rebilly/ReDoc" target="_blank">ReDoc</a>):
|
||||
|
||||

|
||||
|
||||
|
||||
## HTTPS
|
||||
|
||||
### About HTTPS
|
||||
|
||||
It is easy to assume that HTTPS is something that is just "enabled" or not.
|
||||
|
||||
But it is way more complex than that.
|
||||
|
||||
!!! tip
|
||||
If you are in a hurry or don't care, continue with the next section for step by step instructions to set everything up.
|
||||
|
||||
To learn the basics of HTTPS, from a consumer perspective, check <a href="https://howhttps.works/" target="_blank">https://howhttps.works/</a>.
|
||||
|
||||
Now, from a developer's perspective, here are several things to have in mind while thinking about HTTPS:
|
||||
|
||||
* For HTTPS, the server needs to have "certificates" generated by a third party.
|
||||
* Those certificates are actually acquired from the third-party, not "generated".
|
||||
* Certificates have a lifetime.
|
||||
* They expire.
|
||||
* And then they need to be renewed, acquired again from the third party.
|
||||
* The encryption of the connection happens at the TCP level.
|
||||
* That's one layer below HTTP.
|
||||
* So, the certificate and encryption handling is done before HTTP.
|
||||
* TCP doesn't know about "domains". Only about IP addresses.
|
||||
* The information about the specific domain requested goes in the HTTP data.
|
||||
* The HTTPS certificates "certificate" a certain domain, but the protocol and encryption happen at the TCP level, before knowing which domain is being dealt with.
|
||||
* By default, that would mean that you can only have one HTTPS certificate per IP address.
|
||||
* No matter how big is your server and how small each application you have there might be. But...
|
||||
* There's an extension to the TLS protocol (the one handling the encryption at the TCP level, before HTTP) called <a href="https://en.wikipedia.org/wiki/Server_Name_Indication" target="_blank"><abbr title="Server Name Indication">SNI</abbr></a>.
|
||||
* This SNI extension allows one single server (with a single IP address) to have several HTTPS certificates and server multiple HTTPS domains/applications.
|
||||
* For this to work, a single component (program) running in the server, listening in the public IP address, must have all the HTTPS certificates in the server.
|
||||
* After having a secure connection, the communication protocol is the same HTTP.
|
||||
* It goes encrypted, but the encrypted contents are the same HTTP protocol.
|
||||
|
||||
|
||||
It is a common practice to have one program/HTTP server runing in the server (the machine, host, etc) and managing all the HTTPS parts, sending the decrypted HTTP requests to the actual HTTP application running in the same server (the **FastAPI** application, in this case), take the HTTP response from the application, encrypt it using the appropriate certificate and sending it back to the client using HTTPS. This server is ofter called a <a href="https://en.wikipedia.org/wiki/TLS_termination_proxy" target="_blank">TLS Termination Proxy</a>.
|
||||
|
||||
|
||||
### Let's Encrypt
|
||||
|
||||
Up to some years ago, these HTTPS certificates were sold by trusted third-parties.
|
||||
|
||||
The process to acquire one of these certificates used to be cumbersome, require quite some paperwork and the certificates were quite expensive.
|
||||
|
||||
But then <a href="https://letsencrypt.org/" target="_blank">Let's Encrypt</a> was created.
|
||||
|
||||
It is a project from the Linux Foundation. It provides HTTPS certificates for free. In an automated way. These certificates use all the standard cryptographic security, and are short lived (about 3 months), so, the security is actually increased, by reducing their lifespan.
|
||||
|
||||
The domain's are securely verified and the certificates are generated automatically. This also allows automatizing the renewal of these certificates.
|
||||
|
||||
The idea is to automatize the acquisition and renewal of these certificates, so that you can have secure HTTPS, free, forever.
|
||||
|
||||
|
||||
### Traefik
|
||||
|
||||
<a href="https://traefik.io/" target="_blank">Traefik</a> is a high performance reverse proxy / load balancer. It can do the "TLS Termination Proxy" job (apart from other features).
|
||||
|
||||
It has integration with Let's Encrypt. So, it can handle all the HTTPS parts, including certificate acquisition and renewal.
|
||||
|
||||
It also has integrations with Docker. So, you can declare your domains in each application configurations and have it read those configurations, generate the HTTPS certificates and serve HTTPS to your application, all automatically. Without requiring any change in its configuration.
|
||||
|
||||
---
|
||||
|
||||
With this information and tools, continue with the next section to combine everything.
|
||||
|
||||
|
||||
## Docker Swarm mode cluster with Traefik and HTTPS
|
||||
|
||||
You can have a Docker Swarm mode cluster set up in minutes (about 20 min) with a main Traefik handling HTTPS (including certificate acquisition and renewal).
|
||||
|
||||
By using Docker Swarm mode, you can start with a "cluster" of a single machine (it can even be a $5 USD / month server) and then you can grow as much as you need adding more servers.
|
||||
|
||||
To set up a Docker Swarm Mode cluster with Traefik and HTTPS handling, follow this guide:
|
||||
|
||||
### <a href="https://medium.com/@tiangolo/docker-swarm-mode-and-traefik-for-a-https-cluster-20328dba6232" target="_blank">Docker Swarm Mode and Traefik for an HTTPS cluster</a>.
|
||||
|
||||
|
||||
### Deploy a FastAPI application
|
||||
|
||||
The easiest way to set everything up, would be using the <a href="/project-generation/" target="_blank">FastAPI project generator</a>.
|
||||
|
||||
It is designed to be integrated with this Docker Swarm cluster with Traefik and HTTPS described above.
|
||||
|
||||
You can generate a project in about 2 min.
|
||||
|
||||
The generated project has instructions to deploy it, doing it takes other 2 min.
|
||||
|
||||
|
||||
## Alternatively, deploy **FastAPI** without Docker
|
||||
|
||||
You can deploy **FastAPI** directly without Docker too.
|
||||
|
||||
You just need to install <a href="https://www.uvicorn.org/" target="_blank">Uvicorn</a> (or any other ASGI server).
|
||||
|
||||
And run your application the same way you have done in the tutorials, but without the `--debug` option, e.g.:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --host 0.0.0.0 --port 80
|
||||
```
|
||||
|
||||
You might want to set up some tooling to make sure it is restarted automatically if it stops.
|
||||
|
||||
You might also want to install <a href="https://gunicorn.org/" target="_blank">Gunicorn</a> and <a href="https://www.uvicorn.org/#running-with-gunicorn" target="_blank">use it as a manager for Uvicorn</a>.
|
||||
|
||||
Making sure to fine-tune the number of workers, etc.
|
||||
|
||||
But if you are doing all that, you might just use the Docker image that does it automatically.
|
||||
|
||||
@@ -153,7 +153,7 @@ Any integration is designed to be so simple to use (with dependencies) that you
|
||||
|
||||
### Tested
|
||||
|
||||
* 100% <abbr title="The amount of code that is automatically tested">test coverage</abbr> (* not yet, in a couple days).
|
||||
* 100% <abbr title="The amount of code that is automatically tested">test coverage</abbr>.
|
||||
* 100% <abbr title="Python type annotations, with this your editor and external tools can give you better support">type annotated</abbr> code base.
|
||||
* Used in production applications.
|
||||
|
||||
|
||||
BIN
docs/img/tutorial/bigger-applications/image01.png
Normal file
BIN
docs/img/tutorial/bigger-applications/image01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
BIN
docs/img/tutorial/body-schema/image01.png
Normal file
BIN
docs/img/tutorial/body-schema/image01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
BIN
docs/img/tutorial/response-status-code/image01.png
Normal file
BIN
docs/img/tutorial/response-status-code/image01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
BIN
docs/img/tutorial/response-status-code/image02.png
Normal file
BIN
docs/img/tutorial/response-status-code/image02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -24,11 +24,11 @@
|
||||
|
||||
---
|
||||
|
||||
FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+.
|
||||
FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
|
||||
|
||||
The key features are:
|
||||
|
||||
* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic).
|
||||
* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance).
|
||||
|
||||
* **Fast to code**: Increase the speed to develop features by about 200% to 300% *.
|
||||
* **Less bugs**: Reduce about 40% of human (developer) induced errors. *
|
||||
@@ -166,7 +166,7 @@ You will see the alternative automatic documentation (provided by <a href="https
|
||||
|
||||
## Example upgrade
|
||||
|
||||
Now modify the file `main.py` to recive a body from a `PUT` request.
|
||||
Now modify the file `main.py` to receive a body from a `PUT` request.
|
||||
|
||||
Declare the body using standard Python types, thanks to Pydantic.
|
||||
|
||||
@@ -257,7 +257,7 @@ item: Item
|
||||
* Validation of data:
|
||||
* Automatic and clear errors when the data is invalid.
|
||||
* Validation even for deeply nested JSON objects.
|
||||
* <abbr title="also known as: serialization, parsing, marshalling">Conversion</abbr> of input data: coming from the network, to Python data and types. Reading from:
|
||||
* <abbr title="also known as: serialization, parsing, marshalling">Conversion</abbr> of input data: coming from the network to Python data and types. Reading from:
|
||||
* JSON.
|
||||
* Path parameters.
|
||||
* Query parameters.
|
||||
@@ -291,8 +291,8 @@ Coming back to the previous code example, **FastAPI** will:
|
||||
* Check that it has an optional attribute `is_offer`, that should be a `bool`, if present.
|
||||
* All this would also work for deeply nested JSON objects.
|
||||
* Convert from and to JSON automatically.
|
||||
* Document everything as an OpenAPI schema, that can be used by:
|
||||
* Interactive documentation sytems.
|
||||
* Document everything with OpenAPI, that can be used by:
|
||||
* Interactive documentation systems.
|
||||
* Automatic client code generation systems, for many languages.
|
||||
* Provide 2 interactive documentation web interfaces directly.
|
||||
|
||||
@@ -329,7 +329,7 @@ For a more complete example including more features, see the <a href="https://fa
|
||||
**Spoiler alert**: the tutorial - user guide includes:
|
||||
|
||||
* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**.
|
||||
* How to set **validation constrains** as `maximum_length` or `regex`.
|
||||
* How to set **validation constraints** as `maximum_length` or `regex`.
|
||||
* A very powerful and easy to use **<abbr title="also known as components, resources, providers, services, injectables">Dependency Injection</abbr>** system.
|
||||
* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth.
|
||||
* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic).
|
||||
@@ -342,6 +342,11 @@ For a more complete example including more features, see the <a href="https://fa
|
||||
* ...and more.
|
||||
|
||||
|
||||
## Performance
|
||||
|
||||
Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=a979de55-980d-4721-a46f-77298b3f3923&hw=ph&test=fortune&l=zijzen-7" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)
|
||||
|
||||
To understand more about it, see the section <a href="https://fastapi.tiangolo.com/benchmarks/" target="_blank">Benchmarks</a>.
|
||||
|
||||
## Optional Dependencies
|
||||
|
||||
@@ -366,7 +371,7 @@ Used by FastAPI / Starlette:
|
||||
|
||||
* <a href="http://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - for the server that loads and serves your application.
|
||||
|
||||
You can install all of these with `pip3 install fastapi[full]`.
|
||||
You can install all of these with `pip3 install fastapi[all]`.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
9
docs/release-notes.md
Normal file
9
docs/release-notes.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## 0.2.0
|
||||
|
||||
* Fix typos in Security section: <a href="https://github.com/tiangolo/fastapi/pull/24" target="_blank">#24</a> by <a href="https://github.com/kkinder" target="_blank">@kkinder</a>
|
||||
|
||||
* Add support for Pydantic custom JSON encoders: <a href="https://github.com/tiangolo/fastapi/pull/21" target="_blank">#21</a> by <a href="https://github.com/euri10" target="_blank">@euri10</a>
|
||||
|
||||
## 0.1.19
|
||||
|
||||
* Upgrade Starlette version to the current latest `0.10.1`: <a href="https://github.com/tiangolo/fastapi/pull/17" target="_blank">#17</a> by <a href="https://github.com/euri10" target="_blank">@euri10</a>
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
from .routers.tutorial001 import router as users_router
|
||||
from .routers.tutorial002 import router as items_router
|
||||
from .routers.items import router as items_router
|
||||
from .routers.users import router as users_router
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -3,11 +3,11 @@ from fastapi import APIRouter
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
@router.get("/", tags=["items"])
|
||||
async def read_items():
|
||||
return [{"name": "Item Foo"}, {"name": "item Bar"}]
|
||||
|
||||
|
||||
@router.get("/{item_id}")
|
||||
@router.get("/{item_id}", tags=["items"])
|
||||
async def read_item(item_id: str):
|
||||
return {"name": "Fake Specific Item", "item_id": item_id}
|
||||
@@ -3,16 +3,16 @@ from fastapi import APIRouter
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/users/")
|
||||
@router.get("/users/", tags=["users"])
|
||||
async def read_users():
|
||||
return [{"username": "Foo"}, {"username": "Bar"}]
|
||||
|
||||
|
||||
@router.get("/users/me")
|
||||
@router.get("/users/me", tags=["users"])
|
||||
async def read_user_me():
|
||||
return {"username": "fakecurrentuser"}
|
||||
|
||||
|
||||
@router.get("/users/{username}")
|
||||
@router.get("/users/{username}", tags=["users"])
|
||||
async def read_user(username: str):
|
||||
return {"username": username}
|
||||
@@ -10,3 +10,8 @@ async def common_parameters(q: str = None, skip: int = 0, limit: int = 100):
|
||||
@app.get("/items/")
|
||||
async def read_items(commons: dict = Depends(common_parameters)):
|
||||
return commons
|
||||
|
||||
|
||||
@app.get("/users/")
|
||||
async def read_users(commons: dict = Depends(common_parameters)):
|
||||
return commons
|
||||
|
||||
@@ -18,6 +18,6 @@ async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
|
||||
response = {}
|
||||
if commons.q:
|
||||
response.update({"q": commons.q})
|
||||
items = fake_items_db[commons.skip : commons.limit]
|
||||
items = fake_items_db[commons.skip : commons.skip + commons.limit]
|
||||
response.update({"items": items})
|
||||
return response
|
||||
|
||||
@@ -18,6 +18,6 @@ async def read_items(commons=Depends(CommonQueryParams)):
|
||||
response = {}
|
||||
if commons.q:
|
||||
response.update({"q": commons.q})
|
||||
items = fake_items_db[commons.skip : commons.limit]
|
||||
items = fake_items_db[commons.skip : commons.skip + commons.limit]
|
||||
response.update({"items": items})
|
||||
return response
|
||||
|
||||
@@ -18,6 +18,6 @@ async def read_items(commons: CommonQueryParams = Depends()):
|
||||
response = {}
|
||||
if commons.q:
|
||||
response.update({"q": commons.q})
|
||||
items = fake_items_db[commons.skip : commons.limit]
|
||||
items = fake_items_db[commons.skip : commons.skip + commons.limit]
|
||||
response.update({"items": items})
|
||||
return response
|
||||
|
||||
@@ -7,4 +7,4 @@ fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_item(skip: int = 0, limit: int = 100):
|
||||
return fake_items_db[skip:limit]
|
||||
return fake_items_db[skip : skip + limit]
|
||||
|
||||
8
docs/src/response_status_code/tutorial001.py
Normal file
8
docs/src/response_status_code/tutorial001.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/items/", status_code=201)
|
||||
async def create_item(name: str):
|
||||
return {"name": name}
|
||||
9
docs/src/response_status_code/tutorial002.py
Normal file
9
docs/src/response_status_code/tutorial002.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.status import HTTP_201_CREATED
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/items/", status_code=HTTP_201_CREATED)
|
||||
async def create_item(name: str):
|
||||
return {"name": name}
|
||||
@@ -10,7 +10,14 @@ fake_users_db = {
|
||||
"email": "johndoe@example.com",
|
||||
"hashed_password": "fakehashedsecret",
|
||||
"disabled": False,
|
||||
}
|
||||
},
|
||||
"alice": {
|
||||
"username": "alice",
|
||||
"full_name": "Alice Wonderson",
|
||||
"email": "alice@example.com",
|
||||
"hashed_password": "fakehashedsecret2",
|
||||
"disabled": True,
|
||||
},
|
||||
}
|
||||
|
||||
app = FastAPI()
|
||||
@@ -68,7 +75,7 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
if not user_dict:
|
||||
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
||||
user = UserInDB(**user_dict)
|
||||
hashed_password = fake_hash_password(data.password)
|
||||
hashed_password = fake_hash_password(form_data.password)
|
||||
if not hashed_password == user.hashed_password:
|
||||
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
||||
|
||||
|
||||
@@ -1,13 +1,284 @@
|
||||
Coming soon...
|
||||
If you are building an application or a web API, it's rarely the case that you can put everything on a single file.
|
||||
|
||||
```Python
|
||||
{!./src/bigger_applications/app/routers/tutorial001.py!}
|
||||
**FastAPI** provides a convenience tool to structure your application while keeping all the flexibility.
|
||||
|
||||
|
||||
## An example file structure
|
||||
|
||||
Let's say you have a file structure like this:
|
||||
|
||||
```
|
||||
.
|
||||
├── app
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py
|
||||
│ └── routers
|
||||
│ ├── __init__.py
|
||||
│ ├── items.py
|
||||
│ └── users.py
|
||||
```
|
||||
|
||||
```Python
|
||||
{!./src/bigger_applications/app/routers/tutorial002.py!}
|
||||
!!! tip
|
||||
There are two `__init__.py` files: one in each directory or subdirectory.
|
||||
|
||||
This is what allows importing code from one file into another.
|
||||
|
||||
For example, in `app/main.py` you could have a line like:
|
||||
|
||||
```
|
||||
from app.routers import items
|
||||
```
|
||||
|
||||
|
||||
* The `app` directory contains everything.
|
||||
* This `app` directory has an empty file `app/__init__.py`.
|
||||
* So, the `app` directory is a "Python package" (a collection of "Python modules").
|
||||
* The `app` directory also has a `app/main.py` file.
|
||||
* As it is inside a Python package directory (because there's a file `__init__.py`), it is a "module" of that package: `app.main`.
|
||||
* There's a subdirectory `app/routers/`.
|
||||
* The subdirectory `app/routers` also has an empty file `__init__.py`.
|
||||
* So, it is a "Python subpackage".
|
||||
* The file `app/routers/items.py` is beside the `app/routers/__init__.py`.
|
||||
* So, it's a submodule: `app.routers.items`.
|
||||
* The file `app/routers/users.py` is beside the `app/routers/__init__.py`.
|
||||
* So, it's a submodule: `app.routers.users`.
|
||||
|
||||
|
||||
## `APIRouter`
|
||||
|
||||
Let's say the file dedicated to handling just users is the submodule at `/app/routers/users.py`.
|
||||
|
||||
You want to have the *path operations* related to your users separated from the rest of the code, to keep it organized.
|
||||
|
||||
But it's still part of the same **FastAPI** application/web API (it's part of the same "Python Package").
|
||||
|
||||
You can create the *path operations* for that module using `APIRouter`.
|
||||
|
||||
|
||||
### Import `APIRouter`
|
||||
|
||||
You import it and create an "instance" the same way you would with the class `FastAPI`:
|
||||
|
||||
```Python hl_lines="1 3"
|
||||
{!./src/bigger_applications/app/routers/users.py!}
|
||||
```
|
||||
|
||||
```Python
|
||||
{!./src/bigger_applications/app/tutorial003.py!}
|
||||
|
||||
### Path operations with `APIRouter`
|
||||
|
||||
And then you use it to declare your *path operations*.
|
||||
|
||||
Use it the same way you would use the `FastAPI` class:
|
||||
|
||||
```Python hl_lines="6 11 16"
|
||||
{!./src/bigger_applications/app/routers/users.py!}
|
||||
```
|
||||
|
||||
You can think of `APIRouter` as a "mini `FastAPI`" class.
|
||||
|
||||
All the same options are supported.
|
||||
|
||||
All the same parameters, responses, dependencies, tags, etc.
|
||||
|
||||
!!! tip
|
||||
In this example, the variable is called `router`, but you can name it however you want.
|
||||
|
||||
We are going to include this `APIrouter` in the main `FastAPI` app, but first, let's add another `APIRouter`.
|
||||
|
||||
|
||||
## Another module with `APIRouter`
|
||||
|
||||
Let's say you also have the endpoints dedicated to handling "Items" from your application in the module at `app/routers/items.py`.
|
||||
|
||||
You have path operations for:
|
||||
|
||||
* `/items/`
|
||||
* `/items/{item_id}`
|
||||
|
||||
It's all the same structure as with `app/routers/users.py`.
|
||||
|
||||
But let's say that this time we are more lazy.
|
||||
|
||||
And we don't want to have to explicitly type `/items/` in every path operation, we can do it later:
|
||||
|
||||
```Python hl_lines="6 11 16"
|
||||
{!./src/bigger_applications/app/routers/items.py!}
|
||||
```
|
||||
|
||||
|
||||
## The main `FastAPI`
|
||||
|
||||
Now, let's see the module at `app/main.py`.
|
||||
|
||||
Here's where you import and use the class `FastAPI`.
|
||||
|
||||
This will be the main file in your application that ties everything together.
|
||||
|
||||
### Import `FastAPI`
|
||||
|
||||
You import and create a `FastAPI` class as normally:
|
||||
|
||||
```Python hl_lines="1 6"
|
||||
{!./src/bigger_applications/app/main.py!}
|
||||
```
|
||||
|
||||
### Import the `APIRouter`
|
||||
|
||||
But this time we are not adding path operations directly with the `FastAPI` `app`.
|
||||
|
||||
We import the `APIRouter`s from the other files:
|
||||
|
||||
```Python hl_lines="3 4"
|
||||
{!./src/bigger_applications/app/main.py!}
|
||||
```
|
||||
|
||||
As the file `app/routers/items.py` is part of the same Python package, we can import it using "dot notation".
|
||||
|
||||
|
||||
### How the importing works
|
||||
|
||||
The section:
|
||||
|
||||
```Python
|
||||
from .routers.items import router
|
||||
```
|
||||
|
||||
Means:
|
||||
|
||||
* Starting in the same package that this module (the file `app/main.py`) lives in (the directory `app/`)...
|
||||
* look for the subpackage `routers` (the directory at `app/routers/`)...
|
||||
* and from it, the submodule `items` (the file at `app/routers/items.py`)...
|
||||
* and from that submodule, import the variable `router`.
|
||||
|
||||
The variable `router` is the same one we created in the file `app/routers/items.py`. It's an `APIRouter`.
|
||||
|
||||
We could also import it like:
|
||||
|
||||
```Python
|
||||
from app.routers.items import router
|
||||
```
|
||||
|
||||
!!! info
|
||||
The first version is a "relative import".
|
||||
|
||||
The second version is an "absolute import".
|
||||
|
||||
To learn more about Python Packages and Modules, read <a href="https://docs.python.org/3/tutorial/modules.html" target="_blank">the official Python documentation about Modules</a>.
|
||||
|
||||
|
||||
### Avoid name collisions
|
||||
|
||||
We are importing a variable named `router` from the submodule `items`.
|
||||
|
||||
But we also have another variable named `router` in the submodule `users`.
|
||||
|
||||
If we import one after the other, like:
|
||||
|
||||
```Python
|
||||
from .routers.items import router
|
||||
from .routers.users import router
|
||||
```
|
||||
|
||||
The `router` from `users` will overwrite the one form `items` and we won't be able to use them at the same time.
|
||||
|
||||
So, to be able to use both of them in the same file, we rename them while importing them using `as`:
|
||||
|
||||
```Python hl_lines="3 4"
|
||||
{!./src/bigger_applications/app/main.py!}
|
||||
```
|
||||
|
||||
|
||||
### Include an `APIRouter`
|
||||
|
||||
Now, let's include the router from the submodule `users`, now in the variable `users_router`:
|
||||
|
||||
```Python hl_lines="8"
|
||||
{!./src/bigger_applications/app/main.py!}
|
||||
```
|
||||
|
||||
With `app.include_router()` we can add an `APIRouter` to the main `FastAPI` application.
|
||||
|
||||
It will include all the routes from that router as part of it.
|
||||
|
||||
!!! note "Technical Details"
|
||||
It will actually internally create a path operation for each path operation that was declared in the `APIRouter`.
|
||||
|
||||
So, behind the scenes, it will actually work as if everything was the same single app.
|
||||
|
||||
|
||||
!!! check
|
||||
You don't have to worry about performance when including routers.
|
||||
|
||||
This will take microseconds and will only happen at startup.
|
||||
|
||||
So it won't affect performance.
|
||||
|
||||
|
||||
### Include an `APIRouter` with a prefix
|
||||
|
||||
Now, let's include the router form the `items` submodule, now in the variable `items_router`.
|
||||
|
||||
But, remember that we were lazy and didn't add `/items/` to all the path operations?
|
||||
|
||||
We can add a prefix to all the path operations using the parameter `prefix` of `app.include_router()`.
|
||||
|
||||
As the path of each path operation has to start with `/`, like in:
|
||||
|
||||
```Python hl_lines="1"
|
||||
@router.get("/{item_id}", tags=["items"])
|
||||
async def read_item(item_id: str):
|
||||
...
|
||||
```
|
||||
|
||||
...the prefix must not include a final `/`.
|
||||
|
||||
So, the prefix in this case would be `/items`:
|
||||
|
||||
```Python hl_lines="9"
|
||||
{!./src/bigger_applications/app/main.py!}
|
||||
```
|
||||
|
||||
The end result is that the item paths are now:
|
||||
|
||||
* `/items/`
|
||||
* `/items/{item_id}`
|
||||
|
||||
...as we intended.
|
||||
|
||||
!!! check
|
||||
The `prefix` parameter is (as in many other cases) just a feature from **FastAPI** to help you avoid code duplication.
|
||||
|
||||
|
||||
!!! tip
|
||||
You could also add path operations directly, for example with: `@app.get(...)`.
|
||||
|
||||
Apart from `app.include_router()`, in the same **FastAPI** app.
|
||||
|
||||
It would still work the same.
|
||||
|
||||
|
||||
!!! info "Very Technical Details"
|
||||
**Note**: this is a very technical detail that you probably can **just skip**.
|
||||
|
||||
---
|
||||
|
||||
The `APIRouter`s are not "mounted", they are not isolated from the rest of the application.
|
||||
|
||||
This is because we want to include their path operations in the OpenAPI schema and the user interfaces.
|
||||
|
||||
As we cannot just isolate them and "mount" them independently of the rest, the path operations are "cloned" (re-created), not included directly.
|
||||
|
||||
|
||||
## Check the automatic API docs
|
||||
|
||||
Now, run `uvicorn`, using the module `app.main` and the variable `app`:
|
||||
|
||||
```bash
|
||||
uvicorn app.main:app --debug
|
||||
```
|
||||
|
||||
And open the docs at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
|
||||
You will see the automatic API docs, including the paths from all the submodules:
|
||||
|
||||
<img src="/img/tutorial/bigger-applications/image01.png">
|
||||
|
||||
@@ -33,7 +33,7 @@ But you can also declare multiple body parameters, e.g. `item` and `user`:
|
||||
{!./src/body_multiple_params/tutorial002.py!}
|
||||
```
|
||||
|
||||
In this case, **FastAPI** will notice that there are more than one body parameter in the function (two parameters that are Pydantic models).
|
||||
In this case, **FastAPI** will notice that there are more than one body parameters in the function (two parameters that are Pydantic models).
|
||||
|
||||
So, it will then use the parameter names as keys (field names) in the body, and expect a body like:
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ First, you have to import it:
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Notice that `Schema` is imported directly from `pydantic`, not form `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc).
|
||||
Notice that `Schema` is imported directly from `pydantic`, not from `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc).
|
||||
|
||||
|
||||
## Declare model attributes
|
||||
@@ -37,7 +37,7 @@ In `Schema`, `Path`, `Query`, `Body` and others you'll see later, you can declar
|
||||
|
||||
Those parameters will be added as-is to the output JSON Schema.
|
||||
|
||||
If you know JSON Schema and want to add extra information appart from what we have discussed here, you can pass that as extra keyword arguments.
|
||||
If you know JSON Schema and want to add extra information apart from what we have discussed here, you can pass that as extra keyword arguments.
|
||||
|
||||
!!! warning
|
||||
Have in mind that extra parameters passed won't add any validation, only annotation, for documentation purposes.
|
||||
@@ -48,6 +48,10 @@ For example, you can use that functionality to pass a <a href="http://json-schem
|
||||
{!./src/body_schema/tutorial002.py!}
|
||||
```
|
||||
|
||||
And it would look in the `/docs` like this:
|
||||
|
||||
<img src="/img/tutorial/body-schema/image01.png">
|
||||
|
||||
## Recap
|
||||
|
||||
You can use Pydantic's `Schema` to declare extra validations and metadata for model attributes.
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
To declare a request body, you use <a href="https://pydantic-docs.helpmanual.io/" target="_blank">Pydantic</a> models with all their power and benefits.
|
||||
When you need to send data from a client (let's say, a browser) to your API, you send it as a **request body**.
|
||||
|
||||
A **request** body is data sent by the client to your API. A **response** body is the data your API sends to the client.
|
||||
|
||||
Your API almost always has to send a **response** body. But clients don't necessarily need to send **request** bodies all the time.
|
||||
|
||||
To declare a **request** body, you use <a href="https://pydantic-docs.helpmanual.io/" target="_blank">Pydantic</a> models with all their power and benefits.
|
||||
|
||||
!!! info
|
||||
You cannot send a request body using a `GET` operation (HTTP method).
|
||||
|
||||
To send data, you have to use one of: `POST` (the more common), `PUT`, `DELETE` or `PATCH`.
|
||||
|
||||
## Import Pydantic's `BaseModel`
|
||||
|
||||
@@ -75,7 +86,7 @@ And will be also used in the API docs inside each path operation that needs them
|
||||
|
||||
## Editor support
|
||||
|
||||
In your editor, inside your function you will get type hints and completion everywhere (this wouldn't happen if your received a `dict` instead of a Pydantic model):
|
||||
In your editor, inside your function you will get type hints and completion everywhere (this wouldn't happen if you received a `dict` instead of a Pydantic model):
|
||||
|
||||
<img src="/img/tutorial/body/image03.png">
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
You can define Cookie parameters the same way you define `Query` and `Path` parameteres.
|
||||
You can define Cookie parameters the same way you define `Query` and `Path` parameters.
|
||||
|
||||
## Import `Cookie`
|
||||
|
||||
@@ -8,11 +8,11 @@ First import `Cookie`:
|
||||
{!./src/cookie_params/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Declare `Cookie` parameteres
|
||||
## Declare `Cookie` parameters
|
||||
|
||||
Then declare the cookie parameters using the same structure as with `Path` and `Query`.
|
||||
|
||||
The first value is the default value, you can pass all the extra validation or annotation parameteres:
|
||||
The first value is the default value, you can pass all the extra validation or annotation parameters:
|
||||
|
||||
```Python hl_lines="7"
|
||||
{!./src/cookie_params/tutorial001.py!}
|
||||
@@ -22,7 +22,7 @@ The first value is the default value, you can pass all the extra validation or a
|
||||
`Cookie` is a "sister" class of `Path` and `Query`. It also inherits from the same common `Param` class.
|
||||
|
||||
!!! info
|
||||
To declare cookies, you need to use `Cookie`, because otherwise the parameters would be interpreted as query parameteres.
|
||||
To declare cookies, you need to use `Cookie`, because otherwise the parameters would be interpreted as query parameters.
|
||||
|
||||
## Recap
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ Pass `HTMLResponse` as the parameter `content_type` of your path operation:
|
||||
And it will be documented as such in OpenAPI.
|
||||
|
||||
|
||||
### return a Starlette `Response`
|
||||
### Return a Starlette `Response`
|
||||
|
||||
You can also override the response directly in your path operation.
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ To do that, we declare a method `__call__`:
|
||||
{!./src/dependencies/tutorial006.py!}
|
||||
```
|
||||
|
||||
In this case, this `__call__` is what **FastAPI** will use to check for additional parameters and sub-dependencies, and this is what will be called to pass a value to the parameter in your *path operation function* later.
|
||||
|
||||
## Parameterize the instance
|
||||
|
||||
And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency:
|
||||
|
||||
@@ -10,7 +10,7 @@ In the previous example, we where returning a `dict` from our dependency ("depen
|
||||
|
||||
But then we get a `dict` in the parameter `commons` of the path operation function.
|
||||
|
||||
And we know that `dict`s can't provide a lot of editor support because they can't know their keys and value types.
|
||||
And we know that editors can't provide a lot of support (like completion) for `dict`s, because they can't know their keys and value types.
|
||||
|
||||
We can do better...
|
||||
|
||||
@@ -24,7 +24,7 @@ The key factor is that a dependency should be a "callable".
|
||||
|
||||
A "**callable**" in Python is anything that Python can "call" like a function.
|
||||
|
||||
So, if you have an object `something` (that might _not_ be a function) and you can do:
|
||||
So, if you have an object `something` (that might _not_ be a function) and you can "call" it (execute it) like:
|
||||
|
||||
```Python
|
||||
something()
|
||||
@@ -42,6 +42,21 @@ then it is a "callable".
|
||||
|
||||
You might notice that to create an instance of a Python class, you use that same syntax.
|
||||
|
||||
For example:
|
||||
|
||||
```Python
|
||||
class Cat:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
|
||||
fluffy = Cat(name="Mr Fluffy")
|
||||
```
|
||||
|
||||
In this case, `fluffy` is an instance of the class `Cat`.
|
||||
|
||||
And to create `fluffy`, you are "calling" `Cat`.
|
||||
|
||||
So, a Python class is also a **callable**.
|
||||
|
||||
Then, in **FastAPI**, you could use a Python class as a dependency.
|
||||
@@ -50,7 +65,7 @@ What FastAPI actually checks is that it is a "callable" (function, class or anyt
|
||||
|
||||
If you pass a "callable" as a dependency in **FastAPI**, it will analyze the parameters for that "callable", and process them in the same way as the parameters for a path operation function. Including sub-dependencies.
|
||||
|
||||
That also applies to callables with no parameters at all. The same as would be for path operation functions with no parameteres.
|
||||
That also applies to callables with no parameters at all. The same as it would be for path operation functions with no parameters.
|
||||
|
||||
Then, we can change the dependency "dependable" `common_parameters` from above to the class `CommonQueryParameters`:
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
Let's see a very simple example of the **Dependency Injection** system.
|
||||
|
||||
It will be so simple that it is not very useful, for now.
|
||||
|
||||
But this way we can focus on how the **Dependency Injection** system works.
|
||||
|
||||
In the next chapters we'll extend it to see how can it be so useful.
|
||||
|
||||
## Create a dependency, or "dependable"
|
||||
|
||||
Let's first focus on the dependency.
|
||||
|
||||
It is just a function that can take all the same parameters that a path operation function can take:
|
||||
|
||||
```Python hl_lines="6 7"
|
||||
{!./src/dependencies/tutorial001.py!}
|
||||
```
|
||||
|
||||
That's it.
|
||||
|
||||
**2 lines**.
|
||||
|
||||
And it has the same shape and structure that all your path operation functions.
|
||||
|
||||
You can think of it as a path operation function without the "decorator" (without the `@app.get("/some-path")`).
|
||||
|
||||
And it can return anything you want.
|
||||
|
||||
In this case, this dependency expects:
|
||||
|
||||
* An optional query parameter `q` that is a `str`.
|
||||
* An optional query parameter `skip` that is an `int`, and by default is `0`.
|
||||
* An optional query parameter `limit` that is an `int`, and by default is `100`.
|
||||
|
||||
And then it just returns a `dict` containing those values.
|
||||
|
||||
## Import `Depends`
|
||||
|
||||
```Python hl_lines="1"
|
||||
{!./src/dependencies/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Declare the dependency, in the "dependant"
|
||||
|
||||
The same way you use `Body`, `Query`, etc. with your path operation function parameters, use `Depends` with a new parameter:
|
||||
|
||||
```Python hl_lines="11"
|
||||
{!./src/dependencies/tutorial001.py!}
|
||||
```
|
||||
|
||||
Although you use it in the parameters of your function too, `Depends` works a bit differently.
|
||||
|
||||
You only give `Depends` a single parameter.
|
||||
|
||||
This parameter must be a function with the same parameters that can be taken by a path operation function.
|
||||
|
||||
Whenever a new request arrives, **FastAPI** will take care of:
|
||||
|
||||
* Calling your dependency ("dependable") function with the correct parameters.
|
||||
* Get the result from your function.
|
||||
* Assign that result to the parameter in your path operation function.
|
||||
|
||||
!!! note
|
||||
Notice that you don't have to create a special class and pass it somewhere to **FastAPI** or anything similar.
|
||||
|
||||
You just pass it to `Depends` and **FastAPI** knows how to do the rest.
|
||||
|
||||
## To `async` or not to `async`
|
||||
|
||||
As dependencies will also be called by **FastAPI** (the same as your path operation functions), the same rules apply while defining your functions.
|
||||
|
||||
You can use `async def` or normal `def`.
|
||||
|
||||
And you can declare dependencies with `async def` inside of normal `def` path operation functions, or `def` dependencies inside of `async def` path operation functions.
|
||||
|
||||
It doesn't matter. **FastAPI** will know what to do.
|
||||
|
||||
!!! note
|
||||
If you don't know, check the _"In a hurry?"_ section about <a href="https://fastapi.tiangolo.com/async/#in-a-hurry" target="_blank">`async` and `await` in the docs</a>.
|
||||
|
||||
## Integrated wiht OpenAPI
|
||||
|
||||
All the request declarations, validations and requirements of your dependencies (and sub-dependencies) will be integrated in the same OpenAPI schema.
|
||||
|
||||
So, the interactive docs will have all the information they need, while you keep all the flexibility of the dependencies:
|
||||
|
||||
<img src="/img/tutorial/dependencies/image01.png">
|
||||
|
||||
## Recap
|
||||
|
||||
Create Dependencies with **2 lines** of code.
|
||||
167
docs/tutorial/dependencies/first-steps.md
Normal file
167
docs/tutorial/dependencies/first-steps.md
Normal file
@@ -0,0 +1,167 @@
|
||||
**FastAPI** has a very powerful but intuitive **<abbr title="also known as components, resources, providers, services, injectables">Dependency Injection</abbr>** system.
|
||||
|
||||
It is designed to be very simple to use, and to make it very easy for any developer to integrate other components with **FastAPI**.
|
||||
|
||||
## "Dependency Injection"?
|
||||
|
||||
**"Dependency Injection"** means, in programming, that there is a way for your code (in this case, your path operation functions) to declare things that it requires to work and use: "dependencies".
|
||||
|
||||
And then, that system (in this case **FastAPI**) will take care of doing whatever is needed to provide your code with those needed dependencies ("inject" the dependencies).
|
||||
|
||||
This is very useful when you need to:
|
||||
|
||||
* Have shared logic (the same code logic again and again).
|
||||
* Share database connections.
|
||||
* Enforce security, authentication, role requirements, etc.
|
||||
* etc.
|
||||
|
||||
All these, while minimizing code repetition.
|
||||
|
||||
|
||||
## First Steps
|
||||
|
||||
Let's see a very simple example. It will be so simple that it is not very useful, for now.
|
||||
|
||||
But this way we can focus on how the **Dependency Injection** system works.
|
||||
|
||||
|
||||
### Create a dependency, or "dependable"
|
||||
|
||||
Let's first focus on the dependency.
|
||||
|
||||
It is just a function that can take all the same parameters that a path operation function can take:
|
||||
|
||||
```Python hl_lines="6 7"
|
||||
{!./src/dependencies/tutorial001.py!}
|
||||
```
|
||||
|
||||
That's it.
|
||||
|
||||
**2 lines**.
|
||||
|
||||
And it has the same shape and structure that all your path operation functions.
|
||||
|
||||
You can think of it as a path operation function without the "decorator" (without the `@app.get("/some-path")`).
|
||||
|
||||
And it can return anything you want.
|
||||
|
||||
In this case, this dependency expects:
|
||||
|
||||
* An optional query parameter `q` that is a `str`.
|
||||
* An optional query parameter `skip` that is an `int`, and by default is `0`.
|
||||
* An optional query parameter `limit` that is an `int`, and by default is `100`.
|
||||
|
||||
And then it just returns a `dict` containing those values.
|
||||
|
||||
### Import `Depends`
|
||||
|
||||
```Python hl_lines="1"
|
||||
{!./src/dependencies/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Declare the dependency, in the "dependant"
|
||||
|
||||
The same way you use `Body`, `Query`, etc. with your path operation function parameters, use `Depends` with a new parameter:
|
||||
|
||||
```Python hl_lines="11 16"
|
||||
{!./src/dependencies/tutorial001.py!}
|
||||
```
|
||||
|
||||
Although you use `Depends` in the parameters of your function the same way you use `Body`, `Query`, etc, `Depends` works a bit differently.
|
||||
|
||||
You only give `Depends` a single parameter.
|
||||
|
||||
This parameter must be something like a function.
|
||||
|
||||
And that function takes parameters in the same way that path operation functions do.
|
||||
|
||||
!!! tip
|
||||
You'll see what other "things", apart from functions, can be used as dependencies in the next chapter.
|
||||
|
||||
Whenever a new request arrives, **FastAPI** will take care of:
|
||||
|
||||
* Calling your dependency ("dependable") function with the correct parameters.
|
||||
* Get the result from your function.
|
||||
* Assign that result to the parameter in your path operation function.
|
||||
|
||||
!!! note
|
||||
Notice that you don't have to create a special class and pass it somewhere to **FastAPI** to "register" it or anything similar.
|
||||
|
||||
You just pass it to `Depends` and **FastAPI** knows how to do the rest.
|
||||
|
||||
## To `async` or not to `async`
|
||||
|
||||
As dependencies will also be called by **FastAPI** (the same as your path operation functions), the same rules apply while defining your functions.
|
||||
|
||||
You can use `async def` or normal `def`.
|
||||
|
||||
And you can declare dependencies with `async def` inside of normal `def` path operation functions, or `def` dependencies inside of `async def` path operation functions, etc.
|
||||
|
||||
It doesn't matter. **FastAPI** will know what to do.
|
||||
|
||||
!!! note
|
||||
If you don't know, check the _"In a hurry?"_ section about <a href="https://fastapi.tiangolo.com/async/#in-a-hurry" target="_blank">`async` and `await` in the docs</a>.
|
||||
|
||||
|
||||
## Integrated with OpenAPI
|
||||
|
||||
All the request declarations, validations and requirements of your dependencies (and sub-dependencies) will be integrated in the same OpenAPI schema.
|
||||
|
||||
So, the interactive docs will have all the information from these dependencies too:
|
||||
|
||||
<img src="/img/tutorial/dependencies/image01.png">
|
||||
|
||||
|
||||
## Simple usage
|
||||
|
||||
If you look at it, *path operation functions* are declared to be used whenever a *path* and *operation* matches, and then **FastAPI** takes care of calling the function with the correct parameters and use the response.
|
||||
|
||||
Actually, all (or most) of the web frameworks work in this same way.
|
||||
|
||||
You never call those functions directly. They are called by your framework (in this case, **FastAPI**).
|
||||
|
||||
With the Dependency Injection system, you can also tell **FastAPI** that your path operation function also "depends" on something else that should be executed before your *path operation function*, and **FastAPI** will take care of executing it and "injecting" the results.
|
||||
|
||||
Other common terms for this same idea of "dependency injection" are:
|
||||
|
||||
* resources
|
||||
* providers
|
||||
* services
|
||||
* injectables
|
||||
* components
|
||||
|
||||
## **FastAPI** plug-ins
|
||||
|
||||
Integrations and "plug-in"s can be built using the **Dependency Injection** system. But in fact, there is actually **no need to create "plug-ins"**, as by using dependencies it's possible to declare an infinite number of integrations and interactions that become available to your path operation functions.
|
||||
|
||||
And dependencies can be created in a very simple and intuitive way that allow you to just import the Python packages you need, and integrate them with your API functions in a couple of lines of code, _literally_.
|
||||
|
||||
You will see examples of this in the next chapters, about relational and NoSQL databases, security, etc.
|
||||
|
||||
## **FastAPI** compatibility
|
||||
|
||||
The simplicity of the dependency injection system makes **FastAPI** compatible with:
|
||||
|
||||
* all the relational databases
|
||||
* NoSQL databases
|
||||
* external packages
|
||||
* external APIs
|
||||
* authentication and authorization systems
|
||||
* API usage monitoring systems
|
||||
* response data injection systems
|
||||
* etc.
|
||||
|
||||
|
||||
## Simple and Powerful
|
||||
|
||||
Although the hierarchical dependency injection system is very simple to define and use, it's still very powerful.
|
||||
|
||||
You can define dependencies that in turn can define dependencies themselves.
|
||||
|
||||
In the end, a hierarchical tree of dependencies is built, and the **Dependency Injection** system takes care of solving all these dependencies for you (and your dependencies) and providing (injecting) the results at each step.
|
||||
|
||||
## Integrated with **OpenAPI**
|
||||
|
||||
All these dependencies, while declaring their requirements, add parameters, validations, etc. to your path operations.
|
||||
|
||||
**FastAPI** will take care of adding it all to the OpenAPI schema, so that it is shown in the interactive documentation systems.
|
||||
@@ -1,58 +0,0 @@
|
||||
**FastAPI** has a very powerful but intuitive **<abbr title="also known as components, resources, providers, services, injectables">Dependency Injection</abbr>** system.
|
||||
|
||||
It is designed to be very simple to use, and to make it very easy for any developer to integrate other components with **FastAPI**.
|
||||
|
||||
## "Dependency Injection"?
|
||||
|
||||
**"Dependency Injection"** means, in programming, that there is a way for your code (in this case, your path operation functions) to declare things that it requires to work and use.
|
||||
|
||||
And then, that system (in this case **FastAPI**) will take care of doing whatever is needed to provide your code with that thing that it needs.
|
||||
|
||||
If you look at it, path operation functions are declared to be used whenever a path and operation matches, and then **FastAPI** will take care of calling the function with the correct parameters and use the response.
|
||||
|
||||
Actually, all (or most) of the web frameworks work in this same way.
|
||||
|
||||
You never call those functions directly. The are called by your framework (in this case, **FastAPI**).
|
||||
|
||||
With the Dependency Injection system, you can also tell **FastAPI** that your path operation function also "depends" on something else that should be executed before your path operation function, and **FastAPI** will take care of executing it and "injecting" the results.
|
||||
|
||||
Other common terms for this same idea are:
|
||||
|
||||
* resources
|
||||
* providers
|
||||
* services
|
||||
* injectables
|
||||
|
||||
## **FastAPI** plug-ins
|
||||
|
||||
Integrations and "plug-in"s can be built using the **Dependency Injection** system. But in fact, there is actually **no need to create "plug-ins"**, as by using dependencies it's possible to declare an infinite number of integrations and interactions that become available to your path operation functions.
|
||||
|
||||
And dependencies can be created in a very simple and intuitive way that allow you to just import the Python packages you need, and integrate them with your API functions in a couple of lines of code, _literally_.
|
||||
|
||||
## **FastAPI** compatibility
|
||||
|
||||
The simplicity of the dependency injection system makes **FastAPI** compatible with:
|
||||
|
||||
* all the relational databases
|
||||
* NoSQL databases
|
||||
* external packages
|
||||
* external APIs
|
||||
* authentication and authorization systems
|
||||
* API usage monitoring systems
|
||||
* response data injection systems
|
||||
* etc.
|
||||
|
||||
|
||||
## Simple and Powerful
|
||||
|
||||
Although the hierarchical dependency injection system is very simple to define and use, it's still very powerful.
|
||||
|
||||
You can define dependencies that in turn can define dependencies themselves.
|
||||
|
||||
In the end, a hierarchical tree of dependencies is built, and the **Dependency Injection** system takes care of solving all these dependencies for you (and your dependencies) and providing the results at each step.
|
||||
|
||||
## Integrated with **OpenAPI**
|
||||
|
||||
All these dependencies, while declaring their requirements, might have been adding parameters, validations, etc. to your path operations.
|
||||
|
||||
**FastAPI** will take care of adding it all to the OpenAPI schema, so that it is shown in the interactive documentation systems.
|
||||
@@ -7,7 +7,9 @@ This is especially the case for user models, because:
|
||||
* The **database model** would probably need to have a hashed password.
|
||||
|
||||
!!! danger
|
||||
Never store user's plaintext passwords. Always store a secure hash that you can then verify.
|
||||
Never store user's plaintext passwords. Always store a "secure hash" that you can then verify.
|
||||
|
||||
If you don't know, you will learn what a "password hash" is in the <a href="/tutorial/security/simple-oauth2/#password-hashing" target="_blank">security chapters</a>.
|
||||
|
||||
## Multiple models
|
||||
|
||||
@@ -17,6 +19,116 @@ Here's a general idea of how the models could look like with their password fiel
|
||||
{!./src/extra_models/tutorial001.py!}
|
||||
```
|
||||
|
||||
### About `**user_in.dict()`
|
||||
|
||||
#### Pydantic's `.dict()`
|
||||
|
||||
`user_in` is a Pydantic model of class `UserIn`.
|
||||
|
||||
Pydantic models have a `.dict()` method that returns a `dict` with the model's data.
|
||||
|
||||
So, if we create a Pydantic object `user_in` like:
|
||||
|
||||
```Python
|
||||
user_in = UserIn(username="john", password="secret", email="john.doe@example.com")
|
||||
```
|
||||
|
||||
and then we call:
|
||||
|
||||
```Python
|
||||
user_dict = user_in.dict()
|
||||
```
|
||||
|
||||
we now have a `dict` with the data in the variable `user_dict` (it's a `dict` instead of a Pydantic model object).
|
||||
|
||||
And if we call:
|
||||
|
||||
```Python
|
||||
print(user_dict)
|
||||
```
|
||||
|
||||
we would get a Python `dict` with:
|
||||
|
||||
```Python
|
||||
{
|
||||
'username': 'john',
|
||||
'password': 'secret',
|
||||
'email': 'john.doe@example.com',
|
||||
'full_name': None,
|
||||
}
|
||||
```
|
||||
|
||||
#### Unwrapping a `dict`
|
||||
|
||||
If we take a `dict` like `user_dict` and pass it to a function (or class) with `**user_dict`, Python will "unwrap" it. It will pass the keys and values of the `user_dict` directly as key-value arguments.
|
||||
|
||||
So, continuing with the `user_dict` from above, writing:
|
||||
|
||||
```Python
|
||||
UserInDB(**user_dict)
|
||||
```
|
||||
|
||||
Would result in something equivalent to:
|
||||
|
||||
```Python
|
||||
UserInDB(
|
||||
username="john",
|
||||
password="secret",
|
||||
email="john.doe@example.com",
|
||||
full_name=None,
|
||||
)
|
||||
```
|
||||
|
||||
Or more exactly, using `user_dict` directly, with whatever contents it might have in the future:
|
||||
|
||||
```Python
|
||||
UserInDB(
|
||||
username = user_dict["username"],
|
||||
password = user_dict["password"],
|
||||
email = user_dict["email"],
|
||||
full_name = user_dict["full_name"],
|
||||
)
|
||||
```
|
||||
|
||||
#### A Pydantic model from the contents of another
|
||||
|
||||
As in the example above we got `user_dict` from `user_in.dict()`, this code:
|
||||
|
||||
```Python
|
||||
user_dict = user_in.dict()
|
||||
UserInDB(**user_dict)
|
||||
```
|
||||
|
||||
would be equivalent to:
|
||||
|
||||
```Python
|
||||
UserInDB(**user_in.dict())
|
||||
```
|
||||
|
||||
...because `user_in.dict()` is a `dict`, and then we make Python "unwrap" it by passing it to `UserInDB` prepended with `**`.
|
||||
|
||||
So, we get a Pydantic model from the data in another Pydantic model.
|
||||
|
||||
#### Unrapping a `dict` and extra keywords
|
||||
|
||||
And then adding the extra keyword argument `hashed_password=hashed_password`, like in:
|
||||
|
||||
```Python
|
||||
UserInDB(**user_in.dict(), hashed_password=hashed_password)
|
||||
```
|
||||
|
||||
...ends up being like:
|
||||
|
||||
```Python
|
||||
UserInDB(
|
||||
username = user_dict["username"],
|
||||
password = user_dict["password"],
|
||||
email = user_dict["email"],
|
||||
full_name = user_dict["full_name"],
|
||||
hashed_password = hashed_password,
|
||||
)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
The supporting additional functions are just to demo a possible flow of the data, but they of course are not providing any real security.
|
||||
|
||||
@@ -30,7 +142,7 @@ And these models are all sharing a lot of the data and duplicating attribute nam
|
||||
|
||||
We could do better.
|
||||
|
||||
We can declare a `Userbase` model that serves as a base for our other models. And then we can make subclasses of that model that inherit its attributes (type declarations, validation, etc).
|
||||
We can declare a `UserBase` model that serves as a base for our other models. And then we can make subclasses of that model that inherit its attributes (type declarations, validation, etc).
|
||||
|
||||
All the data conversion, validation, documentation, etc. will still work as normally.
|
||||
|
||||
@@ -42,4 +154,6 @@ That way, we can declare just the differences between the models (with plaintext
|
||||
|
||||
## Recap
|
||||
|
||||
Use multiple Pydantic models and inherit freely for each case. You don't need to have a single data model per entity if that entity must be able to have different "states". As the case with the user "entity" with a state including `password`, `password_hash` and no password.
|
||||
Use multiple Pydantic models and inherit freely for each case.
|
||||
|
||||
You don't need to have a single data model per entity if that entity must be able to have different "states". As the case with the user "entity" with a state including `password`, `password_hash` and no password.
|
||||
|
||||
@@ -57,6 +57,32 @@ You will see the alternative automatic documentation (provided by <a href="https
|
||||
|
||||

|
||||
|
||||
### OpenAPI
|
||||
|
||||
**FastAPI** generates a "schema" with all your API using the **OpenAPI** standard for defining APIs.
|
||||
|
||||
#### "Schema"
|
||||
|
||||
A "schema" is a definition or description of something. Not the code that implements it, but just the abstract description.
|
||||
|
||||
#### API "schema"
|
||||
|
||||
In this case, OpenAPI is a specification that dictates how to define a schema of your API.
|
||||
|
||||
This OpenAPI schema would include your API paths, the posible parameters they take, etc.
|
||||
|
||||
#### Data "schema"
|
||||
|
||||
The term "schema" might also refer to the shape of some data, like a JSON content.
|
||||
|
||||
In that case, it would mean the JSON attributes, and data types they have, etc.
|
||||
|
||||
#### OpenAPI and JSON Schema
|
||||
|
||||
OpenAPI defines an API schema for your API. And that schema includes definitions (or "schemas") of the data sent and received by your API using **JSON Schema**, the standard for JSON data schemas.
|
||||
|
||||
#### Check it
|
||||
|
||||
If you are curious about how the raw OpenAPI schema looks like, it is just an automatically generated JSON with the descriptions of all your API.
|
||||
|
||||
You can see it directly at: <a href="http://127.0.0.1:8000/openapi.json" target="_blank">http://127.0.0.1:8000/openapi.json</a>.
|
||||
@@ -84,6 +110,14 @@ It will show a JSON starting with something like:
|
||||
...
|
||||
```
|
||||
|
||||
#### What for?
|
||||
|
||||
This OpenAPI schema is what powers the 2 interactive documentation systems included.
|
||||
|
||||
And there are dozens of alternatives, all based on OpenAPI. You could easily add any of those alternatives to your application built with **FastAPI**.
|
||||
|
||||
You could also use it to generate code automatically, for clients that communicate with your API. For example, frontend, mobile or IoT applications.
|
||||
|
||||
## Recap, step by step
|
||||
|
||||
### Step 1: import `FastAPI`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
You can define Header parameters the same way you define `Query`, `Path` and `Cookie` parameteres.
|
||||
You can define Header parameters the same way you define `Query`, `Path` and `Cookie` parameters.
|
||||
|
||||
## Import `Header`
|
||||
|
||||
@@ -8,11 +8,11 @@ First import `Header`:
|
||||
{!./src/header_params/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Declare `Header` parameteres
|
||||
## Declare `Header` parameters
|
||||
|
||||
Then declare the header parameters using the same structure as with `Path`, `Query` and `Cookie`.
|
||||
|
||||
The first value is the default value, you can pass all the extra validation or annotation parameteres:
|
||||
The first value is the default value, you can pass all the extra validation or annotation parameters:
|
||||
|
||||
```Python hl_lines="7"
|
||||
{!./src/header_params/tutorial001.py!}
|
||||
@@ -22,7 +22,7 @@ The first value is the default value, you can pass all the extra validation or a
|
||||
`Header` is a "sister" class of `Path`, `Query` and `Cookie`. It also inherits from the same common `Param` class.
|
||||
|
||||
!!! info
|
||||
To declare headers, you need to use `Header`, because otherwise the parameters would be interpreted as query parameteres.
|
||||
To declare headers, you need to use `Header`, because otherwise the parameters would be interpreted as query parameters.
|
||||
|
||||
## Automatic conversion
|
||||
|
||||
@@ -49,6 +49,6 @@ If for some reason you need to disable automatic conversion of underscores to hy
|
||||
|
||||
## Recap
|
||||
|
||||
Declare headeres with `Header`, using the same common pattern as `Query`, `Path` and `Cookie`.
|
||||
Declare headers with `Header`, using the same common pattern as `Query`, `Path` and `Cookie`.
|
||||
|
||||
And don't worry about underscores in your variables, **FastAPI** will take care of converting them.
|
||||
|
||||
@@ -39,13 +39,13 @@ pip install fastapi[all]
|
||||
|
||||
This is what you would probably do once you want to deploy your application to production:
|
||||
|
||||
```bash
|
||||
```
|
||||
pip install fastapi
|
||||
```
|
||||
|
||||
Also install `uvicorn` to work as the server:
|
||||
|
||||
```bash
|
||||
```
|
||||
pip install uvicorn
|
||||
```
|
||||
|
||||
|
||||
@@ -130,6 +130,11 @@ You can add more information about the parameter.
|
||||
|
||||
That information will be included in the generated OpenAPI and used by the documentation user interfaces and external tools.
|
||||
|
||||
!!! note
|
||||
Have in mind that different tools might have different levels of OpenAPI support.
|
||||
|
||||
Some of them might not show all the extra information declared yet, although in most of the cases, the missing feature is already planned for development.
|
||||
|
||||
You can add a `title`:
|
||||
|
||||
```Python hl_lines="7"
|
||||
|
||||
@@ -129,7 +129,7 @@ When you declare a default value for non-path parameters (for now, we have only
|
||||
|
||||
If you don't want to add a specific value but just make it optional, set the default as `None`.
|
||||
|
||||
But when you want to make a query parameter required, you can just do not declare any default value:
|
||||
But when you want to make a query parameter required, you can just not declare any default value:
|
||||
|
||||
```Python hl_lines="6 7"
|
||||
{!./src/query_params/tutorial005.py!}
|
||||
|
||||
@@ -22,7 +22,7 @@ The files will be uploaded as form data and you will receive the contents as `by
|
||||
`File` is a class that inherits directly from `Form`.
|
||||
|
||||
!!! info
|
||||
To declare File bodies, you need to use `File`, because otherwise the parameters would be interpreted as query parameteres or body (JSON) parameters.
|
||||
To declare File bodies, you need to use `File`, because otherwise the parameters would be interpreted as query parameters or body (JSON) parameters.
|
||||
|
||||
## "Form Data"?
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ With `Form` you can declare the same metadata and validation as with `Body` (and
|
||||
`Form` is a class that inherits directly from `Body`.
|
||||
|
||||
!!! info
|
||||
To declare form bodies, you need to use `Form` explicitly, because without it the parameters would be interpreted as query parameteres or body (JSON) parameters.
|
||||
To declare form bodies, you need to use `Form` explicitly, because without it the parameters would be interpreted as query parameters or body (JSON) parameters.
|
||||
|
||||
## "Form Fields"?
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ Now, whenever a browser is creating a user with a password, the API will return
|
||||
|
||||
In this case, it might not be a problem, becase the user himself is sending the password.
|
||||
|
||||
But if we use sthe same model for another path operation, we could be sending the passwords of our users to every client.
|
||||
But if we use the same model for another path operation, we could be sending the passwords of our users to every client.
|
||||
|
||||
!!! danger
|
||||
Never send the plain password of a user in a response.
|
||||
|
||||
71
docs/tutorial/response-status-code.md
Normal file
71
docs/tutorial/response-status-code.md
Normal file
@@ -0,0 +1,71 @@
|
||||
The same way you can specify a response model, you can also declare the HTTP status code used for the response with the parameter `status_code` in any of the path operations:
|
||||
|
||||
* `@app.get()`
|
||||
* `@app.post()`
|
||||
* `@app.put()`
|
||||
* `@app.delete()`
|
||||
* etc.
|
||||
|
||||
```Python hl_lines="6"
|
||||
{!./src/response_status_code/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! note
|
||||
Notice that `status_code` is a parameter of the "decorator" method (`get`, `post`, etc). Not of your path operation function, like all the parameters and body.
|
||||
|
||||
The `status_code` parameter receives a number with the HTTP status code.
|
||||
|
||||
It will:
|
||||
|
||||
* Return that status code in the response.
|
||||
* Document it as such in the OpenAPI schema (and so, in the user interfaces):
|
||||
|
||||
<img src="/img/tutorial/response-status-code/image01.png">
|
||||
|
||||
|
||||
## About HTTP status codes
|
||||
|
||||
!!! note
|
||||
If you already know what HTTP status codes are, skip to the next section.
|
||||
|
||||
In HTTP, you send a numeric status code of 3 digits as part of the response.
|
||||
|
||||
These status codes have a name associated to recognize them, but the important part is the number.
|
||||
|
||||
In short:
|
||||
|
||||
* `100` and above are for "Information". You rarely use them directly.
|
||||
* **`200`** and above are for "Successful" responses. These are the ones you would use the most.
|
||||
* `200` is the default status code, which means everything was "OK".
|
||||
* Another example would be `201`, "Created". It is commonly used after creating a new record in the database.
|
||||
* `300` and above are for "Redirection".
|
||||
* **`400`** and above are for "Client error" responses. These are the second type you would probably use the most.
|
||||
* An example is `404`, for a "Not Found" response.
|
||||
* For generic errors from the client, you can just use `400`.
|
||||
* `500` and above are for server errors. You almost never use them directly. When something goes wrong at some part in your application code, or server, it will automatically return one of these status codes.
|
||||
|
||||
!!! tip
|
||||
To know more about each status code and which code is for what, check the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> documentation about HTTP status codes</a>.
|
||||
|
||||
|
||||
## Shortcut to remember the names
|
||||
|
||||
Let's see the previous example again:
|
||||
|
||||
```Python hl_lines="6"
|
||||
{!./src/response_status_code/tutorial001.py!}
|
||||
```
|
||||
|
||||
`201` is the status code for "Created".
|
||||
|
||||
But you don't have to memorize what each of these codes mean.
|
||||
|
||||
You can use the convenience variables from `starlette.status`.
|
||||
|
||||
```Python hl_lines="2 7"
|
||||
{!./src/response_status_code/tutorial002.py!}
|
||||
```
|
||||
|
||||
They are just a convenience, they hold the same number, but that way you can use the editor's autocomplete to find them:
|
||||
|
||||
<img src="/img/tutorial/response-status-code/image02.png">
|
||||
@@ -1,4 +1,4 @@
|
||||
There are many ways to handle security, authentication and autorization.
|
||||
There are many ways to handle security, authentication and authorization.
|
||||
|
||||
And it normally is a complex and "difficult" topic.
|
||||
|
||||
@@ -14,9 +14,9 @@ If you don't care about any of these terms and you just need to add security wit
|
||||
|
||||
## OAuth2
|
||||
|
||||
OAuth2 is a specification that defines several ways to handle authentication and autorization.
|
||||
OAuth2 is a specification that defines several ways to handle authentication and authorization.
|
||||
|
||||
It is quite an extensive especification and covers several complex use cases.
|
||||
It is quite an extensive specification and covers several complex use cases.
|
||||
|
||||
It includes ways to authenticate using a "third party".
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ The recommended algorithm is "Bcrypt".
|
||||
|
||||
So, install PassLib with Bcrypt:
|
||||
|
||||
```Python
|
||||
```bash
|
||||
pip install passlib[bcrypt]
|
||||
```
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ Now let's use the utilities provided by **FastAPI** to handle this.
|
||||
|
||||
First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depends` for the path `/token`:
|
||||
|
||||
```Python hl_lines="2 66"
|
||||
```Python hl_lines="2 73"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -80,7 +80,7 @@ If there is no such user, we return an error saying "incorrect username or passw
|
||||
|
||||
For the error, we use the exception `HTTPException` provided by Starlette directly:
|
||||
|
||||
```Python hl_lines="4 67 68 69"
|
||||
```Python hl_lines="4 74 75 76"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -94,7 +94,21 @@ You should never save plaintext passwords, so, we'll use the (fake) password has
|
||||
|
||||
If the passwords don't match, we return the same error.
|
||||
|
||||
```Python hl_lines="70 71 72 73"
|
||||
#### Password hashing
|
||||
|
||||
"Hashing" means: converting some content (a password in this case) into a sequence of bytes (just a string) that look like gibberish.
|
||||
|
||||
Whenever you pass exactly the same content (exactly the same password) you get exactly the same gibberish.
|
||||
|
||||
But you cannot convert from the gibberish back to the password.
|
||||
|
||||
##### What for?
|
||||
|
||||
If your database is stolen, the thief won't have your users' plaintext passwords, only the hashes.
|
||||
|
||||
So, the thief won't be able to try to use that password in another system (as many users use the same password everywhere, this would be dangerous).
|
||||
|
||||
```Python hl_lines="77 78 79 80"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -114,6 +128,9 @@ UserInDB(
|
||||
)
|
||||
```
|
||||
|
||||
!!! info
|
||||
For a more complete explanation of `**user_dict` check back in <a href="/tutorial/extra-models/#about-user_indict" target="_blank">the documentation for **Extra Models**</a>.
|
||||
|
||||
## Return the token
|
||||
|
||||
The response of the `token` endpoint must be a JSON object.
|
||||
@@ -129,7 +146,7 @@ For this simple example, we are going to just be completely insecure and return
|
||||
|
||||
But for now, let's focus on the specific details we need.
|
||||
|
||||
```Python hl_lines="75"
|
||||
```Python hl_lines="82"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -145,7 +162,7 @@ Both of these dependencies will just return an HTTP error if the user doesn't ex
|
||||
|
||||
So, in our endpoint, we will only get a user if the user exists, was correctly authenticated, and is active:
|
||||
|
||||
```Python hl_lines="50 51 52 53 54 55 56 59 60 61 62 79"
|
||||
```Python hl_lines="57 58 59 60 61 62 63 66 67 68 69 86"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -160,6 +177,7 @@ Click the "Authorize" button.
|
||||
Use the credentials:
|
||||
|
||||
User: `johndoe`
|
||||
|
||||
Password: `secret`
|
||||
|
||||
<img src="/img/tutorial/security/image04.png">
|
||||
@@ -194,6 +212,24 @@ If you click the lock icon and logout, and then try the same operation again, yo
|
||||
}
|
||||
```
|
||||
|
||||
### Inactive user
|
||||
|
||||
Now try with an inactive user, authenticate with:
|
||||
|
||||
User: `alice`
|
||||
|
||||
Password: `secret2`
|
||||
|
||||
And try to use the operation `GET` with the path `/users/me`.
|
||||
|
||||
You will get an "inactive user" error, like:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"detail": "Inactive user"
|
||||
}
|
||||
```
|
||||
|
||||
## Recap
|
||||
|
||||
You now have the tools to implement a complete security system based on `username` and `password` for your API.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.1.14"
|
||||
__version__ = "0.2.0"
|
||||
|
||||
from .applications import FastAPI
|
||||
from .routing import APIRouter
|
||||
|
||||
@@ -3,21 +3,21 @@ import inspect
|
||||
from copy import deepcopy
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import Any, Callable, Dict, List, Mapping, Sequence, Tuple, Type
|
||||
from typing import Any, Callable, Dict, List, Mapping, Sequence, Tuple, Type, Union
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import params
|
||||
from fastapi.dependencies.models import Dependant, SecurityRequirement
|
||||
from fastapi.security.base import SecurityBase
|
||||
from fastapi.utils import get_path_param_names
|
||||
from pydantic import BaseConfig, Schema, create_model
|
||||
from fastapi.utils import UnconstrainedConfig, get_path_param_names
|
||||
from pydantic import Schema, create_model
|
||||
from pydantic.error_wrappers import ErrorWrapper
|
||||
from pydantic.errors import MissingError
|
||||
from pydantic.fields import Field, Required
|
||||
from pydantic.fields import Field, Required, Shape
|
||||
from pydantic.schema import get_annotation_from_schema
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.requests import Request
|
||||
from starlette.requests import Headers, QueryParams, Request
|
||||
|
||||
param_supported_types = (
|
||||
str,
|
||||
@@ -58,8 +58,6 @@ def get_flat_dependant(dependant: Dependant) -> Dependant:
|
||||
security_schemes=dependant.security_requirements.copy(),
|
||||
)
|
||||
for sub_dependant in dependant.dependencies:
|
||||
if sub_dependant is dependant:
|
||||
raise ValueError("recursion", dependant.dependencies)
|
||||
flat_sub = get_flat_dependant(sub_dependant)
|
||||
flat_dependant.path_params.extend(flat_sub.path_params)
|
||||
flat_dependant.query_params.extend(flat_sub.query_params)
|
||||
@@ -99,7 +97,7 @@ def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant:
|
||||
elif (
|
||||
param.default == param.empty
|
||||
or param.default is None
|
||||
or type(param.default) in param_supported_types
|
||||
or isinstance(param.default, param_supported_types)
|
||||
) and (
|
||||
param.annotation == param.empty
|
||||
or lenient_issubclass(param.annotation, param_supported_types)
|
||||
@@ -109,9 +107,18 @@ def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant:
|
||||
)
|
||||
elif isinstance(param.default, params.Param):
|
||||
if param.annotation != param.empty:
|
||||
assert lenient_issubclass(
|
||||
param.annotation, param_supported_types
|
||||
), f"Parameters for Path, Query, Header and Cookies must be of type str, int, float or bool: {param}"
|
||||
origin = getattr(param.annotation, "__origin__", None)
|
||||
param_all_types = param_supported_types + (list, tuple, set)
|
||||
if isinstance(param.default, (params.Query, params.Header)):
|
||||
assert lenient_issubclass(
|
||||
param.annotation, param_all_types
|
||||
) or lenient_issubclass(
|
||||
origin, param_all_types
|
||||
), f"Parameters for Query and Header must be of type str, int, float, bool, list, tuple or set: {param}"
|
||||
else:
|
||||
assert lenient_issubclass(
|
||||
param.annotation, param_supported_types
|
||||
), f"Parameters for Path and Cookies must be of type str, int, float, bool: {param}"
|
||||
add_param_to_fields(
|
||||
param=param, dependant=dependant, default_schema=params.Query
|
||||
)
|
||||
@@ -156,7 +163,7 @@ def add_param_to_fields(
|
||||
default=None if required else default_value,
|
||||
alias=alias,
|
||||
required=required,
|
||||
model_config=BaseConfig(),
|
||||
model_config=UnconstrainedConfig,
|
||||
class_validators=[],
|
||||
schema=schema,
|
||||
)
|
||||
@@ -190,23 +197,19 @@ def add_param_to_body_fields(*, param: inspect.Parameter, dependant: Dependant)
|
||||
default=None if required else default_value,
|
||||
alias=schema.alias or param.name,
|
||||
required=required,
|
||||
model_config=BaseConfig,
|
||||
model_config=UnconstrainedConfig,
|
||||
class_validators=[],
|
||||
schema=schema,
|
||||
)
|
||||
dependant.body_params.append(field)
|
||||
|
||||
|
||||
def is_coroutine_callable(call: Callable = None) -> bool:
|
||||
if not call:
|
||||
return False
|
||||
def is_coroutine_callable(call: Callable) -> bool:
|
||||
if inspect.isfunction(call):
|
||||
return asyncio.iscoroutinefunction(call)
|
||||
if inspect.isclass(call):
|
||||
return False
|
||||
call = getattr(call, "__call__", None)
|
||||
if not call:
|
||||
return False
|
||||
return asyncio.iscoroutinefunction(call)
|
||||
|
||||
|
||||
@@ -220,7 +223,8 @@ async def solve_dependencies(
|
||||
request=request, dependant=sub_dependant, body=body
|
||||
)
|
||||
if sub_errors:
|
||||
return {}, errors
|
||||
errors.extend(sub_errors)
|
||||
continue
|
||||
assert sub_dependant.call is not None, "sub_dependant.call must be a function"
|
||||
if is_coroutine_callable(sub_dependant.call):
|
||||
solved = await sub_dependant.call(**sub_values)
|
||||
@@ -244,7 +248,7 @@ async def solve_dependencies(
|
||||
values.update(query_values)
|
||||
values.update(header_values)
|
||||
values.update(cookie_values)
|
||||
errors = path_errors + query_errors + header_errors + cookie_errors
|
||||
errors += path_errors + query_errors + header_errors + cookie_errors
|
||||
if dependant.body_params:
|
||||
body_values, body_errors = await request_body_to_args( # type: ignore # body_params checked above
|
||||
dependant.body_params, body
|
||||
@@ -257,12 +261,18 @@ async def solve_dependencies(
|
||||
|
||||
|
||||
def request_params_to_args(
|
||||
required_params: Sequence[Field], received_params: Mapping[str, Any]
|
||||
required_params: Sequence[Field],
|
||||
received_params: Union[Mapping[str, Any], QueryParams, Headers],
|
||||
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
|
||||
values = {}
|
||||
errors = []
|
||||
for field in required_params:
|
||||
value = received_params.get(field.alias)
|
||||
if field.shape in {Shape.LIST, Shape.SET, Shape.TUPLE} and isinstance(
|
||||
received_params, (QueryParams, Headers)
|
||||
):
|
||||
value = received_params.getlist(field.alias)
|
||||
else:
|
||||
value = received_params.get(field.alias)
|
||||
schema: params.Param = field.schema
|
||||
assert isinstance(schema, params.Param), "Params must be subclasses of Param"
|
||||
if value is None:
|
||||
@@ -271,7 +281,7 @@ def request_params_to_args(
|
||||
ErrorWrapper(
|
||||
MissingError(),
|
||||
loc=(schema.in_.value, field.alias),
|
||||
config=BaseConfig,
|
||||
config=UnconstrainedConfig,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -301,11 +311,13 @@ async def request_body_to_args(
|
||||
received_body = {}
|
||||
for field in required_params:
|
||||
value = received_body.get(field.alias)
|
||||
if value is None:
|
||||
if value is None or (isinstance(field.schema, params.Form) and value == ""):
|
||||
if field.required:
|
||||
errors.append(
|
||||
ErrorWrapper(
|
||||
MissingError(), loc=("body", field.alias), config=BaseConfig
|
||||
MissingError(),
|
||||
loc=("body", field.alias),
|
||||
config=UnconstrainedConfig,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -346,7 +358,7 @@ def get_body_field(*, dependant: Dependant, name: str) -> Field:
|
||||
type_=BodyModel,
|
||||
default=None,
|
||||
required=required,
|
||||
model_config=BaseConfig,
|
||||
model_config=UnconstrainedConfig,
|
||||
class_validators=[],
|
||||
alias="body",
|
||||
schema=BodySchema(None),
|
||||
|
||||
@@ -12,12 +12,20 @@ def jsonable_encoder(
|
||||
exclude: Set[str] = set(),
|
||||
by_alias: bool = False,
|
||||
include_none: bool = True,
|
||||
custom_encoder: dict = {},
|
||||
) -> Any:
|
||||
if isinstance(obj, BaseModel):
|
||||
return jsonable_encoder(
|
||||
obj.dict(include=include, exclude=exclude, by_alias=by_alias),
|
||||
include_none=include_none,
|
||||
)
|
||||
if not obj.Config.json_encoders:
|
||||
return jsonable_encoder(
|
||||
obj.dict(include=include, exclude=exclude, by_alias=by_alias),
|
||||
include_none=include_none,
|
||||
)
|
||||
else:
|
||||
return jsonable_encoder(
|
||||
obj.dict(include=include, exclude=exclude, by_alias=by_alias),
|
||||
include_none=include_none,
|
||||
custom_encoder=obj.Config.json_encoders,
|
||||
)
|
||||
if isinstance(obj, Enum):
|
||||
return obj.value
|
||||
if isinstance(obj, (str, int, float, type(None))):
|
||||
@@ -25,8 +33,16 @@ def jsonable_encoder(
|
||||
if isinstance(obj, dict):
|
||||
return {
|
||||
jsonable_encoder(
|
||||
key, by_alias=by_alias, include_none=include_none
|
||||
): jsonable_encoder(value, by_alias=by_alias, include_none=include_none)
|
||||
key,
|
||||
by_alias=by_alias,
|
||||
include_none=include_none,
|
||||
custom_encoder=custom_encoder,
|
||||
): jsonable_encoder(
|
||||
value,
|
||||
by_alias=by_alias,
|
||||
include_none=include_none,
|
||||
custom_encoder=custom_encoder,
|
||||
)
|
||||
for key, value in obj.items()
|
||||
if value is not None or include_none
|
||||
}
|
||||
@@ -38,12 +54,16 @@ def jsonable_encoder(
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
include_none=include_none,
|
||||
custom_encoder=custom_encoder,
|
||||
)
|
||||
for item in obj
|
||||
]
|
||||
errors = []
|
||||
try:
|
||||
encoder = ENCODERS_BY_TYPE[type(obj)]
|
||||
if custom_encoder and type(obj) in custom_encoder:
|
||||
encoder = custom_encoder[type(obj)]
|
||||
else:
|
||||
encoder = ENCODERS_BY_TYPE[type(obj)]
|
||||
return encoder(obj)
|
||||
except KeyError as e:
|
||||
errors.append(e)
|
||||
|
||||
@@ -322,7 +322,7 @@ class OpenIdConnect(SecurityBase):
|
||||
openIdConnectUrl: str
|
||||
|
||||
|
||||
SecurityScheme = Union[APIKey, HTTPBase, HTTPBearer, OAuth2, OpenIdConnect]
|
||||
SecurityScheme = Union[APIKey, HTTPBase, OAuth2, OpenIdConnect, HTTPBearer]
|
||||
|
||||
|
||||
class Components(BaseModel):
|
||||
|
||||
@@ -147,61 +147,65 @@ def get_openapi_path(
|
||||
security_schemes: Dict[str, Any] = {}
|
||||
definitions: Dict[str, Any] = {}
|
||||
assert route.methods is not None, "Methods must be a list"
|
||||
for method in route.methods:
|
||||
operation = get_openapi_operation_metadata(route=route, method=method)
|
||||
parameters: List[Dict] = []
|
||||
flat_dependant = get_flat_dependant(route.dependant)
|
||||
security_definitions, operation_security = get_openapi_security_definitions(
|
||||
flat_dependant=flat_dependant
|
||||
)
|
||||
if operation_security:
|
||||
operation.setdefault("security", []).extend(operation_security)
|
||||
if security_definitions:
|
||||
security_schemes.update(security_definitions)
|
||||
all_route_params = get_openapi_params(route.dependant)
|
||||
validation_definitions, operation_parameters = get_openapi_operation_parameters(
|
||||
all_route_params=all_route_params
|
||||
)
|
||||
definitions.update(validation_definitions)
|
||||
parameters.extend(operation_parameters)
|
||||
if parameters:
|
||||
operation["parameters"] = parameters
|
||||
if method in METHODS_WITH_BODY:
|
||||
request_body_oai = get_openapi_operation_request_body(
|
||||
body_field=route.body_field, model_name_map=model_name_map
|
||||
if route.include_in_schema:
|
||||
for method in route.methods:
|
||||
operation = get_openapi_operation_metadata(route=route, method=method)
|
||||
parameters: List[Dict] = []
|
||||
flat_dependant = get_flat_dependant(route.dependant)
|
||||
security_definitions, operation_security = get_openapi_security_definitions(
|
||||
flat_dependant=flat_dependant
|
||||
)
|
||||
if request_body_oai:
|
||||
operation["requestBody"] = request_body_oai
|
||||
if "ValidationError" not in definitions:
|
||||
definitions["ValidationError"] = validation_error_definition
|
||||
definitions[
|
||||
"HTTPValidationError"
|
||||
] = validation_error_response_definition
|
||||
status_code = str(route.status_code)
|
||||
response_schema = {"type": "string"}
|
||||
if lenient_issubclass(route.content_type, JSONResponse):
|
||||
if route.response_field:
|
||||
response_schema, _ = field_schema(
|
||||
route.response_field,
|
||||
model_name_map=model_name_map,
|
||||
ref_prefix=REF_PREFIX,
|
||||
if operation_security:
|
||||
operation.setdefault("security", []).extend(operation_security)
|
||||
if security_definitions:
|
||||
security_schemes.update(security_definitions)
|
||||
all_route_params = get_openapi_params(route.dependant)
|
||||
validation_definitions, operation_parameters = get_openapi_operation_parameters(
|
||||
all_route_params=all_route_params
|
||||
)
|
||||
definitions.update(validation_definitions)
|
||||
parameters.extend(operation_parameters)
|
||||
if parameters:
|
||||
operation["parameters"] = parameters
|
||||
if method in METHODS_WITH_BODY:
|
||||
request_body_oai = get_openapi_operation_request_body(
|
||||
body_field=route.body_field, model_name_map=model_name_map
|
||||
)
|
||||
else:
|
||||
response_schema = {}
|
||||
content = {route.content_type.media_type: {"schema": response_schema}}
|
||||
operation["responses"] = {
|
||||
status_code: {"description": route.response_description, "content": content}
|
||||
}
|
||||
if all_route_params or route.body_field:
|
||||
operation["responses"][str(HTTP_422_UNPROCESSABLE_ENTITY)] = {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": REF_PREFIX + "HTTPValidationError"}
|
||||
}
|
||||
},
|
||||
if request_body_oai:
|
||||
operation["requestBody"] = request_body_oai
|
||||
if "ValidationError" not in definitions:
|
||||
definitions["ValidationError"] = validation_error_definition
|
||||
definitions[
|
||||
"HTTPValidationError"
|
||||
] = validation_error_response_definition
|
||||
status_code = str(route.status_code)
|
||||
response_schema = {"type": "string"}
|
||||
if lenient_issubclass(route.content_type, JSONResponse):
|
||||
if route.response_field:
|
||||
response_schema, _ = field_schema(
|
||||
route.response_field,
|
||||
model_name_map=model_name_map,
|
||||
ref_prefix=REF_PREFIX,
|
||||
)
|
||||
else:
|
||||
response_schema = {}
|
||||
content = {route.content_type.media_type: {"schema": response_schema}}
|
||||
operation["responses"] = {
|
||||
status_code: {
|
||||
"description": route.response_description,
|
||||
"content": content,
|
||||
}
|
||||
}
|
||||
path[method.lower()] = operation
|
||||
if all_route_params or route.body_field:
|
||||
operation["responses"][str(HTTP_422_UNPROCESSABLE_ENTITY)] = {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": REF_PREFIX + "HTTPValidationError"}
|
||||
}
|
||||
},
|
||||
}
|
||||
path[method.lower()] = operation
|
||||
return path, security_schemes, definitions
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ from fastapi import params
|
||||
from fastapi.dependencies.models import Dependant
|
||||
from fastapi.dependencies.utils import get_body_field, get_dependant, solve_dependencies
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseConfig, BaseModel, Schema
|
||||
from fastapi.utils import UnconstrainedConfig
|
||||
from pydantic import BaseModel, Schema
|
||||
from pydantic.error_wrappers import ErrorWrapper, ValidationError
|
||||
from pydantic.fields import Field
|
||||
from pydantic.utils import lenient_issubclass
|
||||
@@ -17,7 +18,7 @@ from starlette.exceptions import HTTPException
|
||||
from starlette.formparsers import UploadFile
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, Response
|
||||
from starlette.routing import get_name, request_response
|
||||
from starlette.routing import compile_path, get_name, request_response
|
||||
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
|
||||
@@ -130,7 +131,7 @@ class APIRoute(routing.Route):
|
||||
class_validators=[],
|
||||
default=None,
|
||||
required=False,
|
||||
model_config=BaseConfig(),
|
||||
model_config=UnconstrainedConfig,
|
||||
schema=Schema(None),
|
||||
)
|
||||
else:
|
||||
@@ -148,9 +149,7 @@ class APIRoute(routing.Route):
|
||||
self.include_in_schema = include_in_schema
|
||||
self.content_type = content_type
|
||||
|
||||
self.path_regex, self.path_format, self.param_convertors = self.compile_path(
|
||||
path
|
||||
)
|
||||
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
|
||||
assert inspect.isfunction(endpoint) or inspect.ismethod(
|
||||
endpoint
|
||||
), f"An endpoint must be a function or method"
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
from .api_key import APIKeyQuery, APIKeyHeader, APIKeyCookie
|
||||
from .http import HTTPBasic, HTTPBearer, HTTPDigest
|
||||
from .http import (
|
||||
HTTPBasic,
|
||||
HTTPBearer,
|
||||
HTTPDigest,
|
||||
HTTPBasicCredentials,
|
||||
HTTPAuthorizationCredentials,
|
||||
)
|
||||
from .oauth2 import OAuth2PasswordRequestForm, OAuth2, OAuth2PasswordBearer
|
||||
from .open_id_connect_url import OpenIdConnect
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from fastapi.openapi.models import APIKey, APIKeyIn
|
||||
from fastapi.security.base import SecurityBase
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
class APIKeyBase(SecurityBase):
|
||||
@@ -9,26 +11,41 @@ class APIKeyBase(SecurityBase):
|
||||
|
||||
class APIKeyQuery(APIKeyBase):
|
||||
def __init__(self, *, name: str, scheme_name: str = None):
|
||||
self.model = APIKey(in_=APIKeyIn.query, name=name)
|
||||
self.model = APIKey(**{"in": APIKeyIn.query}, name=name)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, requests: Request) -> str:
|
||||
return requests.query_params.get(self.model.name)
|
||||
async def __call__(self, request: Request) -> str:
|
||||
api_key: str = request.query_params.get(self.model.name)
|
||||
if not api_key:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
return api_key
|
||||
|
||||
|
||||
class APIKeyHeader(APIKeyBase):
|
||||
def __init__(self, *, name: str, scheme_name: str = None):
|
||||
self.model = APIKey(in_=APIKeyIn.header, name=name)
|
||||
self.model = APIKey(**{"in": APIKeyIn.header}, name=name)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, requests: Request) -> str:
|
||||
return requests.headers.get(self.model.name)
|
||||
async def __call__(self, request: Request) -> str:
|
||||
api_key: str = request.headers.get(self.model.name)
|
||||
if not api_key:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
return api_key
|
||||
|
||||
|
||||
class APIKeyCookie(APIKeyBase):
|
||||
def __init__(self, *, name: str, scheme_name: str = None):
|
||||
self.model = APIKey(in_=APIKeyIn.cookie, name=name)
|
||||
self.model = APIKey(**{"in": APIKeyIn.cookie}, name=name)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, requests: Request) -> str:
|
||||
return requests.cookies.get(self.model.name)
|
||||
async def __call__(self, request: Request) -> str:
|
||||
api_key: str = request.cookies.get(self.model.name)
|
||||
if not api_key:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
return api_key
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import binascii
|
||||
from base64 import b64decode
|
||||
|
||||
from fastapi.openapi.models import (
|
||||
HTTPBase as HTTPBaseModel,
|
||||
HTTPBearer as HTTPBearerModel,
|
||||
)
|
||||
from fastapi.security.base import SecurityBase
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from pydantic import BaseModel
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
class HTTPBasicCredentials(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class HTTPAuthorizationCredentials(BaseModel):
|
||||
scheme: str
|
||||
credentials: str
|
||||
|
||||
|
||||
class HTTPBase(SecurityBase):
|
||||
@@ -12,16 +29,41 @@ class HTTPBase(SecurityBase):
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
return request.headers.get("Authorization")
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||
if not (authorization and scheme and credentials):
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
|
||||
|
||||
class HTTPBasic(HTTPBase):
|
||||
def __init__(self, *, scheme_name: str = None):
|
||||
def __init__(self, *, scheme_name: str = None, realm: str = None):
|
||||
self.model = HTTPBaseModel(scheme="basic")
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.realm = realm
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
return request.headers.get("Authorization")
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
# before implementing headers with 401 errors, wait for: https://github.com/encode/starlette/issues/295
|
||||
# unauthorized_headers = {"WWW-Authenticate": "Basic"}
|
||||
invalid_user_credentials_exc = HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Invalid authentication credentials"
|
||||
)
|
||||
if not authorization or scheme.lower() != "basic":
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
try:
|
||||
data = b64decode(param).decode("ascii")
|
||||
except (ValueError, UnicodeDecodeError, binascii.Error):
|
||||
raise invalid_user_credentials_exc
|
||||
username, separator, password = data.partition(":")
|
||||
if not (separator):
|
||||
raise invalid_user_credentials_exc
|
||||
return HTTPBasicCredentials(username=username, password=password)
|
||||
|
||||
|
||||
class HTTPBearer(HTTPBase):
|
||||
@@ -30,7 +72,18 @@ class HTTPBearer(HTTPBase):
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
return request.headers.get("Authorization")
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||
if not (authorization and scheme and credentials):
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
if scheme.lower() != "bearer":
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
detail="Invalid authentication credentials",
|
||||
)
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
|
||||
|
||||
class HTTPDigest(HTTPBase):
|
||||
@@ -39,4 +92,15 @@ class HTTPDigest(HTTPBase):
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
return request.headers.get("Authorization")
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||
if not (authorization and scheme and credentials):
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
if scheme.lower() != "digest":
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
detail="Invalid authentication credentials",
|
||||
)
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Optional
|
||||
from fastapi.openapi.models import OAuth2 as OAuth2Model, OAuthFlows as OAuthFlowsModel
|
||||
from fastapi.params import Form
|
||||
from fastapi.security.base import SecurityBase
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
@@ -118,7 +119,12 @@ class OAuth2(SecurityBase):
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
return request.headers.get("Authorization")
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
if not authorization:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
return authorization
|
||||
|
||||
|
||||
class OAuth2PasswordBearer(OAuth2):
|
||||
@@ -130,9 +136,9 @@ class OAuth2PasswordBearer(OAuth2):
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
if not authorization or "Bearer " not in authorization:
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "bearer":
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
token = authorization.replace("Bearer ", "")
|
||||
return token
|
||||
return param
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel
|
||||
from fastapi.security.base import SecurityBase
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
class OpenIdConnect(SecurityBase):
|
||||
@@ -9,4 +11,9 @@ class OpenIdConnect(SecurityBase):
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
async def __call__(self, request: Request) -> str:
|
||||
return request.headers.get("Authorization")
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
if not authorization:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
return authorization
|
||||
|
||||
8
fastapi/security/utils.py
Normal file
8
fastapi/security/utils.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
def get_authorization_scheme_param(authorization_header_value: str) -> Tuple[str, str]:
|
||||
if not authorization_header_value:
|
||||
return "", ""
|
||||
scheme, _, param = authorization_header_value.partition(" ")
|
||||
return scheme, param
|
||||
@@ -3,12 +3,17 @@ from typing import Any, Dict, List, Sequence, Set, Type
|
||||
|
||||
from fastapi import routing
|
||||
from fastapi.openapi.constants import REF_PREFIX
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseConfig, BaseModel
|
||||
from pydantic.fields import Field
|
||||
from pydantic.schema import get_flat_models_from_fields, model_process_schema
|
||||
from starlette.routing import BaseRoute
|
||||
|
||||
|
||||
class UnconstrainedConfig(BaseConfig):
|
||||
min_anystr_length = None
|
||||
max_anystr_length = None
|
||||
|
||||
|
||||
def get_flat_models_from_routes(
|
||||
routes: Sequence[Type[BaseRoute]]
|
||||
) -> Set[Type[BaseModel]]:
|
||||
|
||||
16
mkdocs.yml
16
mkdocs.yml
@@ -11,7 +11,10 @@ theme:
|
||||
|
||||
repo_name: tiangolo/fastapi
|
||||
repo_url: https://github.com/tiangolo/fastapi
|
||||
edit_uri: ""
|
||||
edit_uri: ''
|
||||
google_analytics:
|
||||
- 'UA-133183413-1'
|
||||
- 'auto'
|
||||
|
||||
nav:
|
||||
- FastAPI: 'index.md'
|
||||
@@ -23,8 +26,8 @@ nav:
|
||||
- Path Parameters: 'tutorial/path-params.md'
|
||||
- Query Parameters: 'tutorial/query-params.md'
|
||||
- Request Body: 'tutorial/body.md'
|
||||
- Query Parameters - String Validations: 'tutorial/query-params-str-validations.md'
|
||||
- Path Parameters - Numeric Validations: 'tutorial/path-params-numeric-validations.md'
|
||||
- Query Parameters and String Validations: 'tutorial/query-params-str-validations.md'
|
||||
- Path Parameters and Numeric Validations: 'tutorial/path-params-numeric-validations.md'
|
||||
- Body - Multiple Parameters: 'tutorial/body-multiple-params.md'
|
||||
- Body - Schema: 'tutorial/body-schema.md'
|
||||
- Body - Nested Models: 'tutorial/body-nested-models.md'
|
||||
@@ -33,6 +36,7 @@ nav:
|
||||
- Header Parameters: 'tutorial/header-params.md'
|
||||
- Response Model: 'tutorial/response-model.md'
|
||||
- Extra Models: 'tutorial/extra-models.md'
|
||||
- Response Status Code: 'tutorial/response-status-code.md'
|
||||
- Form Data: 'tutorial/request-forms.md'
|
||||
- Request Files: 'tutorial/request-files.md'
|
||||
- Request Forms and Files: 'tutorial/request-forms-and-files.md'
|
||||
@@ -40,8 +44,7 @@ nav:
|
||||
- Path Operation Advanced Configuration: 'tutorial/path-operation-advanced-configuration.md'
|
||||
- Custom Response: 'tutorial/custom-response.md'
|
||||
- Dependencies:
|
||||
- Dependencies Intro: 'tutorial/dependencies/intro.md'
|
||||
- First Steps - Functions: 'tutorial/dependencies/first-steps-functions.md'
|
||||
- First Steps: 'tutorial/dependencies/first-steps.md'
|
||||
- Classes as Dependencies: 'tutorial/dependencies/classes-as-dependencies.md'
|
||||
- Sub-dependencies: 'tutorial/dependencies/sub-dependencies.md'
|
||||
- Advanced Dependencies: 'tutorial/dependencies/advanced-dependencies.md'
|
||||
@@ -59,6 +62,9 @@ nav:
|
||||
- Concurrency and async / await: 'async.md'
|
||||
- Deployment: 'deployment.md'
|
||||
- Project Generation - Template: 'project-generation.md'
|
||||
- Alternatives, Inspiration and Comparisons: 'alternatives.md'
|
||||
- Benchmarks: 'benchmarks.md'
|
||||
- Release Notes: release-notes.md
|
||||
|
||||
markdown_extensions:
|
||||
- markdown.extensions.codehilite:
|
||||
|
||||
@@ -19,8 +19,8 @@ classifiers = [
|
||||
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
||||
]
|
||||
requires = [
|
||||
"starlette >=0.9.7",
|
||||
"pydantic >=0.16"
|
||||
"starlette >=0.9.11,<=0.10.1",
|
||||
"pydantic >=0.17,<=0.18.2"
|
||||
]
|
||||
description-file = "README.md"
|
||||
requires-python = ">=3.6"
|
||||
|
||||
0
scripts/test.sh
Normal file → Executable file
0
scripts/test.sh
Normal file → Executable file
@@ -1,6 +1,4 @@
|
||||
from fastapi import Depends, FastAPI, Path, Query, Security
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from pydantic import BaseModel
|
||||
from fastapi import FastAPI, Path, Query
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -144,8 +142,6 @@ def get_path_param_le_ge_int(item_id: int = Path(..., le=3, ge=1)):
|
||||
|
||||
@app.get("/query")
|
||||
def get_query(query):
|
||||
if query is None:
|
||||
return "foo bar"
|
||||
return f"foo bar {query}"
|
||||
|
||||
|
||||
@@ -158,8 +154,6 @@ def get_query_optional(query=None):
|
||||
|
||||
@app.get("/query/int")
|
||||
def get_query_type(query: int):
|
||||
if query is None:
|
||||
return "foo bar"
|
||||
return f"foo bar {query}"
|
||||
|
||||
|
||||
@@ -184,30 +178,9 @@ def get_query_param(query=Query(None)):
|
||||
|
||||
@app.get("/query/param-required")
|
||||
def get_query_param_required(query=Query(...)):
|
||||
if query is None:
|
||||
return "foo bar"
|
||||
return f"foo bar {query}"
|
||||
|
||||
|
||||
@app.get("/query/param-required/int")
|
||||
def get_query_param_required_type(query: int = Query(...)):
|
||||
if query is None:
|
||||
return "foo bar"
|
||||
return f"foo bar {query}"
|
||||
|
||||
|
||||
reusable_oauth2b = OAuth2PasswordBearer(tokenUrl="/token")
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: str = Security(reusable_oauth2b)):
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/security/oauth2b")
|
||||
def read_current_user(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
@@ -1078,19 +1078,6 @@ openapi_schema = {
|
||||
],
|
||||
}
|
||||
},
|
||||
"/security/oauth2b": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"operationId": "read_current_user_security_oauth2b_get",
|
||||
"security": [{"OAuth2PasswordBearer": []}],
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
@@ -1119,13 +1106,7 @@ openapi_schema = {
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"securitySchemes": {
|
||||
"OAuth2PasswordBearer": {
|
||||
"type": "oauth2",
|
||||
"flows": {"password": {"scopes": {}, "tokenUrl": "/token"}},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1134,6 +1115,7 @@ openapi_schema = {
|
||||
"path,expected_status,expected_response",
|
||||
[
|
||||
("/api_route", 200, {"message": "Hello World"}),
|
||||
("/non_decorated_route", 200, {"message": "Hello World"}),
|
||||
("/nonexistent", 404, {"detail": "Not Found"}),
|
||||
("/openapi.json", 200, openapi_schema),
|
||||
],
|
||||
|
||||
35
tests/test_datetime.py
Normal file
35
tests/test_datetime.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
class ModelWithDatetimeField(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda dt: dt.replace(
|
||||
microsecond=0, tzinfo=timezone.utc
|
||||
).isoformat()
|
||||
}
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
model = ModelWithDatetimeField(dt_field=datetime.utcnow())
|
||||
|
||||
|
||||
@app.get("/model", response_model=ModelWithDatetimeField)
|
||||
def get_model():
|
||||
return model
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_dt():
|
||||
with client:
|
||||
response = client.get("/model")
|
||||
assert json.loads(model.json()) == response.json()
|
||||
@@ -343,7 +343,7 @@ def test_head():
|
||||
|
||||
|
||||
def test_options():
|
||||
response = client.head("/items/foo")
|
||||
response = client.options("/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["x-fastapi-item-id"] == "foo"
|
||||
|
||||
|
||||
23
tests/test_include_route.py
Normal file
23
tests/test_include_route.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.route("/items/")
|
||||
def read_items(request: Request):
|
||||
return JSONResponse({"hello": "world"})
|
||||
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_sub_router():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"hello": "world"}
|
||||
143
tests/test_multi_body_errors.py
Normal file
143
tests/test_multi_body_errors.py
Normal file
@@ -0,0 +1,143 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
|
||||
@app.post("/items/")
|
||||
def save_item_no_body(item: List[Item]):
|
||||
return {"item": item}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Save Item No Body Post",
|
||||
"operationId": "save_item_no_body_items__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "Item",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/Item"},
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Item": {
|
||||
"title": "Item",
|
||||
"required": ["name", "age"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"age": {"title": "Age", "type": "integer"},
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
multiple_errors = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item", 0, "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "item", 0, "age"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "item", 1, "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "item", 1, "age"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_put_correct_body():
|
||||
response = client.post("/items/", json=[{"name": "Foo", "age": 5}])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item": [{"name": "Foo", "age": 5}]}
|
||||
|
||||
|
||||
def test_put_incorrect_body():
|
||||
response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == multiple_errors
|
||||
118
tests/test_multi_query_errors.py
Normal file
118
tests/test_multi_query_errors.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI, Query
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
def read_items(q: List[int] = Query(None)):
|
||||
return {"q": q}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read Items Get",
|
||||
"operationId": "read_items_items__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"title": "Q",
|
||||
"type": "array",
|
||||
"items": {"type": "integer"},
|
||||
},
|
||||
"name": "q",
|
||||
"in": "query",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
multiple_errors = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q", 0],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
{
|
||||
"loc": ["query", "q", 1],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_multi_query():
|
||||
response = client.get("/items/?q=5&q=6")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"q": [5, 6]}
|
||||
|
||||
|
||||
def test_multi_query_incorrect():
|
||||
response = client.get("/items/?q=five&q=six")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == multiple_errors
|
||||
25
tests/test_param_class.py
Normal file
25
tests/test_param_class.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.params import Param
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
def read_items(q: str = Param(None)):
|
||||
return {"q": q}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_default_param_query_none():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"q": None}
|
||||
|
||||
|
||||
def test_default_param_query():
|
||||
response = client.get("/items/?q=foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"q": "foo"}
|
||||
97
tests/test_put_no_body.py
Normal file
97
tests/test_put_no_body.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.put("/items/{item_id}")
|
||||
def save_item_no_body(item_id: str):
|
||||
return {"item_id": item_id}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/{item_id}": {
|
||||
"put": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Save Item No Body Put",
|
||||
"operationId": "save_item_no_body_items__item_id__put",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_put_no_body():
|
||||
response = client.put("/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "foo"}
|
||||
|
||||
|
||||
def test_put_no_body_with_body():
|
||||
response = client.put("/items/foo", json={"name": "Foo"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "foo"}
|
||||
@@ -40,9 +40,19 @@ response_not_valid_int = {
|
||||
("/query/int?query=42.5", 422, response_not_valid_int),
|
||||
("/query/int?query=baz", 422, response_not_valid_int),
|
||||
("/query/int?not_declared=baz", 422, response_missing),
|
||||
("/query/int/optional", 200, "foo bar"),
|
||||
("/query/int/optional?query=50", 200, "foo bar 50"),
|
||||
("/query/int/optional?query=foo", 422, response_not_valid_int),
|
||||
("/query/int/default", 200, "foo bar 10"),
|
||||
("/query/int/default?query=50", 200, "foo bar 50"),
|
||||
("/query/int/default?query=foo", 422, response_not_valid_int),
|
||||
("/query/param", 200, "foo bar"),
|
||||
("/query/param?query=50", 200, "foo bar 50"),
|
||||
("/query/param-required", 422, response_missing),
|
||||
("/query/param-required?query=50", 200, "foo bar 50"),
|
||||
("/query/param-required/int", 422, response_missing),
|
||||
("/query/param-required/int?query=50", 200, "foo bar 50"),
|
||||
("/query/param-required/int?query=foo", 422, response_not_valid_int),
|
||||
],
|
||||
)
|
||||
def test_get_path(path, expected_status, expected_response):
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from .main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_security_oauth2_password_bearer():
|
||||
response = client.get(
|
||||
"/security/oauth2b", headers={"Authorization": "Bearer footokenbar"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "footokenbar"}
|
||||
|
||||
|
||||
def test_security_oauth2_password_bearer_wrong_header():
|
||||
response = client.get("/security/oauth2b", headers={"Authorization": "footokenbar"})
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_security_oauth2_password_bearer_no_header():
|
||||
response = client.get("/security/oauth2b")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
68
tests/test_security_api_key_cookie.py
Normal file
68
tests/test_security_api_key_cookie.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import APIKeyCookie
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
api_key = APIKeyCookie(name="key")
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: str = Security(api_key)):
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"APIKeyCookie": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_api_key():
|
||||
response = client.get("/users/me", cookies={"key": "secret"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "secret"}
|
||||
|
||||
|
||||
def test_security_api_key_no_key():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
68
tests/test_security_api_key_header.py
Normal file
68
tests/test_security_api_key_header.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import APIKeyHeader
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
api_key = APIKeyHeader(name="key")
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: str = Security(api_key)):
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"APIKeyHeader": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"APIKeyHeader": {"type": "apiKey", "name": "key", "in": "header"}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_api_key():
|
||||
response = client.get("/users/me", headers={"key": "secret"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "secret"}
|
||||
|
||||
|
||||
def test_security_api_key_no_key():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
68
tests/test_security_api_key_query.py
Normal file
68
tests/test_security_api_key_query.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import APIKeyQuery
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
api_key = APIKeyQuery(name="key")
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: str = Security(api_key)):
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"APIKeyQuery": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_api_key():
|
||||
response = client.get("/users/me?key=secret")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "secret"}
|
||||
|
||||
|
||||
def test_security_api_key_no_key():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
56
tests/test_security_http_base.py
Normal file
56
tests/test_security_http_base.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from fastapi import FastAPI, Security
|
||||
from fastapi.security.http import HTTPAuthorizationCredentials, HTTPBase
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPBase(scheme="Other")
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(credentials: HTTPAuthorizationCredentials = Security(security)):
|
||||
return {"scheme": credentials.scheme, "credentials": credentials.credentials}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"HTTPBase": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_http_base():
|
||||
response = client.get("/users/me", headers={"Authorization": "Other foobar"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"scheme": "Other", "credentials": "foobar"}
|
||||
|
||||
|
||||
def test_security_http_base_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
76
tests/test_security_http_basic.py
Normal file
76
tests/test_security_http_basic.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from base64 import b64encode
|
||||
|
||||
from fastapi import FastAPI, Security
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPBasic()
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(credentials: HTTPBasicCredentials = Security(security)):
|
||||
return {"username": credentials.username, "password": credentials.password}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"HTTPBasic": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_http_basic():
|
||||
auth = HTTPBasicAuth(username="john", password="secret")
|
||||
response = client.get("/users/me", auth=auth)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "john", "password": "secret"}
|
||||
|
||||
|
||||
def test_security_http_basic_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_security_http_basic_invalid_credentials():
|
||||
response = client.get(
|
||||
"/users/me", headers={"Authorization": "Basic notabase64token"}
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
|
||||
|
||||
def test_security_http_basic_non_basic_credentials():
|
||||
payload = b64encode(b"johnsecret").decode("ascii")
|
||||
auth_header = f"Basic {payload}"
|
||||
response = client.get("/users/me", headers={"Authorization": auth_header})
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
62
tests/test_security_http_bearer.py
Normal file
62
tests/test_security_http_bearer.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from fastapi import FastAPI, Security
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(credentials: HTTPAuthorizationCredentials = Security(security)):
|
||||
return {"scheme": credentials.scheme, "credentials": credentials.credentials}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"HTTPBearer": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_http_bearer():
|
||||
response = client.get("/users/me", headers={"Authorization": "Bearer foobar"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"scheme": "Bearer", "credentials": "foobar"}
|
||||
|
||||
|
||||
def test_security_http_bearer_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_security_http_bearer_incorrect_scheme_credentials():
|
||||
response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
64
tests/test_security_http_digest.py
Normal file
64
tests/test_security_http_digest.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from fastapi import FastAPI, Security
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPDigest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPDigest()
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(credentials: HTTPAuthorizationCredentials = Security(security)):
|
||||
return {"scheme": credentials.scheme, "credentials": credentials.credentials}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"HTTPDigest": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_http_digest():
|
||||
response = client.get("/users/me", headers={"Authorization": "Digest foobar"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"scheme": "Digest", "credentials": "foobar"}
|
||||
|
||||
|
||||
def test_security_http_digest_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_security_http_digest_incorrect_scheme_credentials():
|
||||
response = client.get(
|
||||
"/users/me", headers={"Authorization": "Other invalidauthorization"}
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
247
tests/test_security_oauth2.py
Normal file
247
tests/test_security_oauth2.py
Normal file
@@ -0,0 +1,247 @@
|
||||
import pytest
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import OAuth2
|
||||
from fastapi.security.oauth2 import OAuth2PasswordRequestFormStrict
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
reusable_oauth2 = OAuth2(
|
||||
flows={
|
||||
"password": {
|
||||
"tokenUrl": "/token",
|
||||
"scopes": {"read:users": "Read the users", "write:users": "Create users"},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: str = Security(reusable_oauth2)):
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.post("/login")
|
||||
def read_current_user(form_data: OAuth2PasswordRequestFormStrict = Depends()):
|
||||
return form_data
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/login": {
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read Current User Post",
|
||||
"operationId": "read_current_user_login_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_read_current_user"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
},
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"OAuth2": []}],
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_read_current_user": {
|
||||
"title": "Body_read_current_user",
|
||||
"required": ["grant_type", "username", "password"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"grant_type": {
|
||||
"title": "Grant_Type",
|
||||
"pattern": "password",
|
||||
"type": "string",
|
||||
},
|
||||
"username": {"title": "Username", "type": "string"},
|
||||
"password": {"title": "Password", "type": "string"},
|
||||
"scope": {"title": "Scope", "type": "string", "default": ""},
|
||||
"client_id": {"title": "Client_Id", "type": "string"},
|
||||
"client_secret": {"title": "Client_Secret", "type": "string"},
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"securitySchemes": {
|
||||
"OAuth2": {
|
||||
"type": "oauth2",
|
||||
"flows": {
|
||||
"password": {
|
||||
"scopes": {
|
||||
"read:users": "Read the users",
|
||||
"write:users": "Create users",
|
||||
},
|
||||
"tokenUrl": "/token",
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_oauth2():
|
||||
response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "Bearer footokenbar"}
|
||||
|
||||
|
||||
def test_security_oauth2_password_other_header():
|
||||
response = client.get("/users/me", headers={"Authorization": "Other footokenbar"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "Other footokenbar"}
|
||||
|
||||
|
||||
def test_security_oauth2_password_bearer_no_header():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
required_params = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
grant_type_incorrect = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "grant_type"],
|
||||
"msg": 'string does not match regex "password"',
|
||||
"type": "value_error.str.regex",
|
||||
"ctx": {"pattern": "password"},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data,expected_status,expected_response",
|
||||
[
|
||||
(None, 422, required_params),
|
||||
({"username": "johndoe", "password": "secret"}, 422, grant_type_required),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
|
||||
422,
|
||||
grant_type_incorrect,
|
||||
),
|
||||
(
|
||||
{"username": "johndoe", "password": "secret", "grant_type": "password"},
|
||||
200,
|
||||
{
|
||||
"grant_type": "password",
|
||||
"username": "johndoe",
|
||||
"password": "secret",
|
||||
"scopes": [],
|
||||
"client_id": None,
|
||||
"client_secret": None,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_strict_login(data, expected_status, expected_response):
|
||||
response = client.post("/login", data=data)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
74
tests/test_security_openid_connect.py
Normal file
74
tests/test_security_openid_connect.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security.open_id_connect_url import OpenIdConnect
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
oid = OpenIdConnect(openIdConnectUrl="/openid")
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: str = Security(oid)):
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"OpenIdConnect": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"OpenIdConnect": {"type": "openIdConnect", "openIdConnectUrl": "/openid"}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_oauth2():
|
||||
response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "Bearer footokenbar"}
|
||||
|
||||
|
||||
def test_security_oauth2_password_other_header():
|
||||
response = client.get("/users/me", headers={"Authorization": "Other footokenbar"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "Other footokenbar"}
|
||||
|
||||
|
||||
def test_security_oauth2_password_bearer_no_header():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
@@ -28,7 +28,13 @@ openapi_schema = {
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_scheme():
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_items():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"name": "Foo"}]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from bigger_applications.app.tutorial003 import app
|
||||
from bigger_applications.app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
@@ -17,10 +17,24 @@ openapi_schema = {
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"summary": "Read Users Get",
|
||||
"operationId": "read_users_users__get",
|
||||
}
|
||||
},
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"summary": "Read User Me Get",
|
||||
"operationId": "read_user_me_users_me_get",
|
||||
}
|
||||
},
|
||||
"/users/{username}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
@@ -39,6 +53,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"tags": ["users"],
|
||||
"summary": "Read User Get",
|
||||
"operationId": "read_user_users__username__get",
|
||||
"parameters": [
|
||||
@@ -51,18 +66,6 @@ openapi_schema = {
|
||||
],
|
||||
}
|
||||
},
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read User Me Get",
|
||||
"operationId": "read_user_me_users_me_get",
|
||||
}
|
||||
},
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
@@ -71,6 +74,7 @@ openapi_schema = {
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"tags": ["items"],
|
||||
"summary": "Read Items Get",
|
||||
"operationId": "read_items_items__get",
|
||||
}
|
||||
@@ -93,6 +97,7 @@ openapi_schema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"tags": ["items"],
|
||||
"summary": "Read Item Get",
|
||||
"operationId": "read_item_items__item_id__get",
|
||||
"parameters": [
|
||||
@@ -83,7 +83,7 @@ openapi_schema = {
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_scheme():
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
@@ -101,7 +101,7 @@ openapi_schema = {
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_scheme():
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
@@ -24,7 +24,7 @@ openapi_schema = {
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_scheme():
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
@@ -35,7 +35,7 @@ html_contents = """
|
||||
"""
|
||||
|
||||
|
||||
def test_openapi_scheme():
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
@@ -50,7 +50,49 @@ openapi_schema = {
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read Users Get",
|
||||
"operationId": "read_users_users__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Q", "type": "string"},
|
||||
"name": "q",
|
||||
"in": "query",
|
||||
},
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Skip", "type": "integer", "default": 0},
|
||||
"name": "skip",
|
||||
"in": "query",
|
||||
},
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Limit", "type": "integer", "default": 100},
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
@@ -97,6 +139,7 @@ def test_openapi_schema():
|
||||
("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}),
|
||||
("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}),
|
||||
("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}),
|
||||
("/users", 200, {"q": None, "skip": 0, "limit": 100}),
|
||||
("/openapi.json", 200, openapi_schema),
|
||||
],
|
||||
)
|
||||
|
||||
144
tests/test_tutorial/test_dependencies/test_tutorial004.py
Normal file
144
tests/test_tutorial/test_dependencies/test_tutorial004.py
Normal file
@@ -0,0 +1,144 @@
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from dependencies.tutorial004 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read Items Get",
|
||||
"operationId": "read_items_items__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Q", "type": "string"},
|
||||
"name": "q",
|
||||
"in": "query",
|
||||
},
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Skip", "type": "integer", "default": 0},
|
||||
"name": "skip",
|
||||
"in": "query",
|
||||
},
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Limit", "type": "integer", "default": 100},
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/items",
|
||||
200,
|
||||
{
|
||||
"items": [
|
||||
{"item_name": "Foo"},
|
||||
{"item_name": "Bar"},
|
||||
{"item_name": "Baz"},
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items?q=foo",
|
||||
200,
|
||||
{
|
||||
"items": [
|
||||
{"item_name": "Foo"},
|
||||
{"item_name": "Bar"},
|
||||
{"item_name": "Baz"},
|
||||
],
|
||||
"q": "foo",
|
||||
},
|
||||
),
|
||||
(
|
||||
"/items?q=foo&skip=1",
|
||||
200,
|
||||
{"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"},
|
||||
),
|
||||
(
|
||||
"/items?q=bar&limit=2",
|
||||
200,
|
||||
{"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"},
|
||||
),
|
||||
(
|
||||
"/items?q=bar&skip=1&limit=1",
|
||||
200,
|
||||
{"items": [{"item_name": "Bar"}], "q": "bar"},
|
||||
),
|
||||
(
|
||||
"/items?limit=1&q=bar&skip=1",
|
||||
200,
|
||||
{"items": [{"item_name": "Bar"}], "q": "bar"},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_get(path, expected_status, expected_response):
|
||||
response = client.get(path)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
@@ -74,7 +74,7 @@ openapi_schema = {
|
||||
},
|
||||
"process_after": {
|
||||
"title": "Process_After",
|
||||
"type": "string",
|
||||
"type": "number",
|
||||
"format": "time-delta",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from path_operation_advanced_configuration.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Items Get",
|
||||
"operationId": "some_specific_id_you_define",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_get():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"item_id": "Foo"}]
|
||||
@@ -0,0 +1,23 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from path_operation_advanced_configuration.tutorial002 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_get():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"item_id": "Foo"}]
|
||||
@@ -0,0 +1,112 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from path_operation_configuration.tutorial005 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The created item",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Create an item",
|
||||
"description": "\n Create an item with all the information:\n \n * name: each item must have a name\n * description: a long description\n * price: required\n * tax: if the item doesn't have tax, you can omit this\n * tags: a set of unique tag strings for this item\n ",
|
||||
"operationId": "create_item_items__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Item": {
|
||||
"title": "Item",
|
||||
"required": ["name", "price"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tags": {
|
||||
"title": "Tags",
|
||||
"uniqueItems": True,
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"default": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_query_params_str_validations():
|
||||
response = client.post("/items/", json={"name": "Foo", "price": 42})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"name": "Foo",
|
||||
"price": 42,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
"tags": [],
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from path_operation_configuration.tutorial006 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"tags": ["items"],
|
||||
"summary": "Read Items Get",
|
||||
"operationId": "read_items_items__get",
|
||||
}
|
||||
},
|
||||
"/users/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"summary": "Read Users Get",
|
||||
"operationId": "read_users_users__get",
|
||||
}
|
||||
},
|
||||
"/elements/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"tags": ["items"],
|
||||
"summary": "Read Elements Get",
|
||||
"operationId": "read_elements_elements__get",
|
||||
"deprecated": True,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_status,expected_response",
|
||||
[
|
||||
("/items/", 200, [{"name": "Foo", "price": 42}]),
|
||||
("/users/", 200, [{"username": "johndoe"}]),
|
||||
("/elements/", 200, [{"item_id": "Foo"}]),
|
||||
],
|
||||
)
|
||||
def test_query_params_str_validations(path, expected_status, expected_response):
|
||||
response = client.get(path)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
@@ -1,3 +1,4 @@
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from query_params_str_validations.tutorial010 import app
|
||||
@@ -80,7 +81,42 @@ openapi_schema = {
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_scheme():
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
regex_error = {
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"pattern": "^fixedquery$"},
|
||||
"loc": ["query", "item-query"],
|
||||
"msg": 'string does not match regex "^fixedquery$"',
|
||||
"type": "value_error.str.regex",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"q_name,q,expected_status,expected_response",
|
||||
[
|
||||
(None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
|
||||
(
|
||||
"item-query",
|
||||
"fixedquery",
|
||||
200,
|
||||
{"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"},
|
||||
),
|
||||
("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
|
||||
("item-query", "nonregexquery", 422, regex_error),
|
||||
],
|
||||
)
|
||||
def test_query_params_str_validations(q_name, q, expected_status, expected_response):
|
||||
url = "/items/"
|
||||
if q_name and q:
|
||||
url = f"{url}?{q_name}={q}"
|
||||
response = client.get(url)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user