mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-24 14:48:35 -05:00
Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5592fa0f6f | ||
|
|
b65be5d496 | ||
|
|
6c7da43e51 | ||
|
|
dfec2d7644 | ||
|
|
8c3ef76139 | ||
|
|
7a504a721c | ||
|
|
dd963511d6 | ||
|
|
fdb6d43e10 | ||
|
|
a7c718e968 | ||
|
|
f4d753620b | ||
|
|
fadfe4c586 | ||
|
|
5fd83c5fa4 | ||
|
|
14daaf409f | ||
|
|
c7dc26b760 | ||
|
|
f5ccb3c35d | ||
|
|
4cea311e6e | ||
|
|
f8718072a0 | ||
|
|
3dbbecdd16 | ||
|
|
6d5530ec1c | ||
|
|
0761f11d1a | ||
|
|
f2e7ef7056 | ||
|
|
d5d9a20937 | ||
|
|
96f092179f | ||
|
|
8505b716af | ||
|
|
78272ac1f3 | ||
|
|
f1bee9a271 | ||
|
|
b20b2218cd | ||
|
|
b9cf69cd42 | ||
|
|
f803c77515 | ||
|
|
0c67022048 | ||
|
|
d8fe307d61 | ||
|
|
580cf8f4e2 | ||
|
|
af390af77c | ||
|
|
4642f63a1e | ||
|
|
203e10596f | ||
|
|
5a2278d09a | ||
|
|
47a8387a04 | ||
|
|
27ca0c9dca | ||
|
|
9418d78de6 | ||
|
|
4b74aef429 | ||
|
|
fc7d123347 | ||
|
|
53da56146e | ||
|
|
3799b9027e | ||
|
|
c70f3f1198 | ||
|
|
58dddc5e4f | ||
|
|
c90c4fb6c1 | ||
|
|
5b3df28f0c | ||
|
|
6c6bdb6233 | ||
|
|
f156f45193 | ||
|
|
f24d744a3b | ||
|
|
937b462cdd | ||
|
|
3025a368c6 | ||
|
|
c218e0d560 | ||
|
|
1ed5aa23e6 | ||
|
|
106d2171d8 | ||
|
|
c5817912d2 | ||
|
|
a7a92bc637 | ||
|
|
68d1fea961 | ||
|
|
8c6b2d5804 | ||
|
|
19c53b21c1 | ||
|
|
44d63cd555 | ||
|
|
55c4b5fb0b | ||
|
|
c32e800c23 | ||
|
|
73dbbeab55 | ||
|
|
417a3ab140 | ||
|
|
a3235ed8de | ||
|
|
38495fffa5 | ||
|
|
b77a43bcac | ||
|
|
483eb73b26 | ||
|
|
51a928d3f5 | ||
|
|
e71636e381 | ||
|
|
f7f17fcfd6 | ||
|
|
033bc2a6c9 | ||
|
|
28d3b9f783 | ||
|
|
0c55553328 | ||
|
|
b66056aa34 | ||
|
|
4f10b8b98d | ||
|
|
06eb421934 | ||
|
|
bf229ad5d8 | ||
|
|
d0319001be | ||
|
|
c4682af13d | ||
|
|
6ca3ce80e4 | ||
|
|
25e85c8522 | ||
|
|
6bf3ab3b7a | ||
|
|
f5ea5eef2a | ||
|
|
46a986cacf | ||
|
|
e620aeb46d | ||
|
|
d1e2e46b80 | ||
|
|
b1c4a8acd5 | ||
|
|
362e2cdc79 | ||
|
|
93e6a08acd | ||
|
|
3ec4342282 | ||
|
|
dc483478eb | ||
|
|
bdd251a05b | ||
|
|
195559ccba | ||
|
|
9a71672a95 | ||
|
|
7e48be1561 | ||
|
|
508f9ce954 | ||
|
|
afbdf2546f | ||
|
|
62df417807 | ||
|
|
09d2747a70 | ||
|
|
d3ea6f7514 | ||
|
|
02187636ea | ||
|
|
687065509b | ||
|
|
b30cca8e9e | ||
|
|
3906777065 | ||
|
|
d60a10fa59 | ||
|
|
54368e7b22 | ||
|
|
acc556e416 | ||
|
|
700585f99d | ||
|
|
4c2993f353 | ||
|
|
ea9277aab4 | ||
|
|
8d86fca027 | ||
|
|
fc0716a7dd | ||
|
|
1e593dc4d4 | ||
|
|
dcc1e1bcf8 | ||
|
|
06eb775c63 | ||
|
|
ab77c069d4 | ||
|
|
46fffc0e94 | ||
|
|
1c2cdb97e9 | ||
|
|
76b6fd5c18 | ||
|
|
a2fb716035 | ||
|
|
aa84ac8e3e | ||
|
|
4ed2bd1fea | ||
|
|
87b7a63ff2 | ||
|
|
06d0918c3d | ||
|
|
5b3adfe449 | ||
|
|
bdd794a0e6 | ||
|
|
f0df79aa91 | ||
|
|
c26f1760d4 | ||
|
|
e5fa4b0af6 | ||
|
|
a33c299fd7 | ||
|
|
6939621730 | ||
|
|
120ab08360 | ||
|
|
3f5521fdfb | ||
|
|
7244e4b612 | ||
|
|
d329745064 | ||
|
|
5f7fe926ab | ||
|
|
c8eea09664 | ||
|
|
5700d65188 | ||
|
|
46178a5347 | ||
|
|
bff5dbbf5d | ||
|
|
09cd7c47a1 | ||
|
|
e2fadcbc90 | ||
|
|
b3bb29afa8 | ||
|
|
c7db2ff858 | ||
|
|
2a7ef5504a | ||
|
|
27964c5ffd | ||
|
|
d262f6e929 | ||
|
|
d61f5e4b55 | ||
|
|
3ed112e8a9 | ||
|
|
9da626eb2c | ||
|
|
6f74c7327b | ||
|
|
360a2797c1 | ||
|
|
0552977cd6 | ||
|
|
bd407cc4ed |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,3 +12,4 @@ coverage.xml
|
||||
.netlify
|
||||
test.db
|
||||
log.txt
|
||||
Pipfile.lock
|
||||
|
||||
@@ -7,6 +7,13 @@ cache: pip
|
||||
python:
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
- "3.8-dev"
|
||||
- "nightly"
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- python: "3.8-dev"
|
||||
- python: "nightly"
|
||||
|
||||
install:
|
||||
- pip install flit
|
||||
|
||||
5
Pipfile
5
Pipfile
@@ -25,10 +25,11 @@ sqlalchemy = "*"
|
||||
uvicorn = "*"
|
||||
|
||||
[packages]
|
||||
starlette = "==0.12.0"
|
||||
pydantic = "==0.26.0"
|
||||
starlette = "==0.12.9"
|
||||
pydantic = "==0.32.2"
|
||||
databases = {extras = ["sqlite"],version = "*"}
|
||||
hypercorn = "*"
|
||||
orjson = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
|
||||
936
Pipfile.lock
generated
936
Pipfile.lock
generated
@@ -1,936 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "4a33b47e814fa75533548874ffadbc6163b3058db4d1615ff633512366d72ccb"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.6"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"aiocontextvars": {
|
||||
"hashes": [
|
||||
"sha256:885daf8261818767d8f7cbd79f9d4482d118f024b6586ef6e67980236a27bfa3",
|
||||
"sha256:f027372dc48641f683c559f247bd84962becaacdc9ba711d583c3871fb5652aa"
|
||||
],
|
||||
"version": "==0.2.2"
|
||||
},
|
||||
"aiosqlite": {
|
||||
"hashes": [
|
||||
"sha256:ad84fbd7516ca7065d799504fc41d6845c938e5306d1b7dd960caaeda12e22a9"
|
||||
],
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"contextvars": {
|
||||
"hashes": [
|
||||
"sha256:f38c908aaa59c14335eeea12abea5f443646216c4e29380d7bf34d2018e2c39e"
|
||||
],
|
||||
"markers": "python_version < '3.7'",
|
||||
"version": "==2.4"
|
||||
},
|
||||
"databases": {
|
||||
"extras": [
|
||||
"sqlite"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:d365cff2035c5177ef5fd8c5abf6671da01189521da64848a01251c870daf48f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.2.2"
|
||||
},
|
||||
"dataclasses": {
|
||||
"hashes": [
|
||||
"sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f",
|
||||
"sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"
|
||||
],
|
||||
"markers": "python_version < '3.7'",
|
||||
"version": "==0.6"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208",
|
||||
"sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"
|
||||
],
|
||||
"version": "==0.8.1"
|
||||
},
|
||||
"h2": {
|
||||
"hashes": [
|
||||
"sha256:c8f387e0e4878904d4978cd688a3195f6b169d49b1ffa572a3d347d7adc5e09f",
|
||||
"sha256:fd07e865a3272ac6ef195d8904de92dc7b38dc28297ec39cfa22716b6d62e6eb"
|
||||
],
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"hpack": {
|
||||
"hashes": [
|
||||
"sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89",
|
||||
"sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"
|
||||
],
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"hypercorn": {
|
||||
"hashes": [
|
||||
"sha256:cfe7811a93ab7bc22c8a0d5514a2a7a512e812c1e4ee13b9731b705b79d4d453",
|
||||
"sha256:f2577806223fa44d57d6f136b6c37a046794f961252699aec8afb15f35d226d5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.5.4"
|
||||
},
|
||||
"hyperframe": {
|
||||
"hashes": [
|
||||
"sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40",
|
||||
"sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"
|
||||
],
|
||||
"version": "==5.2.0"
|
||||
},
|
||||
"immutables": {
|
||||
"hashes": [
|
||||
"sha256:10861f2a2b86139f0c91d5073392d76117f37e84f912dc47c943c23a64008cc7",
|
||||
"sha256:3e23eeb4bc55d57b2a97bef4c1a2891bbb731050b4167c855545797d45e84e45",
|
||||
"sha256:4373876879f147986808f71e6ca02380192a279e8b8d45832f6fed4e7f717562",
|
||||
"sha256:46f9122da033fecf84d7f4c6257aec780f370b20f3ce6bc521702b63ee3d99f7",
|
||||
"sha256:5104db6102e53702af45c6b0af36e45a80970123b11a80c14e0fce48444cdbe3",
|
||||
"sha256:59274bcb631f4fdc9731e9a4a96d16d96b3a17e29fd5e46516518f38406f678f",
|
||||
"sha256:65a9c624e50ca5c50464dbf432996b5c4f056a411bcff5690ef4cab59f913f99",
|
||||
"sha256:b64e0672497b884d21170ca61c693da8488d77f043650efa7911378cbbad0f2c",
|
||||
"sha256:b70655dba00742b033310933066a2202e1cfbbb0f63841b4597cd8787974b242",
|
||||
"sha256:c3d8c238a6f9b60355578579563773348674b6da63c1a0d7394384ed341f3d41",
|
||||
"sha256:cd66bcd11b6a1c1a80fb8d90e25870ff2d5c705ab5eb9666355a33d3fef6ac70",
|
||||
"sha256:d59310fc4f97c1ff8c3660cb98032db266ac0c285a86ca7a512e8e84a95f44c9",
|
||||
"sha256:d71d1c822498646143270580dd6f743bb31ab89ae0ded8b2307c356d3a00f1c0",
|
||||
"sha256:f53da698b42db83cfb1f5073560838051430798c8d8e34a57a27031edbc3041d",
|
||||
"sha256:f958ba15745e30d3a38e3c9fcead8496037135bb21c78c0f925c104abba3a6fa",
|
||||
"sha256:ff95e2aa618eed1a0ef4479938f18f3522c89562b9bbb59d677597c0337569dd"
|
||||
],
|
||||
"version": "==0.9"
|
||||
},
|
||||
"pydantic": {
|
||||
"hashes": [
|
||||
"sha256:b72e0df2463cee746cf42639845d4106c19f30136375e779352d710e69617731",
|
||||
"sha256:dab99d3070e040b8b2e987dfbe237350ab92d5d57a22d4e0e268ede2d85c7964"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.26.0"
|
||||
},
|
||||
"pytoml": {
|
||||
"hashes": [
|
||||
"sha256:ca2d0cb127c938b8b76a9a0d0f855cf930c1d50cc3a0af6d3595b566519a1013"
|
||||
],
|
||||
"version": "==0.1.20"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:91c54ca8345008fceaec987e10924bf07dcab36c442925357e5a467b36a38319"
|
||||
],
|
||||
"version": "==1.3.3"
|
||||
},
|
||||
"starlette": {
|
||||
"hashes": [
|
||||
"sha256:d313433ef5cc38e0a276b59688ca2b11b8f031c78808c1afdf9d55cb86f34590"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.12.0"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:07b2c978670896022a43c4b915df8958bec4a6b84add7f2c87b2b728bda3ba64",
|
||||
"sha256:f3f0e67e1d42de47b5c67c32c9b26641642e9170fe7e292991793705cd5fef7c",
|
||||
"sha256:fb2cd053238d33a8ec939190f30cfd736c00653a85a2919415cecf7dc3d9da71"
|
||||
],
|
||||
"version": "==3.7.2"
|
||||
},
|
||||
"wsproto": {
|
||||
"hashes": [
|
||||
"sha256:55c3da870460e8838b2fbe4d10f3accc0cea3a13d5e8dbbdc6da5d537d6d44dc",
|
||||
"sha256:c7f35e0af250b9f25583b090039eb2159a079fbe71b7daf86cc3ddcd2f3a70b3"
|
||||
],
|
||||
"version": "==0.14.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
|
||||
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
|
||||
"sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
|
||||
],
|
||||
"version": "==19.1.0"
|
||||
},
|
||||
"autoflake": {
|
||||
"hashes": [
|
||||
"sha256:6b59e5b9b82e30077499578856282debb81186d10b4f899e8c2e1d616cdef973"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3"
|
||||
},
|
||||
"backcall": {
|
||||
"hashes": [
|
||||
"sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4",
|
||||
"sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"
|
||||
],
|
||||
"version": "==0.1.0"
|
||||
},
|
||||
"better-exceptions": {
|
||||
"hashes": [
|
||||
"sha256:bf79c87659bc849989d726bf0e4a2100edefe7eded112d201f54fe08467fdf63",
|
||||
"sha256:c196cad849de615abb9f6eb67ca1b83f33b938818f0e2fe8fa157b22aeb7b992"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.2.2"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf",
|
||||
"sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.3b0"
|
||||
},
|
||||
"bleach": {
|
||||
"hashes": [
|
||||
"sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16",
|
||||
"sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa"
|
||||
],
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
|
||||
"sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
|
||||
],
|
||||
"version": "==2019.3.9"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"version": "==7.0"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:0402b1822d513d0231589494bceddb067d20581f5083598c451b56c684b0e5d6",
|
||||
"sha256:0644e28e8aea9d9d563607ee8b7071b07dd57a4a3de11f8684cd33c51c0d1b93",
|
||||
"sha256:0874a283686803884ec0665018881130604956dbaa344f2539c46d82cbe29eda",
|
||||
"sha256:0988c3837df4bc371189bb3425d5232cf150055452034c232dda9cbe04f9c38e",
|
||||
"sha256:20bc3205b3100956bb72293fabb97f0ed972c81fed10b3251c90c70dcb0599ab",
|
||||
"sha256:2cc9142a3367e74eb6b19d58c53ebb1dfd7336b91cdcc91a6a2888bf8c7af984",
|
||||
"sha256:3ae9a0a59b058ce0761c3bd2c2d66ecb2ee2b8ac592620184370577f7a546fb3",
|
||||
"sha256:3b2e30b835df58cb973f478d09f3d82e90c98c8e5059acc245a8e4607e023801",
|
||||
"sha256:401e9b04894eb1498c639c6623ee78a646990ce5f095248e2440968aafd6e90e",
|
||||
"sha256:41ec5812d5decdaa72708be3018e7443e90def4b5a71294236a4df192cf9eab9",
|
||||
"sha256:475769b638a055e75b3d3219e054fe2a023c0b077ff15bff6c95aba7e93e6cac",
|
||||
"sha256:61424f4e2e82c4129a4ba71e10ebacb32a9ecd6f80de2cd05bdead6ba75ed736",
|
||||
"sha256:811969904d4dd0bee7d958898be8d9d75cef672d9b7e7db819dfeac3d20d2d0c",
|
||||
"sha256:86224bb99abfd672bf2f9fcecad5e8d7a3fa94f7f71513f2210460a0350307cd",
|
||||
"sha256:9a238a20a3af00665f8381f7e53e9c606f9bb652d2423f6b822f6cb790d887e8",
|
||||
"sha256:a23b3fbc14d4e6182ecebfd22f3729beef0636d151d94764a1c28330d185e4e5",
|
||||
"sha256:ac162b4ebe51b7a2b7f5e462c4402802633eb81e77c94f8a7c1ed8a556e72c75",
|
||||
"sha256:b6187378726c84365bf297b5dcdae8789b6a5823b200bea23797777e5a63be09",
|
||||
"sha256:bcd5723d905ed4a825f17410a53535f880b6d7548ae3d89078db7b1ceefcd853",
|
||||
"sha256:c48a4f9c5fb385269bb7fbaf9c1326a94863b65ec7f5c96b2ea56b252f01ad08",
|
||||
"sha256:cd40199d6f1c29c85b170d25589be9a97edff8ee7e62be180a2a137823896030",
|
||||
"sha256:d1bc331a7d069485ac1d8c25a0ea1f6aab6cb2a87146fb652222481c1bddc9ff",
|
||||
"sha256:d7e0cdc249aa0f94aa2e531b03999ddaf03a10b4fa090a894712d4c8066abd89",
|
||||
"sha256:e9ee8fcd8e067fcc5d7276d46e07e863102b70a52545ef4254df1ff0893ce75f",
|
||||
"sha256:eb313c23d983b7810504f42104e8dcd1c7ccdda8fbaab82aab92ab79fea19345",
|
||||
"sha256:f9cfd478654b509941b85ed70f870f5e3c74678f566bec12fd26545e5340ba47",
|
||||
"sha256:fae1fa144034d021a52cb9ea200eb8dedf91869c6df8202ad5d149b41ed91cc8"
|
||||
],
|
||||
"version": "==5.0a5"
|
||||
},
|
||||
"decorator": {
|
||||
"hashes": [
|
||||
"sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de",
|
||||
"sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"
|
||||
],
|
||||
"version": "==4.4.0"
|
||||
},
|
||||
"defusedxml": {
|
||||
"hashes": [
|
||||
"sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93",
|
||||
"sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"dnspython": {
|
||||
"hashes": [
|
||||
"sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01",
|
||||
"sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"
|
||||
],
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
|
||||
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
|
||||
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
|
||||
],
|
||||
"version": "==0.14"
|
||||
},
|
||||
"email-validator": {
|
||||
"hashes": [
|
||||
"sha256:79966e318d6d68fed359c90f8f19d242bcc178b724011f1c07145bd093da6cc7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.4"
|
||||
},
|
||||
"entrypoints": {
|
||||
"hashes": [
|
||||
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
|
||||
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
|
||||
],
|
||||
"version": "==0.3"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661",
|
||||
"sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.7"
|
||||
},
|
||||
"flit": {
|
||||
"hashes": [
|
||||
"sha256:1d93f7a833ed8a6e120ddc40db5c4763bc39bccc75c05081ec8285ece718aefb",
|
||||
"sha256:6f6f0fb83c51ffa3a150fa41b5ac118df9ea4a87c2c06dff4ebf9adbe7b52b36"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208",
|
||||
"sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"
|
||||
],
|
||||
"version": "==0.8.1"
|
||||
},
|
||||
"httptools": {
|
||||
"hashes": [
|
||||
"sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc"
|
||||
],
|
||||
"version": "==0.0.13"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"ipykernel": {
|
||||
"hashes": [
|
||||
"sha256:346189536b88859937b5f4848a6fd85d1ad0729f01724a411de5cae9b618819c",
|
||||
"sha256:f0e962052718068ad3b1d8bcc703794660858f58803c3798628817f492a8769c"
|
||||
],
|
||||
"version": "==5.1.1"
|
||||
},
|
||||
"ipython": {
|
||||
"hashes": [
|
||||
"sha256:54c5a8aa1eadd269ac210b96923688ccf01ebb2d0f21c18c3c717909583579a8",
|
||||
"sha256:e840810029224b56cd0d9e7719dc3b39cf84d577f8ac686547c8ba7a06eeab26"
|
||||
],
|
||||
"markers": "python_version >= '3.3'",
|
||||
"version": "==7.5.0"
|
||||
},
|
||||
"ipython-genutils": {
|
||||
"hashes": [
|
||||
"sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8",
|
||||
"sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"
|
||||
],
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
"ipywidgets": {
|
||||
"hashes": [
|
||||
"sha256:0f2b5cde9f272cb49d52f3f0889fdd1a7ae1e74f37b48dac35a83152780d2b7b",
|
||||
"sha256:a3e224f430163f767047ab9a042fc55adbcab0c24bbe6cf9f306c4f89fdf0ba3"
|
||||
],
|
||||
"version": "==7.4.2"
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
"sha256:c40744b6bc5162bbb39c1257fe298b7a393861d50978b565f3ccd9cb9de0182a",
|
||||
"sha256:f57abacd059dc3bd666258d1efb0377510a89777fda3e3274e3c01f7c03ae22d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.3.20"
|
||||
},
|
||||
"jedi": {
|
||||
"hashes": [
|
||||
"sha256:2bb0603e3506f708e792c7f4ad8fc2a7a9d9c2d292a358fbbd58da531695595b",
|
||||
"sha256:2c6bcd9545c7d6440951b12b44d373479bf18123a401a52025cf98563fbd826c"
|
||||
],
|
||||
"version": "==0.13.3"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
|
||||
"sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
|
||||
],
|
||||
"version": "==2.10.1"
|
||||
},
|
||||
"jsonschema": {
|
||||
"hashes": [
|
||||
"sha256:0c0a81564f181de3212efa2d17de1910f8732fa1b71c42266d983cd74304e20d",
|
||||
"sha256:a5f6559964a3851f59040d3b961de5e68e70971afb88ba519d27e6a039efff1a"
|
||||
],
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"jupyter": {
|
||||
"hashes": [
|
||||
"sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7",
|
||||
"sha256:5b290f93b98ffbc21c0c7e749f054b3267782166d72fa5e3ed1ed4eaf34a2b78",
|
||||
"sha256:d9dc4b3318f310e34c82951ea5d6683f67bed7def4b259fafbfe4f1beb1d8e5f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"jupyter-client": {
|
||||
"hashes": [
|
||||
"sha256:b5f9cb06105c1d2d30719db5ffb3ea67da60919fb68deaefa583deccd8813551",
|
||||
"sha256:c44411eb1463ed77548bc2d5ec0d744c9b81c4a542d9637c7a52824e2121b987"
|
||||
],
|
||||
"version": "==5.2.4"
|
||||
},
|
||||
"jupyter-console": {
|
||||
"hashes": [
|
||||
"sha256:308ce876354924fb6c540b41d5d6d08acfc946984bf0c97777c1ddcb42e0b2f5",
|
||||
"sha256:cc80a97a5c389cbd30252ffb5ce7cefd4b66bde98219edd16bf5cb6f84bb3568"
|
||||
],
|
||||
"version": "==6.0.0"
|
||||
},
|
||||
"jupyter-core": {
|
||||
"hashes": [
|
||||
"sha256:927d713ffa616ea11972534411544589976b2493fc7e09ad946e010aa7eb9970",
|
||||
"sha256:ba70754aa680300306c699790128f6fbd8c306ee5927976cbe48adacf240c0b7"
|
||||
],
|
||||
"version": "==4.4.0"
|
||||
},
|
||||
"livereload": {
|
||||
"hashes": [
|
||||
"sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b",
|
||||
"sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"
|
||||
],
|
||||
"version": "==2.6.1"
|
||||
},
|
||||
"markdown": {
|
||||
"hashes": [
|
||||
"sha256:2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a",
|
||||
"sha256:56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c"
|
||||
],
|
||||
"version": "==3.1.1"
|
||||
},
|
||||
"markdown-include": {
|
||||
"hashes": [
|
||||
"sha256:72a45461b589489a088753893bc95c5fa5909936186485f4ed55caa57d10250f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
|
||||
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
||||
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
||||
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
||||
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
||||
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
||||
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
||||
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
||||
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
||||
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
||||
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
||||
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
||||
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
||||
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
||||
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
|
||||
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
|
||||
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
|
||||
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
|
||||
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
|
||||
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
|
||||
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
|
||||
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
|
||||
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
|
||||
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
|
||||
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
||||
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
||||
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||
],
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"mistune": {
|
||||
"hashes": [
|
||||
"sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e",
|
||||
"sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"
|
||||
],
|
||||
"version": "==0.8.4"
|
||||
},
|
||||
"mkdocs": {
|
||||
"hashes": [
|
||||
"sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939",
|
||||
"sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.4"
|
||||
},
|
||||
"mkdocs-material": {
|
||||
"hashes": [
|
||||
"sha256:1c39b6af13a900d9f47ab2b8ac67b3258799f4570b552573e9d6868ad6a438e9",
|
||||
"sha256:22073941cff7176e810b719aced6a90381e64a96d346b8a6803a06b7192b7ad5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.3.0"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7",
|
||||
"sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a"
|
||||
],
|
||||
"markers": "python_version > '2.7'",
|
||||
"version": "==7.0.0"
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:2afe51527b1f6cdc4a5f34fc90473109b22bf7f21086ba3e9451857cf11489e6",
|
||||
"sha256:56a16df3e0abb145d8accd5dbb70eba6c4bd26e2f89042b491faa78c9635d1e2",
|
||||
"sha256:5764f10d27b2e93c84f70af5778941b8f4aa1379b2430f85c827e0f5464e8714",
|
||||
"sha256:5bbc86374f04a3aa817622f98e40375ccb28c4836f36b66706cf3c6ccce86eda",
|
||||
"sha256:6a9343089f6377e71e20ca734cd8e7ac25d36478a9df580efabfe9059819bf82",
|
||||
"sha256:6c9851bc4a23dc1d854d3f5dfd5f20a016f8da86bcdbb42687879bb5f86434b0",
|
||||
"sha256:b8e85956af3fcf043d6f87c91cbe8705073fc67029ba6e22d3468bfee42c4823",
|
||||
"sha256:b9a0af8fae490306bc112229000aa0c2ccc837b49d29a5c42e088c132a2334dd",
|
||||
"sha256:bbf643528e2a55df2c1587008d6e3bda5c0445f1240dfa85129af22ae16d7a9a",
|
||||
"sha256:c46ab3438bd21511db0f2c612d89d8344154c0c9494afc7fbc932de514cf8d15",
|
||||
"sha256:f7a83d6bd805855ef83ec605eb01ab4fa42bcef254b13631e451cbb44914a9b0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.701"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
"sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812",
|
||||
"sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e"
|
||||
],
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"nbconvert": {
|
||||
"hashes": [
|
||||
"sha256:138381baa41d83584459b5cfecfc38c800ccf1f37d9ddd0bd440783346a4c39c",
|
||||
"sha256:4a978548d8383f6b2cfca4a3b0543afb77bc7cb5a96e8b424337ab58c12da9bc"
|
||||
],
|
||||
"version": "==5.5.0"
|
||||
},
|
||||
"nbformat": {
|
||||
"hashes": [
|
||||
"sha256:b9a0dbdbd45bb034f4f8893cafd6f652ea08c8c1674ba83f2dc55d3955743b0b",
|
||||
"sha256:f7494ef0df60766b7cabe0a3651556345a963b74dbc16bc7c18479041170d402"
|
||||
],
|
||||
"version": "==4.4.0"
|
||||
},
|
||||
"notebook": {
|
||||
"hashes": [
|
||||
"sha256:573e0ae650c5d76b18b6e564ba6d21bf321d00847de1d215b418acb64f056eb8",
|
||||
"sha256:f64fa6624d2323fbef6210a621817d6505a45d0d4a9367f1843b20a38a4666ee"
|
||||
],
|
||||
"version": "==5.7.8"
|
||||
},
|
||||
"pandocfilters": {
|
||||
"hashes": [
|
||||
"sha256:b3dd70e169bb5449e6bc6ff96aea89c5eea8c5f6ab5e207fc2f521a2cf4a0da9"
|
||||
],
|
||||
"version": "==1.4.2"
|
||||
},
|
||||
"parso": {
|
||||
"hashes": [
|
||||
"sha256:17cc2d7a945eb42c3569d4564cdf49bde221bc2b552af3eca9c1aad517dcdd33",
|
||||
"sha256:2e9574cb12e7112a87253e14e2c380ce312060269d04bd018478a3c92ea9a376"
|
||||
],
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"pexpect": {
|
||||
"hashes": [
|
||||
"sha256:2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1",
|
||||
"sha256:9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb"
|
||||
],
|
||||
"markers": "sys_platform != 'win32'",
|
||||
"version": "==4.7.0"
|
||||
},
|
||||
"pickleshare": {
|
||||
"hashes": [
|
||||
"sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca",
|
||||
"sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"
|
||||
],
|
||||
"version": "==0.7.5"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:25a1bc1d148c9a640211872b4ff859878d422bccb59c9965e04eed468a0aa180",
|
||||
"sha256:964cedd2b27c492fbf0b7f58b3284a09cf7f99b0f715941fb24a439b3af1bd1a"
|
||||
],
|
||||
"version": "==0.11.0"
|
||||
},
|
||||
"prometheus-client": {
|
||||
"hashes": [
|
||||
"sha256:1b38b958750f66f208bcd9ab92a633c0c994d8859c831f7abc1f46724fcee490"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"prompt-toolkit": {
|
||||
"hashes": [
|
||||
"sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780",
|
||||
"sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1",
|
||||
"sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"
|
||||
],
|
||||
"version": "==2.0.9"
|
||||
},
|
||||
"ptyprocess": {
|
||||
"hashes": [
|
||||
"sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0",
|
||||
"sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"
|
||||
],
|
||||
"markers": "os_name != 'nt'",
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
|
||||
"sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
|
||||
],
|
||||
"version": "==1.8.0"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
|
||||
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
|
||||
],
|
||||
"version": "==2.5.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
|
||||
"sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
|
||||
],
|
||||
"version": "==2.1.1"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:31cba6ffb739f099a85e243eff8cb717089fdd3c7300767d9fc34cb8e1b065f5",
|
||||
"sha256:5ad302949b3c98dd73f8d9fcdc7e9cb592f120e32a18e23efd7f3dc51194472b"
|
||||
],
|
||||
"version": "==2.4.0"
|
||||
},
|
||||
"pymdown-extensions": {
|
||||
"hashes": [
|
||||
"sha256:25b0a7967fa697b5035e23340a48594e3e93acb10b06d74574218ace3347d1df",
|
||||
"sha256:6cf0cf36b5a03b291ace22dc2f320f4789ce56fbdb6635a3be5fadbf5d7694dd"
|
||||
],
|
||||
"version": "==6.0"
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
"sha256:16692ee739d42cf5e39cef8d27649a8c1fdb7aa99887098f1460057c5eb75c3a"
|
||||
],
|
||||
"version": "==0.15.2"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:1a8aa4fa958f8f451ac5441f3ac130d9fc86ea38780dd2715e6d5c5882700b24",
|
||||
"sha256:b8bf138592384bd4e87338cb0f256bf5f615398a649d4bd83915f0e4047a5ca6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.5.0"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
"sha256:2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6",
|
||||
"sha256:e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.7.1"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
|
||||
"sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
|
||||
],
|
||||
"version": "==2.8.0"
|
||||
},
|
||||
"python-multipart": {
|
||||
"hashes": [
|
||||
"sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.0.5"
|
||||
},
|
||||
"pytoml": {
|
||||
"hashes": [
|
||||
"sha256:ca2d0cb127c938b8b76a9a0d0f855cf930c1d50cc3a0af6d3595b566519a1013"
|
||||
],
|
||||
"version": "==0.1.20"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c",
|
||||
"sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95",
|
||||
"sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2",
|
||||
"sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4",
|
||||
"sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad",
|
||||
"sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba",
|
||||
"sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1",
|
||||
"sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e",
|
||||
"sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673",
|
||||
"sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13",
|
||||
"sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19"
|
||||
],
|
||||
"version": "==5.1"
|
||||
},
|
||||
"pyzmq": {
|
||||
"hashes": [
|
||||
"sha256:1651e52ed91f0736afd6d94ef9f3259b5534ce8beddb054f3d5ca989c4ef7c4f",
|
||||
"sha256:5ccb9b3d4cd20c000a9b75689d5add8cd3bce67fcbd0f8ae1b59345247d803af",
|
||||
"sha256:5e120c4cd3872e332fb35d255ad5998ebcee32ace4387b1b337416b6b90436c7",
|
||||
"sha256:5e2a3707c69a7281a9957f83718815fd74698cba31f6d69f9ed359921f662221",
|
||||
"sha256:63d51add9af8d0442dc90f916baf98fdc04e3b0a32afec4bfc83f8d85e72959f",
|
||||
"sha256:65c5a0bdc49e20f7d6b03a661f71e2fda7a99c51270cafe71598146d09810d0d",
|
||||
"sha256:66828fabe911aa545d919028441a585edb7c9c77969a5fea6722ef6e6ece38ab",
|
||||
"sha256:7d79427e82d9dad6e9b47c0b3e7ae5f9d489b1601e3a36ea629bb49501a4daf3",
|
||||
"sha256:824ee5d3078c4eae737ffc500fbf32f2b14e6ec89b26b435b7834febd70120cf",
|
||||
"sha256:89dc0a83cccec19ff3c62c091e43e66e0183d1e6b4658c16ee4e659518131494",
|
||||
"sha256:8b319805f6f7c907b101c864c3ca6cefc9db8ce0791356f180b1b644c7347e4c",
|
||||
"sha256:90facfb379ab47f94b19519c1ecc8ec8d10813b69d9c163117944948bdec5d15",
|
||||
"sha256:a0a178c7420021fc0730180a914a4b4b3092ce9696ceb8e72d0f60f8ce1655dd",
|
||||
"sha256:a7a89591ae315baccb8072f216614b3e59aed7385aef4393a6c741783d6ee9cf",
|
||||
"sha256:ba2578f0ae582452c02ed9fac2dc477b08e80ce05d2c0885becf5fff6651ccb0",
|
||||
"sha256:c69b0055c55702f5b0b6b354133e8325b9a56dbc80e1be2d240bead253fb9825",
|
||||
"sha256:ca434e1858fe222380221ddeb81e86f45522773344c9da63c311d17161df5e06",
|
||||
"sha256:d4b8ecfc3d92f114f04d5c40f60a65e5196198b827503341521dda12d8b14939",
|
||||
"sha256:d706025c47b09a54f005953ebe206f6d07a22516776faa4f509aaff681cc5468",
|
||||
"sha256:d8f27e958f8a2c0c8ffd4d8855c3ce8ac3fa1e105f0491ce31729aa2b3229740",
|
||||
"sha256:dbd264298f76b9060ce537008eb989317ca787c857e23cbd1b3ddf89f190a9b1",
|
||||
"sha256:e926d66f0df8fdbf03ba20583af0f215e475c667fb033d45fd031c66c63e34c9",
|
||||
"sha256:efc3bd48237f973a749f7312f68062f1b4ca5c2032a0673ca3ea8e46aa77187b",
|
||||
"sha256:f59bc782228777cbfe04555707a9c56d269c787ed25d6d28ed9d0fbb41cb1ad2",
|
||||
"sha256:f8da5322f4ff5f667a0d5a27e871b560c6637153c81e318b35cb012b2a98835c"
|
||||
],
|
||||
"version": "==18.0.1"
|
||||
},
|
||||
"qtconsole": {
|
||||
"hashes": [
|
||||
"sha256:a667558c7b1e1442a2e5bcef1686c55e096efd0b58d8b2a0a8415f4579991ee3",
|
||||
"sha256:fdfc6002d9d2834c88f9c92e0f6f590284ff3740fa53016f188a62d58bcca6d8"
|
||||
],
|
||||
"version": "==4.4.4"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
||||
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.22.0"
|
||||
},
|
||||
"send2trash": {
|
||||
"hashes": [
|
||||
"sha256:60001cc07d707fe247c94f74ca6ac0d3255aabcb930529690897ca2a39db28b2",
|
||||
"sha256:f1691922577b6fa12821234aeb57599d887c4900b9ca537948d2dac34aea888b"
|
||||
],
|
||||
"version": "==1.5.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:91c54ca8345008fceaec987e10924bf07dcab36c442925357e5a467b36a38319"
|
||||
],
|
||||
"version": "==1.3.3"
|
||||
},
|
||||
"terminado": {
|
||||
"hashes": [
|
||||
"sha256:d9d012de63acb8223ac969c17c3043337c2fcfd28f3aea1ee429b345d01ef460",
|
||||
"sha256:de08e141f83c3a0798b050ecb097ab6259c3f0331b2f7b7750c9075ced2c20c2"
|
||||
],
|
||||
"version": "==0.8.2"
|
||||
},
|
||||
"testpath": {
|
||||
"hashes": [
|
||||
"sha256:46c89ebb683f473ffe2aab0ed9f12581d4d078308a3cb3765d79c6b2317b0109",
|
||||
"sha256:b694b3d9288dbd81685c5d2e7140b81365d46c29f5db4bc659de5aa6b98780f8"
|
||||
],
|
||||
"version": "==0.4.2"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
|
||||
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
|
||||
],
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"tornado": {
|
||||
"hashes": [
|
||||
"sha256:1174dcb84d08887b55defb2cda1986faeeea715fff189ef3dc44cce99f5fca6b",
|
||||
"sha256:2613fab506bd2aedb3722c8c64c17f8f74f4070afed6eea17f20b2115e445aec",
|
||||
"sha256:44b82bc1146a24e5b9853d04c142576b4e8fa7a92f2e30bc364a85d1f75c4de2",
|
||||
"sha256:457fcbee4df737d2defc181b9073758d73f54a6cfc1f280533ff48831b39f4a8",
|
||||
"sha256:49603e1a6e24104961497ad0c07c799aec1caac7400a6762b687e74c8206677d",
|
||||
"sha256:8c2f40b99a8153893793559919a355d7b74649a11e59f411b0b0a1793e160bc0",
|
||||
"sha256:e1d897889c3b5a829426b7d52828fb37b28bc181cd598624e65c8be40ee3f7fa"
|
||||
],
|
||||
"version": "==6.0.2"
|
||||
},
|
||||
"traitlets": {
|
||||
"hashes": [
|
||||
"sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
|
||||
"sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"
|
||||
],
|
||||
"version": "==4.3.2"
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:132eae51d6ef3ff4a8c47c393a4ef5ebf0d1aecc96880eb5d6c8ceab7017cc9b",
|
||||
"sha256:18141c1484ab8784006c839be8b985cfc82a2e9725837b0ecfa0203f71c4e39d",
|
||||
"sha256:2baf617f5bbbfe73fd8846463f5aeafc912b5ee247f410700245d68525ec584a",
|
||||
"sha256:3d90063f2cbbe39177e9b4d888e45777012652d6110156845b828908c51ae462",
|
||||
"sha256:4304b2218b842d610aa1a1d87e1dc9559597969acc62ce717ee4dfeaa44d7eee",
|
||||
"sha256:4983ede548ffc3541bae49a82675996497348e55bafd1554dc4e4a5d6eda541a",
|
||||
"sha256:5315f4509c1476718a4825f45a203b82d7fdf2a6f5f0c8f166435975b1c9f7d4",
|
||||
"sha256:6cdfb1b49d5345f7c2b90d638822d16ba62dc82f7616e9b4caa10b72f3f16649",
|
||||
"sha256:7b325f12635598c604690efd7a0197d0b94b7d7778498e76e0710cd582fd1c7a",
|
||||
"sha256:8d3b0e3b8626615826f9a626548057c5275a9733512b137984a68ba1598d3d2f",
|
||||
"sha256:8f8631160c79f53081bd23446525db0bc4c5616f78d04021e6e434b286493fd7",
|
||||
"sha256:912de10965f3dc89da23936f1cc4ed60764f712e5fa603a09dd904f88c996760",
|
||||
"sha256:b010c07b975fe853c65d7bbe9d4ac62f1c69086750a574f6292597763781ba18",
|
||||
"sha256:c908c10505904c48081a5415a1e295d8403e353e0c14c42b6d67f8f97fae6616",
|
||||
"sha256:c94dd3807c0c0610f7c76f078119f4ea48235a953512752b9175f9f98f5ae2bd",
|
||||
"sha256:ce65dee7594a84c466e79d7fb7d3303e7295d16a83c22c7c4037071b059e2c21",
|
||||
"sha256:eaa9cfcb221a8a4c2889be6f93da141ac777eb8819f077e1d09fb12d00a09a93",
|
||||
"sha256:f3376bc31bad66d46d44b4e6522c5c21976bf9bca4ef5987bb2bf727f4506cbb",
|
||||
"sha256:f9202fa138544e13a4ec1a6792c35834250a85958fde1251b6a22e07d1260ae7"
|
||||
],
|
||||
"version": "==1.3.5"
|
||||
},
|
||||
"ujson": {
|
||||
"hashes": [
|
||||
"sha256:f66073e5506e91d204ab0c614a148d5aa938bdbf104751be66f8ad7a222f5f86"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.35"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:a53063d8b9210a7bdec15e7b272776b9d42b2fd6816401a0d43006ad2f9902db",
|
||||
"sha256:d363e3607d8de0c220d31950a8f38b18d5ba7c0830facd71a1c6b1036b7ce06c"
|
||||
],
|
||||
"version": "==1.25.2"
|
||||
},
|
||||
"uvicorn": {
|
||||
"hashes": [
|
||||
"sha256:c10da7a54a6552279870900c881a2f1726314e2dd6270d4d3f9251683c643783"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.7.1"
|
||||
},
|
||||
"uvloop": {
|
||||
"hashes": [
|
||||
"sha256:0fcd894f6fc3226a962ee7ad895c4f52e3f5c3c55098e21efb17c071849a0573",
|
||||
"sha256:2f31de1742c059c96cb76b91c5275b22b22b965c886ee1fced093fa27dde9e64",
|
||||
"sha256:459e4649fcd5ff719523de33964aa284898e55df62761e7773d088823ccbd3e0",
|
||||
"sha256:67867aafd6e0bc2c30a079603a85d83b94f23c5593b3cc08ec7e58ac18bf48e5",
|
||||
"sha256:8c200457e6847f28d8bb91c5e5039d301716f5f2fce25646f5fb3fd65eda4a26",
|
||||
"sha256:958906b9ca39eb158414fbb7d6b8ef1b7aee4db5c8e8e5d00fcbb69a1ce9dca7",
|
||||
"sha256:ac1dca3d8f3ef52806059e81042ee397ac939e5a86c8a3cea55d6b087db66115",
|
||||
"sha256:b284c22d8938866318e3b9d178142b8be316c52d16fcfe1560685a686718a021",
|
||||
"sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f",
|
||||
"sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe"
|
||||
],
|
||||
"version": "==0.12.2"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
|
||||
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
|
||||
],
|
||||
"version": "==0.1.7"
|
||||
},
|
||||
"webencodings": {
|
||||
"hashes": [
|
||||
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
|
||||
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
|
||||
],
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
"sha256:04b42a1b57096ffa5627d6a78ea1ff7fad3bc2c0331ffc17bc32a4024da7fea0",
|
||||
"sha256:08e3c3e0535befa4f0c4443824496c03ecc25062debbcf895874f8a0b4c97c9f",
|
||||
"sha256:10d89d4326045bf5e15e83e9867c85d686b612822e4d8f149cf4840aab5f46e0",
|
||||
"sha256:232fac8a1978fc1dead4b1c2fa27c7756750fb393eb4ac52f6bc87ba7242b2fa",
|
||||
"sha256:4bf4c8097440eff22bc78ec76fe2a865a6e658b6977a504679aaf08f02c121da",
|
||||
"sha256:51642ea3a00772d1e48fb0c492f0d3ae3b6474f34d20eca005a83f8c9c06c561",
|
||||
"sha256:55d86102282a636e195dad68aaaf85b81d0bef449d7e2ef2ff79ac450bb25d53",
|
||||
"sha256:564d2675682bd497b59907d2205031acbf7d3fadf8c763b689b9ede20300b215",
|
||||
"sha256:5d13bf5197a92149dc0badcc2b699267ff65a867029f465accfca8abab95f412",
|
||||
"sha256:5eda665f6789edb9b57b57a159b9c55482cbe5b046d7db458948370554b16439",
|
||||
"sha256:5edb2524d4032be4564c65dc4f9d01e79fe8fad5f966e5b552f4e5164fef0885",
|
||||
"sha256:79691794288bc51e2a3b8de2bc0272ca8355d0b8503077ea57c0716e840ebaef",
|
||||
"sha256:7fcc8681e9981b9b511cdee7c580d5b005f3bb86b65bde2188e04a29f1d63317",
|
||||
"sha256:8e447e05ec88b1b408a4c9cde85aa6f4b04f06aa874b9f0b8e8319faf51b1fee",
|
||||
"sha256:90ea6b3e7787620bb295a4ae050d2811c807d65b1486749414f78cfd6fb61489",
|
||||
"sha256:9e13239952694b8b831088431d15f771beace10edfcf9ef230cefea14f18508f",
|
||||
"sha256:d40f081187f7b54d7a99d8a5c782eaa4edc335a057aa54c85059272ed826dc09",
|
||||
"sha256:e1df1a58ed2468c7b7ce9a2f9752a32ad08eac2bcd56318625c3647c2cd2da6f",
|
||||
"sha256:e98d0cec437097f09c7834a11c69d79fe6241729b23f656cfc227e93294fc242",
|
||||
"sha256:f8d59627702d2ff27cb495ca1abdea8bd8d581de425c56e93bff6517134e0a9b",
|
||||
"sha256:fc30cdf2e949a2225b012a7911d1d031df3d23e99b7eda7dfc982dc4a860dae9"
|
||||
],
|
||||
"version": "==7.0"
|
||||
},
|
||||
"widgetsnbextension": {
|
||||
"hashes": [
|
||||
"sha256:14b2c65f9940c9a7d3b70adbe713dbd38b5ec69724eebaba034d1036cf3d4740",
|
||||
"sha256:fa618be8435447a017fd1bf2c7ae922d0428056cfc7449f7a8641edf76b48265"
|
||||
],
|
||||
"version": "==3.4.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
11
README.md
11
README.md
@@ -63,8 +63,19 @@ The key features are:
|
||||
|
||||
---
|
||||
|
||||
"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*"
|
||||
|
||||
"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> founders - <a href="https://spacy.io" target="_blank">spaCy</a> creators</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ It was one of the first extremely fast Python frameworks based on `asyncio`. It
|
||||
|
||||
Falcon is another high performance Python framework, it is designed to be minimal, and work as the foundation of other frameworks like Hug.
|
||||
|
||||
It uses the previous standard for Python web frameworks (WSGI) which is synchronous, so it can't handle Websockets and other use cases. Nevertheless, it also has a very good performance.
|
||||
It uses the previous standard for Python web frameworks (WSGI) which is synchronous, so it can't handle WebSockets and other use cases. Nevertheless, it also has a very good performance.
|
||||
|
||||
It is designed to have functions that receive two parameters, one "request" and one "response". Then you "read" parts from the request, and "write" parts to the response. Because of this design, it is not possible to declare request parameters and bodies with standard Python type hints as function parameters.
|
||||
|
||||
@@ -249,6 +249,10 @@ So, data validation, serialization, and documentation, have to be done in code,
|
||||
!!! check "Inspired **FastAPI** to"
|
||||
Find ways to get great performance.
|
||||
|
||||
Along with Hug (as Hug is based on Falcon) inspired **FastAPI** to declare a `response` parameter in functions.
|
||||
|
||||
Although in FastAPI it's optional, and is used mainly to set headers, cookies, and alternative status codes.
|
||||
|
||||
### <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:
|
||||
@@ -292,6 +296,7 @@ As it is based on the previous standard for synchronous Python web frameworks (W
|
||||
|
||||
Hug helped inspiring **FastAPI** to use Python type hints to declare parameters, and to generate a schema defining the API automatically.
|
||||
|
||||
Hug inspired **FastAPI** to declare a `response` parameter in functions to set headers and cookies.
|
||||
|
||||
### <a href="https://github.com/encode/apistar" target="_blank">APIStar</a> (<= 0.5)
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ But then <a href="https://letsencrypt.org/" target="_blank">Let's Encrypt</a> wa
|
||||
|
||||
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 domains 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.
|
||||
|
||||
|
||||
66
docs/external-links.md
Normal file
66
docs/external-links.md
Normal file
@@ -0,0 +1,66 @@
|
||||
**FastAPI** has a great community constantly growing.
|
||||
|
||||
There are many posts, articles, tools, and projects, related to **FastAPI**.
|
||||
|
||||
Here's an incomplete list of some of them.
|
||||
|
||||
!!! tip
|
||||
If you have an article, project, tool, or anything related to **FastAPI** that is not yet listed here, create a <a href="https://github.com/tiangolo/fastapi/edit/master/docs/external-links.md" target="_blank">Pull Request adding it</a>.
|
||||
|
||||
## Articles
|
||||
|
||||
### English
|
||||
|
||||
* <a href="https://medium.com/@williamhayes/fastapi-starlette-debug-vs-prod-5f7561db3a59" target="_blank">FastAPI/Starlette debug vs prod</a> by <a href="https://medium.com/@williamhayes" target="_blank">William Hayes</a>.
|
||||
|
||||
* <a href="https://medium.com/data-rebels/fastapi-google-as-an-external-authentication-provider-3a527672cf33" target="_blank">FastAPI — Google as an external authentication provider</a> by <a href="https://medium.com/@nils_29588" target="_blank">Nils de Bruin</a>.
|
||||
|
||||
* <a href="https://medium.com/data-rebels/fastapi-how-to-add-basic-and-cookie-authentication-a45c85ef47d3" target="_blank">FastAPI — How to add basic and cookie authentication</a> by <a href="https://medium.com/@nils_29588" target="_blank">Nils de Bruin</a>.
|
||||
|
||||
* <a href="https://dev.to/errietta/introduction-to-the-fastapi-python-framework-2n10" target="_blank">Introduction to the fastapi python framework</a> by <a href="https://dev.to/errietta" target="_blank">Errieta Kostala</a>.
|
||||
|
||||
* <a href="http://nickc1.github.io/api,/scikit-learn/2019/01/10/scikit-fastapi.html" target="_blank">FastAPI and Scikit-Learn: Easily Deploy Models</a> by <a href="http://nickc1.github.io/" target="_blank">Nick Cortale</a>.
|
||||
|
||||
* <a href="https://medium.com/data-rebels/fastapi-authentication-revisited-enabling-api-key-authentication-122dc5975680" target="_blank">FastAPI authentication revisited: Enabling API key authentication</a> by <a href="https://medium.com/@nils_29588" target="_blank">Nils de Bruin</a>.
|
||||
|
||||
* <a href="https://blog.bartab.fr/fastapi-logging-on-the-fly/" target="_blank">FastAPI, a simple use case on logging</a> by <a href="https://blog.bartab.fr/" target="_blank">@euri10</a>.
|
||||
|
||||
* <a href="https://medium.com/@nico.axtmann95/deploying-a-scikit-learn-model-with-onnx-und-fastapi-1af398268915" target="_blank">Deploying a scikit-learn model with ONNX and FastAPI</a> by <a href="https://www.linkedin.com/in/nico-axtmann" target="_blank">Nico Axtmann</a>.
|
||||
|
||||
* <a href="https://geekflare.com/python-asynchronous-web-frameworks/" target="_blank">Top 5 Asynchronous Web Frameworks for Python</a> by <a href="https://geekflare.com/author/ankush/" target="_blank">Ankush Thakur</a> on <a href="https://geekflare.com" target="_blank">GeekFlare</a>.
|
||||
|
||||
* <a href="https://medium.com/@gntrm/jwt-authentication-with-fastapi-and-aws-cognito-1333f7f2729e" target="_blank">JWT Authentication with FastAPI and AWS Cognito</a> by <a href="https://twitter.com/gntrm" target="_blank">Johannes Gontrum</a>.
|
||||
|
||||
* <a href="https://towardsdatascience.com/how-to-deploy-a-machine-learning-model-dc51200fe8cf" target="_blank">How to Deploy a Machine Learning Model</a> by <a href="https://www.linkedin.com/in/mgrootendorst/" target="_blank">Maarten Grootendorst</a> on <a href="https://towardsdatascience.com/" target="_blank">Towards Data Science</a>.
|
||||
|
||||
* <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank">Uber: Ludwig v0.2 Adds New Features and Other Improvements to its Deep Learning Toolbox [including a FastAPI server]</a> on <a href="https://eng.uber.com" target="_blank">Uber Engineering</a>.
|
||||
|
||||
### Japanese
|
||||
|
||||
* <a href="https://qiita.com/mtitg/items/47770e9a562dd150631d" target="_blank">FastAPI|DB接続してCRUDするPython製APIサーバーを構築</a> by <a href="https://qiita.com/mtitg" target="_blank">@mtitg</a>.
|
||||
|
||||
* <a href="https://qiita.com/ryoryomaru/items/59958ed385b3571d50de" target="_blank">python製の最新APIフレームワーク FastAPI を触ってみた</a> by <a href="https://qiita.com/ryoryomaru" target="_blank">@ryoryomaru</a>.
|
||||
|
||||
* <a href="https://qiita.com/angel_katayoku/items/0e1f5dbbe62efc612a78" target="_blank">FastAPIでCORSを回避</a> by <a href="https://qiita.com/angel_katayoku" target="_blank">@angel_katayoku</a>.
|
||||
|
||||
* <a href="https://qiita.com/angel_katayoku/items/4fbc1a4e2b33fa2237d2" target="_blank">FastAPIをMySQLと接続してDockerで管理してみる</a> by <a href="https://qiita.com/angel_katayoku" target="_blank">@angel_katayoku</a>.
|
||||
|
||||
* <a href="https://qiita.com/angel_katayoku/items/8a458a8952f50b73f420" target="_blank">FastAPIでPOSTされたJSONのレスポンスbodyを受け取る</a> by <a href="https://qiita.com/angel_katayoku" target="_blank">@angel_katayoku</a>.
|
||||
|
||||
* <a href="https://qiita.com/hikarut/items/b178af2e2440c67c6ac4" target="_blank">フロントエンド開発者向けのDockerによるPython開発環境構築</a> by <a href="https://qiita.com/hikarut" target="_blank">Hikaru Takahashi</a>.
|
||||
|
||||
### Chinese
|
||||
|
||||
* <a href="https://cloud.tencent.com/developer/article/1431448" target="_blank">使用FastAPI框架快速构建高性能的api服务</a> by <a href="https://cloud.tencent.com/developer/user/5471722" target="_blank">逍遥散人</a>.
|
||||
|
||||
### Vietnamese
|
||||
|
||||
* <a href="https://fullstackstation.com/fastapi-trien-khai-bang-docker/" target="_blank">FASTAPI: TRIỂN KHAI BẰNG DOCKER</a> by <a href="https://fullstackstation.com/author/figonking/" target="_blank">Nguyễn Nhân</a>.
|
||||
|
||||
### Russian
|
||||
|
||||
* <a href="https://habr.com/ru/post/454440/" target="_blank">Мелкая питонячая радость #2: Starlette - Солидная примочка – FastAPI</a> by <a href="https://habr.com/ru/users/57uff3r/" target="_blank">Andrey Korchak</a>.
|
||||
|
||||
## Podcasts
|
||||
|
||||
* <a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">FastAPI on PythonBytes</a> by <a href="https://pythonbytes.fm/" target="_blank">Python Bytes FM</a>.
|
||||
@@ -37,7 +37,7 @@ from datetime import date
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Declare a variable as an str
|
||||
# Declare a variable as a str
|
||||
# and get editor support inside the function
|
||||
def main(user_id: str):
|
||||
return user_id
|
||||
|
||||
@@ -70,10 +70,8 @@ You can let me know:
|
||||
|
||||
## Vote for FastAPI
|
||||
|
||||
You can vote to include FastAPI in several "awesome lists":
|
||||
|
||||
* <a href="https://github.com/vinta/awesome-python/pull/1209" target="_blank">Vote to include **FastAPI** in `awesome-python`</a>.
|
||||
* <a href="https://github.com/timofurrer/awesome-asyncio/pull/43" target="_blank">Vote to include **FastAPI** in `awesome-asyncio`</a>.
|
||||
* <a href="https://www.slant.co/options/34241/~fastapi-review" target="_blank">Vote for **FastAPI** in Slant</a>.
|
||||
|
||||
## Help others with issues in GitHub
|
||||
|
||||
|
||||
BIN
docs/img/tutorial/path-params/image03.png
Normal file
BIN
docs/img/tutorial/path-params/image03.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
@@ -63,8 +63,19 @@ The key features are:
|
||||
|
||||
---
|
||||
|
||||
"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*"
|
||||
|
||||
"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> founders - <a href="https://spacy.io" target="_blank">spaCy</a> creators</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -1,4 +1,216 @@
|
||||
## Next release
|
||||
## Latest changes
|
||||
|
||||
## 0.41.0
|
||||
|
||||
* Upgrade required Starlette to `0.12.9`, the new range is `>=0.12.9,<=0.12.9`.
|
||||
* Add `State` to FastAPI apps at `app.state`.
|
||||
* PR [#593](https://github.com/tiangolo/fastapi/pull/593).
|
||||
* Improve handling of custom classes for `Request`s and `APIRoute`s.
|
||||
* This helps to more easily solve use cases like:
|
||||
* Reading a body before and/or after a request (equivalent to a middleware).
|
||||
* Run middleware-like code only for a subset of *path operations*.
|
||||
* Process a request before passing it to a *path operation function*. E.g. decompressing, deserializing, etc.
|
||||
* Processing a response after being generated by *path operation functions* but before returning it. E.g. adding custom headers, logging, adding extra metadata.
|
||||
* New docs section: [Custom Request and APIRoute class](https://fastapi.tiangolo.com/tutorial/custom-request-and-route/).
|
||||
* PR [#589](https://github.com/tiangolo/fastapi/pull/589) by [@dmontagu](https://github.com/dmontagu).
|
||||
* Fix preserving custom route class in routers when including other sub-routers. PR [#538](https://github.com/tiangolo/fastapi/pull/538) by [@dmontagu](https://github.com/dmontagu).
|
||||
|
||||
## 0.40.0
|
||||
|
||||
* Add notes to docs about installing `python-multipart` when using forms. PR [#574](https://github.com/tiangolo/fastapi/pull/574) by [@sliptonic](https://github.com/sliptonic).
|
||||
* Generate OpenAPI schemas in alphabetical order. PR [#554](https://github.com/tiangolo/fastapi/pull/554) by [@dmontagu](https://github.com/dmontagu).
|
||||
* Add support for truncating docstrings from *path operation functions*.
|
||||
* New docs at [Advanced description from docstring](https://fastapi.tiangolo.com/tutorial/path-operation-advanced-configuration/#advanced-description-from-docstring).
|
||||
* PR [#556](https://github.com/tiangolo/fastapi/pull/556) by [@svalouch](https://github.com/svalouch).
|
||||
* Fix `DOCTYPE` in HTML files generated for Swagger UI and ReDoc. PR [#537](https://github.com/tiangolo/fastapi/pull/537) by [@Trim21](https://github.com/Trim21).
|
||||
* Fix handling `4XX` responses overriding default `422` validation error responses. PR [#517](https://github.com/tiangolo/fastapi/pull/517) by [@tsouvarev](https://github.com/tsouvarev).
|
||||
* Fix typo in documentation for [Simple HTTP Basic Auth](https://fastapi.tiangolo.com/tutorial/security/http-basic-auth/#simple-http-basic-auth). PR [#514](https://github.com/tiangolo/fastapi/pull/514) by [@prostomarkeloff](https://github.com/prostomarkeloff).
|
||||
* Fix incorrect documentation example in [first steps](https://fastapi.tiangolo.com/tutorial/first-steps/). PR [#511](https://github.com/tiangolo/fastapi/pull/511) by [@IgnatovFedor](https://github.com/IgnatovFedor).
|
||||
* Add support for Swagger UI [initOauth](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/oauth2.md) settings with the parameter `swagger_ui_init_oauth`. PR [#499](https://github.com/tiangolo/fastapi/pull/499) by [@zamiramir](https://github.com/zamiramir).
|
||||
|
||||
## 0.39.0
|
||||
|
||||
* Allow path parameters to have default values (e.g. `None`) and discard them instead of raising an error.
|
||||
* This allows declaring a parameter like `user_id: str = None` that can be taken from a query parameter, but the same path operation can be included in a router with a path `/users/{user_id}`, in which case will be taken from the path and will be required.
|
||||
* PR [#464](https://github.com/tiangolo/fastapi/pull/464) by [@jonathanunderwood](https://github.com/jonathanunderwood).
|
||||
* Add support for setting a `default_response_class` in the `FastAPI` instance or in `include_router`. Initial PR [#467](https://github.com/tiangolo/fastapi/pull/467) by [@toppk](https://github.com/toppk).
|
||||
* Add support for type annotations using strings and `from __future__ import annotations`. PR [#451](https://github.com/tiangolo/fastapi/pull/451) by [@dmontagu](https://github.com/dmontagu).
|
||||
|
||||
## 0.38.1
|
||||
|
||||
* Fix incorrect `Request` class import. PR [#493](https://github.com/tiangolo/fastapi/pull/493) by [@kamalgill](https://github.com/kamalgill).
|
||||
|
||||
## 0.38.0
|
||||
|
||||
* Add recent articles to [External Links](https://fastapi.tiangolo.com/external-links/) and recent opinions. PR [#490](https://github.com/tiangolo/fastapi/pull/490).
|
||||
* Upgrade support range for Starlette to include `0.12.8`. The new range is `>=0.11.1,<=0.12.8"`. PR [#477](https://github.com/tiangolo/fastapi/pull/477) by [@dmontagu](https://github.com/dmontagu).
|
||||
* Upgrade support to Pydantic version 0.32.2 and update internal code to use it (breaking change). PR [#463](https://github.com/tiangolo/fastapi/pull/463) by [@dmontagu](https://github.com/dmontagu).
|
||||
|
||||
## 0.37.0
|
||||
|
||||
* Add support for custom route classes for advanced use cases. PR [#468](https://github.com/tiangolo/fastapi/pull/468) by [@dmontagu](https://github.com/dmontagu).
|
||||
* Allow disabling Google fonts in ReDoc. PR [#481](https://github.com/tiangolo/fastapi/pull/481) by [@b1-luettje](https://github.com/b1-luettje).
|
||||
* Fix security issue: when returning a sub-class of a response model and using `skip_defaults` it could leak information. PR [#485](https://github.com/tiangolo/fastapi/pull/485) by [@dmontagu](https://github.com/dmontagu).
|
||||
* Enable tests for Python 3.8-dev. PR [#465](https://github.com/tiangolo/fastapi/pull/465) by [@Jamim](https://github.com/Jamim).
|
||||
* Add support and tests for Pydantic dataclasses in `response_model`. PR [#454](https://github.com/tiangolo/fastapi/pull/454) by [@dconathan](https://github.com/dconathan).
|
||||
* Fix typo in OAuth2 JWT tutorial. PR [#447](https://github.com/tiangolo/fastapi/pull/447) by [@pablogamboa](https://github.com/pablogamboa).
|
||||
* Use the `media_type` parameter in `Body()` params to set the media type in OpenAPI for `requestBody`. PR [#439](https://github.com/tiangolo/fastapi/pull/439) by [@divums](https://github.com/divums).
|
||||
* Add article [Deploying a scikit-learn model with ONNX and FastAPI](https://medium.com/@nico.axtmann95/deploying-a-scikit-learn-model-with-onnx-und-fastapi-1af398268915) by [https://www.linkedin.com/in/nico-axtmann](Nico Axtmann). PR [#438](https://github.com/tiangolo/fastapi/pull/438) by [@naxty](https://github.com/naxty).
|
||||
* Allow setting custom `422` (validation error) response/schema in OpenAPI.
|
||||
* And use media type from response class instead of fixed `application/json` (the default).
|
||||
* PR [#437](https://github.com/tiangolo/fastapi/pull/437) by [@divums](https://github.com/divums).
|
||||
* Fix using `"default"` extra response with status codes at the same time. PR [#489](https://github.com/tiangolo/fastapi/pull/489).
|
||||
* Allow additional responses to use status code ranges (like `5XX` and `4XX`) and `"default"`. PR [#435](https://github.com/tiangolo/fastapi/pull/435) by [@divums](https://github.com/divums).
|
||||
|
||||
## 0.36.0
|
||||
|
||||
* Fix implementation for `skip_defaults` when returning a Pydantic model. PR [#422](https://github.com/tiangolo/fastapi/pull/422) by [@dmontagu](https://github.com/dmontagu).
|
||||
* Fix OpenAPI generation when using the same dependency in multiple places for the same *path operation*. PR [#417](https://github.com/tiangolo/fastapi/pull/417) by [@dmontagu](https://github.com/dmontagu).
|
||||
* Allow having empty paths in *path operations* used with `include_router` and a `prefix`.
|
||||
* This allows having a router for `/cats` and all its *path operations*, while having one of them for `/cats`.
|
||||
* Now it doesn't have to be only `/cats/` (with a trailing slash).
|
||||
* To use it, declare the path in the *path operation* as the empty string (`""`).
|
||||
* PR [#415](https://github.com/tiangolo/fastapi/pull/415) by [@vitalik](https://github.com/vitalik).
|
||||
* Fix mypy error after merging PR #415. PR [#462](https://github.com/tiangolo/fastapi/pull/462).
|
||||
|
||||
## 0.35.0
|
||||
|
||||
* Fix typo in routing `assert`. PR [#419](https://github.com/tiangolo/fastapi/pull/419) by [@pablogamboa](https://github.com/pablogamboa).
|
||||
* Fix typo in docs. PR [#411](https://github.com/tiangolo/fastapi/pull/411) by [@bronsen](https://github.com/bronsen).
|
||||
* Fix parsing a body type declared with `Union`. PR [#400](https://github.com/tiangolo/fastapi/pull/400) by [@koxudaxi](https://github.com/koxudaxi).
|
||||
|
||||
## 0.34.0
|
||||
|
||||
* Upgrade Starlette supported range to include the latest `0.12.7`. The new range is `0.11.1,<=0.12.7`. PR [#367](https://github.com/tiangolo/fastapi/pull/367) by [@dedsm](https://github.com/dedsm).
|
||||
|
||||
* Add test for OpenAPI schema with duplicate models from PR [#333](https://github.com/tiangolo/fastapi/pull/333) by [@dmontagu](https://github.com/dmontagu). PR [#385](https://github.com/tiangolo/fastapi/pull/385).
|
||||
|
||||
## 0.33.0
|
||||
|
||||
* Upgrade Pydantic version to `0.30.0`. PR [#384](https://github.com/tiangolo/fastapi/pull/384) by [@jekirl](https://github.com/jekirl).
|
||||
|
||||
## 0.32.0
|
||||
|
||||
* Fix typo in docs for features. PR [#380](https://github.com/tiangolo/fastapi/pull/380) by [@MartinoMensio](https://github.com/MartinoMensio).
|
||||
|
||||
* Fix source code `limit` for example in [Query Parameters](https://fastapi.tiangolo.com/tutorial/query-params/). PR [#366](https://github.com/tiangolo/fastapi/pull/366) by [@Smashman](https://github.com/Smashman).
|
||||
|
||||
* Update wording in docs about [OAuth2 scopes](https://fastapi.tiangolo.com/tutorial/security/oauth2-scopes/). PR [#371](https://github.com/tiangolo/fastapi/pull/371) by [@cjw296](https://github.com/cjw296).
|
||||
|
||||
* Update docs for `Enum`s to inherit from `str` and improve Swagger UI rendering. PR [#351](https://github.com/tiangolo/fastapi/pull/351).
|
||||
|
||||
* Fix regression, add Swagger UI deep linking again. PR [#350](https://github.com/tiangolo/fastapi/pull/350).
|
||||
|
||||
* Add test for having path templates in `prefix` of `.include_router`. PR [#349](https://github.com/tiangolo/fastapi/pull/349).
|
||||
|
||||
* Add note to docs: [Include the same router multiple times with different `prefix`](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-the-same-router-multiple-times-with-different-prefix). PR [#348](https://github.com/tiangolo/fastapi/pull/348).
|
||||
|
||||
* Fix OpenAPI/JSON Schema generation for two functions with the same name (in different modules) with the same composite bodies.
|
||||
* Composite bodies' IDs are now based on path, not only on route name, as the auto-generated name uses the function names, that can be duplicated in different modules.
|
||||
* The same new ID generation applies to response models.
|
||||
* This also changes the generated title for those models.
|
||||
* Only composite bodies and response models are affected because those are generated dynamically, they don't have a module (a Python file).
|
||||
* This also adds the possibility of using `.include_router()` with the same `APIRouter` *multiple* times, with different prefixes, e.g. `/api/v2` and `/api/latest`, and it will now work correctly.
|
||||
* PR [#347](https://github.com/tiangolo/fastapi/pull/347).
|
||||
|
||||
## 0.31.0
|
||||
|
||||
* Upgrade Pydantic supported version to `0.29.0`.
|
||||
* New supported version range is `"pydantic >=0.28,<=0.29.0"`.
|
||||
* This adds support for Pydantic [Generic Models](https://pydantic-docs.helpmanual.io/#generic-models), kudos to [@dmontagu](https://github.com/dmontagu).
|
||||
* PR [#344](https://github.com/tiangolo/fastapi/pull/344).
|
||||
|
||||
## 0.30.1
|
||||
|
||||
* Add section in docs about [External Links and Articles](https://fastapi.tiangolo.com/external-links/). PR [#341](https://github.com/tiangolo/fastapi/pull/341).
|
||||
|
||||
* Remove `Pipfile.lock` from the repository as it is only used by FastAPI contributors (developers of FastAPI itself). See the PR for more details. PR [#340](https://github.com/tiangolo/fastapi/pull/340).
|
||||
|
||||
* Update section about [Help FastAPI - Get Help](https://fastapi.tiangolo.com/help-fastapi/). PR [#339](https://github.com/tiangolo/fastapi/pull/339).
|
||||
|
||||
* Refine internal type declarations to improve/remove Mypy errors in users' code. PR [#338](https://github.com/tiangolo/fastapi/pull/338).
|
||||
|
||||
* Update and clarify [SQL tutorial with SQLAlchemy](https://fastapi.tiangolo.com/tutorial/sql-databases/). PR [#331](https://github.com/tiangolo/fastapi/pull/331) by [@mariacamilagl](https://github.com/mariacamilagl).
|
||||
|
||||
* Add SQLite [online viewers to the docs](https://fastapi.tiangolo.com/tutorial/sql-databases/#interact-with-the-database-directly). PR [#330](https://github.com/tiangolo/fastapi/pull/330) by [@cyrilbois](https://github.com/cyrilbois).
|
||||
|
||||
## 0.30.0
|
||||
|
||||
* Add support for Pydantic's ORM mode:
|
||||
* Updated documentation about SQL with SQLAlchemy, using Pydantic models with ORM mode, SQLAlchemy models with relations, separation of files, simplification of code and other changes. New docs: [SQL (Relational) Databases](https://fastapi.tiangolo.com/tutorial/sql-databases/).
|
||||
* The new support for ORM mode fixes issues/adds features related to ORMs with lazy-loading, hybrid properties, dynamic/getters (using `@property` decorators) and several other use cases.
|
||||
* This applies to ORMs like SQLAlchemy, Peewee, Tortoise ORM, GINO ORM and virtually any other.
|
||||
* If your *path operations* return an arbitrary object with attributes (e.g. `my_item.name` instead of `my_item["name"]`) AND you use a `response_model`, make sure to update the Pydantic models with `orm_mode = True` as described in the docs (link above).
|
||||
* New documentation about receiving plain `dict`s as request bodies: [Bodies of arbitrary `dict`s](https://fastapi.tiangolo.com/tutorial/body-nested-models/#bodies-of-arbitrary-dicts).
|
||||
* New documentation about returning arbitrary `dict`s in responses: [Response with arbitrary `dict`](https://fastapi.tiangolo.com/tutorial/extra-models/#response-with-arbitrary-dict).
|
||||
* **Technical Details**:
|
||||
* When declaring a `response_model` it is used directly to generate the response content, from whatever was returned from the *path operation function*.
|
||||
* Before this, the return content was first passed through `jsonable_encoder` to ensure it was a "jsonable" object, like a `dict`, instead of an arbitrary object with attributes (like an ORM model). That's why you should make sure to update your Pydantic models for objects with attributes to use `orm_mode = True`.
|
||||
* If you don't have a `response_model`, the return object will still be passed through `jsonable_encoder` first.
|
||||
* When a `response_model` is declared, the same `response_model` type declaration won't be used as is, it will be "cloned" to create an new one (a cloned Pydantic `Field` with all the submodels cloned as well).
|
||||
* This avoids/fixes a potential security issue: as the returned object is passed directly to Pydantic, if the returned object was a subclass of the `response_model` (e.g. you return a `UserInDB` that inherits from `User` but contains extra fields, like `hashed_password`, and `User` is used in the `response_model`), it would still pass the validation (because `UserInDB` is a subclass of `User`) and the object would be returned as-is, including the `hashed_password`. To fix this, the declared `response_model` is cloned, if it is a Pydantic model class (or contains Pydantic model classes in it, e.g. in a `List[Item]`), the Pydantic model class(es) will be a different one (the "cloned" one). So, an object that is a subclass won't simply pass the validation and returned as-is, because it is no longer a sub-class of the cloned `response_model`. Instead, a new Pydantic model object will be created with the contents of the returned object. So, it will be a new object (made with the data from the returned one), and will be filtered by the cloned `response_model`, containing only the declared fields as normally.
|
||||
* PR [#322](https://github.com/tiangolo/fastapi/pull/322).
|
||||
|
||||
* Remove/clean unused RegEx code in routing. PR [#314](https://github.com/tiangolo/fastapi/pull/314) by [@dmontagu](https://github.com/dmontagu).
|
||||
|
||||
* Use default response status code descriptions for additional responses. PR [#313](https://github.com/tiangolo/fastapi/pull/313) by [@duxiaoyao](https://github.com/duxiaoyao).
|
||||
|
||||
* Upgrade Pydantic support to `0.28`. PR [#320](https://github.com/tiangolo/fastapi/pull/320) by [@jekirl](https://github.com/jekirl).
|
||||
|
||||
## 0.29.1
|
||||
|
||||
* Fix handling an empty-body request with a required body param. PR [#311](https://github.com/tiangolo/fastapi/pull/311).
|
||||
|
||||
* Fix broken link in docs: [Return a Response directly](https://fastapi.tiangolo.com/tutorial/response-directly/). PR [#306](https://github.com/tiangolo/fastapi/pull/306) by [@dmontagu](https://github.com/dmontagu).
|
||||
|
||||
* Fix docs discrepancy in docs for [Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). PR [#288](https://github.com/tiangolo/fastapi/pull/288) by [@awiddersheim](https://github.com/awiddersheim).
|
||||
|
||||
## 0.29.0
|
||||
|
||||
* Add support for declaring a `Response` parameter:
|
||||
* This allows declaring:
|
||||
* [Response Cookies](https://fastapi.tiangolo.com/tutorial/response-cookies/).
|
||||
* [Response Headers](https://fastapi.tiangolo.com/tutorial/response-headers/).
|
||||
* An HTTP Status Code different than the default: [Response - Change Status Code](https://fastapi.tiangolo.com/tutorial/response-change-status-code/).
|
||||
* All of this while still being able to return arbitrary objects (`dict`, DB model, etc).
|
||||
* Update attribution to Hug, for inspiring the `response` parameter pattern.
|
||||
* PR [#294](https://github.com/tiangolo/fastapi/pull/294).
|
||||
|
||||
## 0.28.0
|
||||
|
||||
* Implement dependency cache per request.
|
||||
* This avoids calling each dependency multiple times for the same request.
|
||||
* This is useful while calling external services, performing costly computation, etc.
|
||||
* This also means that if a dependency was declared as a *path operation decorator* dependency, possibly at the router level (with `.include_router()`) and then it is declared again in a specific *path operation*, the dependency will be called only once.
|
||||
* The cache can be disabled per dependency declaration, using `use_cache=False` as in `Depends(your_dependency, use_cache=False)`.
|
||||
* Updated docs at: [Using the same dependency multiple times](https://fastapi.tiangolo.com/tutorial/dependencies/sub-dependencies/#using-the-same-dependency-multiple-times).
|
||||
* PR [#292](https://github.com/tiangolo/fastapi/pull/292).
|
||||
|
||||
* Implement dependency overrides for testing.
|
||||
* This allows using overrides/mocks of dependencies during tests.
|
||||
* New docs: [Testing Dependencies with Overrides](https://fastapi.tiangolo.com/tutorial/testing-dependencies/).
|
||||
* PR [#291](https://github.com/tiangolo/fastapi/pull/291).
|
||||
|
||||
## 0.27.2
|
||||
|
||||
* Fix path and query parameters receiving `dict` as a valid type. It should be mapped to a body payload. PR [#287](https://github.com/tiangolo/fastapi/pull/287). Updated docs at: [Query parameter list / multiple values with defaults: Using `list`](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#using-list).
|
||||
|
||||
## 0.27.1
|
||||
|
||||
* Fix `auto_error=False` handling in `HTTPBearer` security scheme. Do not `raise` when there's an incorrect `Authorization` header if `auto_error=False`. PR [#282](https://github.com/tiangolo/fastapi/pull/282).
|
||||
|
||||
* Fix type declaration of `HTTPException`. PR [#279](https://github.com/tiangolo/fastapi/pull/279).
|
||||
|
||||
## 0.27.0
|
||||
|
||||
* Fix broken link in docs about OAuth 2.0 with scopes. PR [#275](https://github.com/tiangolo/fastapi/pull/275) by [@dmontagu](https://github.com/dmontagu).
|
||||
|
||||
* Refactor param extraction using Pydantic `Field`:
|
||||
* Large refactor, improvement, and simplification of param extraction from *path operations*.
|
||||
* Fix/add support for list *query parameters* with list defaults. New documentation: [Query parameter list / multiple values with defaults](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#query-parameter-list-multiple-values-with-defaults).
|
||||
* Add support for enumerations in *path operation* parameters. New documentation: [Path Parameters: Predefined values](https://fastapi.tiangolo.com/tutorial/path-params/#predefined-values).
|
||||
* Add support for type annotations using `Optional` as in `param: Optional[str] = None`. New documentation: [Optional type declarations](https://fastapi.tiangolo.com/tutorial/query-params/#optional-type-declarations).
|
||||
* PR [#278](https://github.com/tiangolo/fastapi/pull/278).
|
||||
|
||||
## 0.26.0
|
||||
|
||||
|
||||
10
docs/src/body_nested_models/tutorial009.py
Normal file
10
docs/src/body_nested_models/tutorial009.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/index-weights/")
|
||||
async def create_index_weights(weights: Dict[int, float]):
|
||||
return weights
|
||||
37
docs/src/custom_request_and_route/tutorial001.py
Normal file
37
docs/src/custom_request_and_route/tutorial001.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import gzip
|
||||
from typing import Callable, List
|
||||
|
||||
from fastapi import Body, FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
|
||||
class GzipRequest(Request):
|
||||
async def body(self) -> bytes:
|
||||
if not hasattr(self, "_body"):
|
||||
body = await super().body()
|
||||
if "gzip" in self.headers.getlist("Content-Encoding"):
|
||||
body = gzip.decompress(body)
|
||||
self._body = body
|
||||
return self._body
|
||||
|
||||
|
||||
class GzipRoute(APIRoute):
|
||||
def get_route_handler(self) -> Callable:
|
||||
original_route_handler = super().get_route_handler()
|
||||
|
||||
async def custom_route_handler(request: Request) -> Response:
|
||||
request = GzipRequest(request.scope, request.receive)
|
||||
return await original_route_handler(request)
|
||||
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
app.router.route_class = GzipRoute
|
||||
|
||||
|
||||
@app.post("/sum")
|
||||
async def sum_numbers(numbers: List[int] = Body(...)):
|
||||
return {"sum": sum(numbers)}
|
||||
31
docs/src/custom_request_and_route/tutorial002.py
Normal file
31
docs/src/custom_request_and_route/tutorial002.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from typing import Callable, List
|
||||
|
||||
from fastapi import Body, FastAPI, HTTPException
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
|
||||
class ValidationErrorLoggingRoute(APIRoute):
|
||||
def get_route_handler(self) -> Callable:
|
||||
original_route_handler = super().get_route_handler()
|
||||
|
||||
async def custom_route_handler(request: Request) -> Response:
|
||||
try:
|
||||
return await original_route_handler(request)
|
||||
except RequestValidationError as exc:
|
||||
body = await request.body()
|
||||
detail = {"errors": exc.errors(), "body": body.decode()}
|
||||
raise HTTPException(status_code=422, detail=detail)
|
||||
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
app.router.route_class = ValidationErrorLoggingRoute
|
||||
|
||||
|
||||
@app.post("/")
|
||||
async def sum_numbers(numbers: List[int] = Body(...)):
|
||||
return sum(numbers)
|
||||
41
docs/src/custom_request_and_route/tutorial003.py
Normal file
41
docs/src/custom_request_and_route/tutorial003.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
|
||||
class TimedRoute(APIRoute):
|
||||
def get_route_handler(self) -> Callable:
|
||||
original_route_handler = super().get_route_handler()
|
||||
|
||||
async def custom_route_handler(request: Request) -> Response:
|
||||
before = time.time()
|
||||
response: Response = await original_route_handler(request)
|
||||
duration = time.time() - before
|
||||
response.headers["X-Response-Time"] = str(duration)
|
||||
print(f"route duration: {duration}")
|
||||
print(f"route response: {response}")
|
||||
print(f"route response headers: {response.headers}")
|
||||
return response
|
||||
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
router = APIRouter(route_class=TimedRoute)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def not_timed():
|
||||
return {"message": "Not timed"}
|
||||
|
||||
|
||||
@router.get("/timed")
|
||||
async def timed():
|
||||
return {"message": "It's the time of my life"}
|
||||
|
||||
|
||||
app.include_router(router)
|
||||
55
docs/src/dependency_testing/tutorial001.py
Normal file
55
docs/src/dependency_testing/tutorial001.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from fastapi import Depends, FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
async def common_parameters(q: str = None, skip: int = 0, limit: int = 100):
|
||||
return {"q": q, "skip": skip, "limit": limit}
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items(commons: dict = Depends(common_parameters)):
|
||||
return {"message": "Hello Items!", "params": commons}
|
||||
|
||||
|
||||
@app.get("/users/")
|
||||
async def read_users(commons: dict = Depends(common_parameters)):
|
||||
return {"message": "Hello Users!", "params": commons}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
async def override_dependency(q: str = None):
|
||||
return {"q": q, "skip": 5, "limit": 10}
|
||||
|
||||
|
||||
app.dependency_overrides[common_parameters] = override_dependency
|
||||
|
||||
|
||||
def test_override_in_items():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"message": "Hello Items!",
|
||||
"params": {"q": None, "skip": 5, "limit": 10},
|
||||
}
|
||||
|
||||
|
||||
def test_override_in_items_with_q():
|
||||
response = client.get("/items/?q=foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"message": "Hello Items!",
|
||||
"params": {"q": "foo", "skip": 5, "limit": 10},
|
||||
}
|
||||
|
||||
|
||||
def test_override_in_items_with_params():
|
||||
response = client.get("/items/?q=foo&skip=100&limit=200")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"message": "Hello Items!",
|
||||
"params": {"q": "foo", "skip": 5, "limit": 10},
|
||||
}
|
||||
10
docs/src/extra_models/tutorial005.py
Normal file
10
docs/src/extra_models/tutorial005.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/keyword-weights/", response_model=Dict[str, float])
|
||||
async def read_keyword_weights():
|
||||
return {"foo": 2.3, "bar": 3.4}
|
||||
@@ -0,0 +1,30 @@
|
||||
from typing import Set
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
description: str = None
|
||||
price: float
|
||||
tax: float = None
|
||||
tags: Set[str] = []
|
||||
|
||||
|
||||
@app.post("/items/", response_model=Item, summary="Create an item")
|
||||
async def create_item(*, item: Item):
|
||||
"""
|
||||
Create an item with all the information:
|
||||
|
||||
- **name**: each item must have a name
|
||||
- **description**: a long description
|
||||
- **price**: required
|
||||
- **tax**: if the item doesn't have tax, you can omit this
|
||||
- **tags**: a set of unique tag strings for this item
|
||||
\f
|
||||
:param item: User input.
|
||||
"""
|
||||
return item
|
||||
21
docs/src/path_params/tutorial005.py
Normal file
21
docs/src/path_params/tutorial005.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from enum import Enum
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
|
||||
class ModelName(str, Enum):
|
||||
alexnet = "alexnet"
|
||||
resnet = "resnet"
|
||||
lenet = "lenet"
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/model/{model_name}")
|
||||
async def get_model(model_name: ModelName):
|
||||
if model_name == ModelName.alexnet:
|
||||
return {"model_name": model_name, "message": "Deep Learning FTW!"}
|
||||
if model_name.value == "lenet":
|
||||
return {"model_name": model_name, "message": "LeCNN all the images"}
|
||||
return {"model_name": model_name, "message": "Have some residuals"}
|
||||
@@ -6,5 +6,5 @@ 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):
|
||||
async def read_item(skip: int = 0, limit: int = 10):
|
||||
return fake_items_db[skip : skip + limit]
|
||||
|
||||
11
docs/src/query_params/tutorial007.py
Normal file
11
docs/src/query_params/tutorial007.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def read_user_item(item_id: str, limit: Optional[int] = None):
|
||||
item = {"item_id": item_id, "limit": limit}
|
||||
return item
|
||||
11
docs/src/query_params_str_validations/tutorial012.py
Normal file
11
docs/src/query_params_str_validations/tutorial012.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI, Query
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items(q: List[str] = Query(["foo", "bar"])):
|
||||
query_items = {"q": q}
|
||||
return query_items
|
||||
9
docs/src/query_params_str_validations/tutorial013.py
Normal file
9
docs/src/query_params_str_validations/tutorial013.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from fastapi import FastAPI, Query
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items(q: list = Query(None)):
|
||||
query_items = {"q": q}
|
||||
return query_items
|
||||
15
docs/src/response_change_status_code/tutorial001.py
Normal file
15
docs/src/response_change_status_code/tutorial001.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.responses import Response
|
||||
from starlette.status import HTTP_201_CREATED
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
tasks = {"foo": "Listen to the Bar Fighters"}
|
||||
|
||||
|
||||
@app.put("/get-or-create-task/{task_id}", status_code=200)
|
||||
def get_or_create_task(task_id: str, response: Response):
|
||||
if task_id not in tasks:
|
||||
tasks[task_id] = "This didn't exist before"
|
||||
response.status_code = HTTP_201_CREATED
|
||||
return tasks[task_id]
|
||||
10
docs/src/response_cookies/tutorial002.py
Normal file
10
docs/src/response_cookies/tutorial002.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.responses import Response
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/cookie-and-object/")
|
||||
def create_cookie(response: Response):
|
||||
response.set_cookie(key="fakesession", value="fake-cookie-session-value")
|
||||
return {"message": "Come to the dark side, we have cookies"}
|
||||
10
docs/src/response_headers/tutorial002.py
Normal file
10
docs/src/response_headers/tutorial002.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.responses import Response
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/headers-and-object/")
|
||||
def get_headers(response: Response):
|
||||
response.headers["X-Cat-Dog"] = "alone in the world"
|
||||
return {"message": "Hello World"}
|
||||
0
docs/src/sql_databases/__init__.py
Normal file
0
docs/src/sql_databases/__init__.py
Normal file
0
docs/src/sql_databases/sql_app/__init__.py
Normal file
0
docs/src/sql_databases/sql_app/__init__.py
Normal file
36
docs/src/sql_databases/sql_app/crud.py
Normal file
36
docs/src/sql_databases/sql_app/crud.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from . import models, schemas
|
||||
|
||||
|
||||
def get_user(db: Session, user_id: int):
|
||||
return db.query(models.User).filter(models.User.id == user_id).first()
|
||||
|
||||
|
||||
def get_user_by_email(db: Session, email: str):
|
||||
return db.query(models.User).filter(models.User.email == email).first()
|
||||
|
||||
|
||||
def get_users(db: Session, skip: int = 0, limit: int = 100):
|
||||
return db.query(models.User).offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
def create_user(db: Session, user: schemas.UserCreate):
|
||||
fake_hashed_password = user.password + "notreallyhashed"
|
||||
db_user = models.User(email=user.email, hashed_password=fake_hashed_password)
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
|
||||
def get_items(db: Session, skip: int = 0, limit: int = 100):
|
||||
return db.query(models.Item).offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int):
|
||||
db_item = models.Item(**item.dict(), owner_id=user_id)
|
||||
db.add(db_item)
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
return db_item
|
||||
13
docs/src/sql_databases/sql_app/database.py
Normal file
13
docs/src/sql_databases/sql_app/database.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
|
||||
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"
|
||||
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||
)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
64
docs/src/sql_databases/sql_app/main.py
Normal file
64
docs/src/sql_databases/sql_app/main.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
from . import crud, models, schemas
|
||||
from .database import SessionLocal, engine
|
||||
|
||||
models.Base.metadata.create_all(bind=engine)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def db_session_middleware(request: Request, call_next):
|
||||
response = Response("Internal server error", status_code=500)
|
||||
try:
|
||||
request.state.db = SessionLocal()
|
||||
response = await call_next(request)
|
||||
finally:
|
||||
request.state.db.close()
|
||||
return response
|
||||
|
||||
|
||||
# Dependency
|
||||
def get_db(request: Request):
|
||||
return request.state.db
|
||||
|
||||
|
||||
@app.post("/users/", response_model=schemas.User)
|
||||
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
|
||||
db_user = crud.get_user_by_email(db, email=user.email)
|
||||
if db_user:
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
return crud.create_user(db=db, user=user)
|
||||
|
||||
|
||||
@app.get("/users/", response_model=List[schemas.User])
|
||||
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||
users = crud.get_users(db, skip=skip, limit=limit)
|
||||
return users
|
||||
|
||||
|
||||
@app.get("/users/{user_id}", response_model=schemas.User)
|
||||
def read_user(user_id: int, db: Session = Depends(get_db)):
|
||||
db_user = crud.get_user(db, user_id=user_id)
|
||||
if db_user is None:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return db_user
|
||||
|
||||
|
||||
@app.post("/users/{user_id}/items/", response_model=schemas.Item)
|
||||
def create_item_for_user(
|
||||
user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)
|
||||
):
|
||||
return crud.create_user_item(db=db, item=item, user_id=user_id)
|
||||
|
||||
|
||||
@app.get("/items/", response_model=List[schemas.Item])
|
||||
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||
items = crud.get_items(db, skip=skip, limit=limit)
|
||||
return items
|
||||
26
docs/src/sql_databases/sql_app/models.py
Normal file
26
docs/src/sql_databases/sql_app/models.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from .database import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True)
|
||||
hashed_password = Column(String)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
items = relationship("Item", back_populates="owner")
|
||||
|
||||
|
||||
class Item(Base):
|
||||
__tablename__ = "items"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String, index=True)
|
||||
description = Column(String, index=True)
|
||||
owner_id = Column(Integer, ForeignKey("users.id"))
|
||||
|
||||
owner = relationship("User", back_populates="items")
|
||||
37
docs/src/sql_databases/sql_app/schemas.py
Normal file
37
docs/src/sql_databases/sql_app/schemas.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ItemBase(BaseModel):
|
||||
title: str
|
||||
description: str = None
|
||||
|
||||
|
||||
class ItemCreate(ItemBase):
|
||||
pass
|
||||
|
||||
|
||||
class Item(ItemBase):
|
||||
id: int
|
||||
owner_id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
email: str
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str
|
||||
|
||||
|
||||
class User(UserBase):
|
||||
id: int
|
||||
is_active: bool
|
||||
items: List[Item] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
@@ -1,76 +0,0 @@
|
||||
from fastapi import Depends, FastAPI
|
||||
from sqlalchemy import Boolean, Column, Integer, String, create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base, declared_attr
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
# SQLAlchemy specific code, as with any other app
|
||||
SQLALCHEMY_DATABASE_URI = "sqlite:///./test.db"
|
||||
# SQLALCHEMY_DATABASE_URI = "postgresql://user:password@postgresserver/db"
|
||||
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URI, connect_args={"check_same_thread": False}
|
||||
)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
class CustomBase:
|
||||
# Generate __tablename__ automatically
|
||||
@declared_attr
|
||||
def __tablename__(cls):
|
||||
return cls.__name__.lower()
|
||||
|
||||
|
||||
Base = declarative_base(cls=CustomBase)
|
||||
|
||||
|
||||
class User(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True)
|
||||
hashed_password = Column(String)
|
||||
is_active = Column(Boolean(), default=True)
|
||||
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
db_session = SessionLocal()
|
||||
|
||||
first_user = db_session.query(User).first()
|
||||
if not first_user:
|
||||
u = User(email="johndoe@example.com", hashed_password="notreallyhashed")
|
||||
db_session.add(u)
|
||||
db_session.commit()
|
||||
|
||||
db_session.close()
|
||||
|
||||
|
||||
# Utility
|
||||
def get_user(db_session: Session, user_id: int):
|
||||
return db_session.query(User).filter(User.id == user_id).first()
|
||||
|
||||
|
||||
# Dependency
|
||||
def get_db(request: Request):
|
||||
return request.state.db
|
||||
|
||||
|
||||
# FastAPI specific code
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/users/{user_id}")
|
||||
def read_user(user_id: int, db: Session = Depends(get_db)):
|
||||
user = get_user(db, user_id=user_id)
|
||||
return user
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def db_session_middleware(request: Request, call_next):
|
||||
response = Response("Internal server error", status_code=500)
|
||||
try:
|
||||
request.state.db = SessionLocal()
|
||||
response = await call_next(request)
|
||||
finally:
|
||||
request.state.db.close()
|
||||
return response
|
||||
@@ -174,7 +174,6 @@ from app.routers import items, users
|
||||
|
||||
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 the submodule `items` directly, instead of importing just its variable `router`.
|
||||
@@ -216,7 +215,6 @@ It will include all the routes from that router as part of it.
|
||||
|
||||
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.
|
||||
|
||||
@@ -295,7 +293,6 @@ The end result is that the item paths are now:
|
||||
|
||||
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`:
|
||||
@@ -309,3 +306,11 @@ And open the docs at <a href="http://127.0.0.1:8000/docs" target="_blank">http:/
|
||||
You will see the automatic API docs, including the paths from all the submodules, using the correct paths (and prefixes) and the correct tags:
|
||||
|
||||
<img src="/img/tutorial/bigger-applications/image01.png">
|
||||
|
||||
## Include the same router multiple times with different `prefix`
|
||||
|
||||
You can also use `.include_router()` multiple times with the *same* router using different prefixes.
|
||||
|
||||
This could be useful, for example, to expose the same API under different prefixes, e.g. `/api/v1` and `/api/latest`.
|
||||
|
||||
This is an advanced usage that you might not really need, but it's there in case you do.
|
||||
|
||||
@@ -200,6 +200,35 @@ You couldn't get this kind of editor support if you where working directly with
|
||||
|
||||
But you don't have to worry about them either, incoming dicts are converted automatically and your output is converted automatically to JSON too.
|
||||
|
||||
## Bodies of arbitrary `dict`s
|
||||
|
||||
You can also declare a body as a `dict` with keys of some type and values of other type.
|
||||
|
||||
Without having to know beforehand what are the valid field/attribute names (as would be the case with Pydantic models).
|
||||
|
||||
This would be useful if you want to receive keys that you don't already know.
|
||||
|
||||
---
|
||||
|
||||
Other useful case is when you want to have keys of other type, e.g. `int`.
|
||||
|
||||
That's what we are going to see here.
|
||||
|
||||
In this case, you would accept any `dict` as long as it has `int` keys with `float` values:
|
||||
|
||||
```Python hl_lines="15"
|
||||
{!./src/body_nested_models/tutorial009.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Have in mind that JSON only supports `str` as keys.
|
||||
|
||||
But Pydantic has automatic data conversion.
|
||||
|
||||
This means that, even though your API clients can only send strings as keys, as long as those strings contain pure integers, Pydantic will convert them and validate them.
|
||||
|
||||
And the `dict` you receive as `weights` will actually have `int` keys and `float` values.
|
||||
|
||||
## Recap
|
||||
|
||||
With **FastAPI** you have the maximum flexibility provided by Pydantic models, while keeping your code simple, short and elegant.
|
||||
|
||||
100
docs/tutorial/custom-request-and-route.md
Normal file
100
docs/tutorial/custom-request-and-route.md
Normal file
@@ -0,0 +1,100 @@
|
||||
In some cases, you may want to override the logic used by the `Request` and `APIRoute` classes.
|
||||
|
||||
In particular, this may be a good alternative to logic in a middleware.
|
||||
|
||||
For example, if you want to read or manipulate the request body before it is processed by your application.
|
||||
|
||||
!!! danger
|
||||
This is an "advanced" feature.
|
||||
|
||||
If you are just starting with **FastAPI** you might want to skip this section.
|
||||
|
||||
## Use cases
|
||||
|
||||
Some use cases include:
|
||||
|
||||
* Converting non-JSON request bodies to JSON (e.g. [`msgpack`](https://msgpack.org/index.html)).
|
||||
* Decompressing gzip-compressed request bodies.
|
||||
* Automatically logging all request bodies.
|
||||
* Accessing the request body in an exception handler.
|
||||
|
||||
## Handling custom request body encodings
|
||||
|
||||
Let's see how to make use of a custom `Request` subclass to decompress gzip requests.
|
||||
|
||||
And an `APIRoute` subclass to use that custom request class.
|
||||
|
||||
### Create a custom `GzipRequest` class
|
||||
|
||||
First, we create a `GzipRequest` class, which will overwrite the `Request.body()` method to decompress the body in the presence of an appropriate header.
|
||||
|
||||
If there's no `gzip` in the header, it will not try to decompress the body.
|
||||
|
||||
That way, the same route class can handle gzip compressed or uncompressed requests.
|
||||
|
||||
```Python hl_lines="10 11 12 13 14 15 16 17"
|
||||
{!./src/custom_request_and_route/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Create a custom `GzipRoute` class
|
||||
|
||||
Next, we create a custom subclass of `fastapi.routing.APIRoute` that will make use of the `GzipRequest`.
|
||||
|
||||
This time, it will overwrite the method `APIRoute.get_route_handler()`.
|
||||
|
||||
This method returns a function. And that function is what will receive a request and return a response.
|
||||
|
||||
Here we use it to create a `GzipRequest` from the original request.
|
||||
|
||||
```Python hl_lines="20 21 22 23 24 25 26 27 28"
|
||||
{!./src/custom_request_and_route/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! note "Technical Details"
|
||||
A `Request` has a `request.scope` attribute, that's just a Python `dict` containing the metadata related to the request.
|
||||
|
||||
A `Request` also has a `request.receive`, that's a function to "receive" the body of the request.
|
||||
|
||||
The `scope` `dict` and `receive` function are both part of the ASGI specification.
|
||||
|
||||
And those two things, `scope` and `receive`, are what is needed to create a new `Request` instance.
|
||||
|
||||
To learn more about the `Request` check <a href="https://www.starlette.io/requests/" target="_blank">Starlette's docs about Requests</a>.
|
||||
|
||||
The only thing the function returned by `GzipRequest.get_route_handler` does differently is convert the `Request` to a `GzipRequest`.
|
||||
|
||||
Doing this, our `GzipRequest` will take care of decompressing the data (if necessary) before passing it to our *path operations*.
|
||||
|
||||
After that, all of the processing logic is the same.
|
||||
|
||||
But because of our changes in `GzipRequest.body`, the request body will be automatically decompressed when it is loaded by **FastAPI** when needed.
|
||||
|
||||
## Accessing the request body in an exception handler
|
||||
|
||||
We can also use this same approach to access the request body in an exception handler.
|
||||
|
||||
All we need to do is handle the request inside a `try`/`except` block:
|
||||
|
||||
```Python hl_lines="15 17"
|
||||
{!./src/custom_request_and_route/tutorial002.py!}
|
||||
```
|
||||
|
||||
If an exception occurs, the`Request` instance will still be in scope, so we can read and make use of the request body when handling the error:
|
||||
|
||||
```Python hl_lines="18 19 20"
|
||||
{!./src/custom_request_and_route/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Custom `APIRoute` class in a router
|
||||
|
||||
You can also set the `route_class` parameter of an `APIRouter`:
|
||||
|
||||
```Python hl_lines="25"
|
||||
{!./src/custom_request_and_route/tutorial003.py!}
|
||||
```
|
||||
|
||||
In this example, the *path operations* under the `router` will use the custom `TimedRoute` class, and will have an extra `X-Response-Time` header in the response with the time it took to generate the response:
|
||||
|
||||
```Python hl_lines="15 16 17 18 19"
|
||||
{!./src/custom_request_and_route/tutorial003.py!}
|
||||
```
|
||||
@@ -17,14 +17,12 @@ This is very useful when you need to:
|
||||
|
||||
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.
|
||||
@@ -151,7 +149,6 @@ The simplicity of the dependency injection system makes **FastAPI** compatible w
|
||||
* 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.
|
||||
|
||||
@@ -11,6 +11,7 @@ You could create a first dependency ("dependable") like:
|
||||
```Python hl_lines="6 7"
|
||||
{!./src/dependencies/tutorial005.py!}
|
||||
```
|
||||
|
||||
It declares an optional query parameter `q` as a `str`, and then it just returns it.
|
||||
|
||||
This is quite simple (not very useful), but will help us focus on how the sub-dependencies work.
|
||||
@@ -43,6 +44,18 @@ Then we can use the dependency with:
|
||||
|
||||
But **FastAPI** will know that it has to solve `query_extractor` first, to pass the results of that to `query_or_cookie_extractor` while calling it.
|
||||
|
||||
## Using the same dependency multiple times
|
||||
|
||||
If one of your dependencies is declared multiple times for the same *path operation*, for example, multiple dependencies have a common sub-dependency, **FastAPI** will know to call that sub-dependency only once per request.
|
||||
|
||||
And it will save the returned value in a <abbr title="A utility/system to store computed/generated values, to re-use them instead of computing them again.">"cache"</abbr> and pass it to all the "dependants" that need it in that specific request, instead of calling the dependency multiple times for the same request.
|
||||
|
||||
In an advanced scenario where you know you need the dependency to be called at every step (possibly multiple times) in the same request instead of using the "cached" value, you can set the parameter `use_cache=False` when using `Depends`:
|
||||
|
||||
```Python hl_lines="1"
|
||||
async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)):
|
||||
return {"fresh_value": fresh_value}
|
||||
```
|
||||
|
||||
## Recap
|
||||
|
||||
@@ -54,7 +67,7 @@ But still, it is very powerful, and allows you to declare arbitrarily deeply nes
|
||||
|
||||
!!! tip
|
||||
All this might not seem as useful with these simple examples.
|
||||
|
||||
|
||||
But you will see how useful it is in the chapters about **security**.
|
||||
|
||||
And you will also see the amounts of code it will save you.
|
||||
|
||||
@@ -174,6 +174,18 @@ For that, use the standard Python `typing.List`:
|
||||
{!./src/extra_models/tutorial004.py!}
|
||||
```
|
||||
|
||||
## Response with arbitrary `dict`
|
||||
|
||||
You can also declare a response using a plain arbitrary `dict`, declaring just the type of the keys and values, without using a Pydantic model.
|
||||
|
||||
This is useful if you don't know the valid field/attribute names (that would be needed for a Pydantic model) beforehand.
|
||||
|
||||
In this case, you can use `typing.Dict`:
|
||||
|
||||
```Python hl_lines="1 8"
|
||||
{!./src/extra_models/tutorial005.py!}
|
||||
```
|
||||
|
||||
## Recap
|
||||
|
||||
Use multiple Pydantic models and inherit freely for each case.
|
||||
|
||||
@@ -37,7 +37,7 @@ Open your browser at <a href="http://127.0.0.1:8000" target="_blank">http://127.
|
||||
You will see the JSON response as:
|
||||
|
||||
```JSON
|
||||
{"hello": "world"}
|
||||
{"message": "Hello World"}
|
||||
```
|
||||
|
||||
### Interactive API docs
|
||||
|
||||
@@ -18,3 +18,15 @@ To exclude a path operation from the generated OpenAPI schema (and thus, from th
|
||||
```Python hl_lines="6"
|
||||
{!./src/path_operation_advanced_configuration/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Advanced description from docstring
|
||||
|
||||
You can limit the lines used from the docstring of a *path operation function* for OpenAPI.
|
||||
|
||||
Adding an `\f` (an escaped "form feed" character) causes **FastAPI** to truncate the output used for OpenAPI at this point.
|
||||
|
||||
It won't show up in the documentation, but other tools (such as Sphinx) will be able to use the rest.
|
||||
|
||||
```Python hl_lines="19 20 21 22 23 24 25 26 27 28 29"
|
||||
{!./src/path_operation_advanced_configuration/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -35,7 +35,7 @@ If you run this example and open your browser at <a href="http://127.0.0.1:8000/
|
||||
|
||||
!!! check
|
||||
Notice that the value your function received (and returned) is `3`, as a Python `int`, not a string `"3"`.
|
||||
|
||||
|
||||
So, with that type declaration, **FastAPI** gives you automatic request <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>.
|
||||
|
||||
## Data validation
|
||||
@@ -61,12 +61,11 @@ because the path parameter `item_id` had a value of `"foo"`, which is not an `in
|
||||
|
||||
The same error would appear if you provided a `float` instead of an int, as in: <a href="http://127.0.0.1:8000/items/4.2" target="_blank">http://127.0.0.1:8000/items/4.2</a>
|
||||
|
||||
|
||||
!!! check
|
||||
So, with the same Python type declaration, **FastAPI** gives you data validation.
|
||||
|
||||
Notice that the error also clearly states exactly the point where the validation didn't pass.
|
||||
|
||||
Notice that the error also clearly states exactly the point where the validation didn't pass.
|
||||
|
||||
This is incredibly helpful while developing and debugging code that interacts with your API.
|
||||
|
||||
## Documentation
|
||||
@@ -96,8 +95,7 @@ All the data validation is performed under the hood by <a href="https://pydantic
|
||||
|
||||
You can use the same type declarations with `str`, `float`, `bool` and many other complex data types.
|
||||
|
||||
These are explored in the next chapters of the tutorial.
|
||||
|
||||
Several of these are explored in the next chapters of the tutorial.
|
||||
|
||||
## Order matters
|
||||
|
||||
@@ -115,6 +113,75 @@ Because path operations are evaluated in order, you need to make sure that the p
|
||||
|
||||
Otherwise, the path for `/users/{user_id}` would match also for `/users/me`, "thinking" that it's receiving a parameter `user_id` with a value of `"me"`.
|
||||
|
||||
## Predefined values
|
||||
|
||||
If you have a *path operation* that receives a *path parameter*, but you want the possible valid *path parameter* values to be predefined, you can use a standard Python <abbr title="Enumeration">`Enum`</abbr>.
|
||||
|
||||
### Create an `Enum` class
|
||||
|
||||
Import `Enum` and create a sub-class that inherits from `str` and from `Enum`.
|
||||
|
||||
By inheriting from `str` the API docs will be able to know that the values must be of type `string` and will be able to render correctly.
|
||||
|
||||
And create class attributes with fixed values, those fixed values will be the available valid values:
|
||||
|
||||
```Python hl_lines="1 6 7 8 9"
|
||||
{!./src/path_params/tutorial005.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
<a href="https://docs.python.org/3/library/enum.html" target="_blank">Enumerations (or enums) are available in Python</a> since version 3.4.
|
||||
|
||||
!!! tip
|
||||
If you are wondering, "AlexNet", "ResNet", and "LeNet" are just names of Machine Learning <abbr title="Technically, Deep Learning model architectures">models</abbr>.
|
||||
|
||||
### Declare a *path parameter*
|
||||
|
||||
Then create a *path parameter* with a type annotation using the enum class you created (`ModelName`):
|
||||
|
||||
```Python hl_lines="16"
|
||||
{!./src/path_params/tutorial005.py!}
|
||||
```
|
||||
|
||||
### Check the docs
|
||||
|
||||
Because the available values for the *path parameter* are specified, the interactive docs can show them nicely:
|
||||
|
||||
<img src="/img/tutorial/path-params/image03.png">
|
||||
|
||||
### Working with Python *enumerations*
|
||||
|
||||
The value of the *path parameter* will be an *enumeration member*.
|
||||
|
||||
#### Compare *enumeration members*
|
||||
|
||||
You can compare it with the *enumeration member* in your created enum `ModelName`:
|
||||
|
||||
```Python hl_lines="17"
|
||||
{!./src/path_params/tutorial005.py!}
|
||||
```
|
||||
|
||||
#### Get the *enumeration value*
|
||||
|
||||
You can get the actual value (a `str` in this case) using `model_name.value`, or in general, `your_enum_member.value`:
|
||||
|
||||
```Python hl_lines="19"
|
||||
{!./src/path_params/tutorial005.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
You could also access the value `"lenet"` with `ModelName.lenet.value`.
|
||||
|
||||
#### Return *enumeration members*
|
||||
|
||||
You can return *enum members* from your *path operation*, even nested in a JSON body (e.g. a `dict`).
|
||||
|
||||
They will be converted to their corresponding values before returning them to the client:
|
||||
|
||||
```Python hl_lines="18 20 21"
|
||||
{!./src/path_params/tutorial005.py!}
|
||||
```
|
||||
|
||||
## Path parameters containing paths
|
||||
|
||||
Let's say you have a *path operation* with a path `/files/{file_path}`.
|
||||
|
||||
@@ -12,7 +12,6 @@ The query parameter `q` is of type `str`, and by default is `None`, so it is opt
|
||||
|
||||
We are going to enforce that even though `q` is optional, whenever it is provided, it **doesn't exceed a length of 50 characters**.
|
||||
|
||||
|
||||
### Import `Query`
|
||||
|
||||
To achieve that, first import `Query` from `fastapi`:
|
||||
@@ -29,7 +28,7 @@ And now use it as the default value of your parameter, setting the parameter `ma
|
||||
{!./src/query_params_str_validations/tutorial002.py!}
|
||||
```
|
||||
|
||||
As we have to replace the default value `None` with `Query(None)`, the first parameter to `Query` serves the same purpose of defining that default value.
|
||||
As we have to replace the default value `None` with `Query(None)`, the first parameter to `Query` serves the same purpose of defining that default value.
|
||||
|
||||
So:
|
||||
|
||||
@@ -41,7 +40,7 @@ q: str = Query(None)
|
||||
|
||||
```Python
|
||||
q: str = None
|
||||
```
|
||||
```
|
||||
|
||||
But it declares it explicitly as being a query parameter.
|
||||
|
||||
@@ -53,7 +52,6 @@ q: str = Query(None, max_length=50)
|
||||
|
||||
This will validate the data, show a clear error when the data is not valid, and document the parameter in the OpenAPI schema path operation.
|
||||
|
||||
|
||||
## Add more validations
|
||||
|
||||
You can also add a parameter `min_length`:
|
||||
@@ -119,7 +117,7 @@ So, when you need to declare a value as required while using `Query`, you can us
|
||||
{!./src/query_params_str_validations/tutorial006.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
!!! info
|
||||
If you hadn't seen that `...` before: it is a a special single value, it is <a href="https://docs.python.org/3/library/constants.html#Ellipsis" target="_blank">part of Python and is called "Ellipsis"</a>.
|
||||
|
||||
This will let **FastAPI** know that this parameter is required.
|
||||
@@ -156,11 +154,48 @@ So, the response to that URL would be:
|
||||
!!! tip
|
||||
To declare a query parameter with a type of `list`, like in the example above, you need to explicitly use `Query`, otherwise it would be interpreted as a request body.
|
||||
|
||||
|
||||
The interactive API docs will update accordingly, to allow multiple values:
|
||||
|
||||
<img src="/img/tutorial/query-params-str-validations/image02.png">
|
||||
|
||||
### Query parameter list / multiple values with defaults
|
||||
|
||||
And you can also define a default `list` of values if none are provided:
|
||||
|
||||
```Python hl_lines="9"
|
||||
{!./src/query_params_str_validations/tutorial012.py!}
|
||||
```
|
||||
|
||||
If you go to:
|
||||
|
||||
```
|
||||
http://localhost:8000/items/
|
||||
```
|
||||
|
||||
the default of `q` will be: `["foo", "bar"]` and your response will be:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"q": [
|
||||
"foo",
|
||||
"bar"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Using `list`
|
||||
|
||||
You can also use `list` directly instead of `List[str]`:
|
||||
|
||||
```Python hl_lines="7"
|
||||
{!./src/query_params_str_validations/tutorial013.py!}
|
||||
```
|
||||
|
||||
!!! note
|
||||
Have in mind that in this case, FastAPI won't check the contents of the list.
|
||||
|
||||
For example, `List[int]` would check (and document) that the contents of the list are integers. But `list` alone wouldn't.
|
||||
|
||||
## Declare more metadata
|
||||
|
||||
You can add more information about the parameter.
|
||||
|
||||
@@ -186,3 +186,39 @@ In this case, there are 3 query parameters:
|
||||
* `needy`, a required `str`.
|
||||
* `skip`, an `int` with a default value of `0`.
|
||||
* `limit`, an optional `int`.
|
||||
|
||||
!!! tip
|
||||
You could also use `Enum`s <a href="https://fastapi.tiangolo.com/tutorial/path-params/#predefined-values" target="_blank">the same way as with *path parameters*</a>.
|
||||
|
||||
## Optional type declarations
|
||||
|
||||
!!! warning
|
||||
This might be an advanced use case.
|
||||
|
||||
You might want to skip it.
|
||||
|
||||
If you are using `mypy` it could complain with type declarations like:
|
||||
|
||||
```Python
|
||||
limit: int = None
|
||||
```
|
||||
|
||||
With an error like:
|
||||
|
||||
```
|
||||
Incompatible types in assignment (expression has type "None", variable has type "int")
|
||||
```
|
||||
|
||||
In those cases you can use `Optional` to tell `mypy` that the value could be `None`, like:
|
||||
|
||||
```Python
|
||||
from typing import Optional
|
||||
|
||||
limit: Optional[int] = None
|
||||
```
|
||||
|
||||
In a *path operation* that could look like:
|
||||
|
||||
```Python hl_lines="9"
|
||||
{!./src/query_params/tutorial007.py!}
|
||||
```
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
You can define files to be uploaded by the client using `File`.
|
||||
|
||||
!!! info
|
||||
To receive uploaded files, first install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
|
||||
|
||||
E.g. `pip install python-multipart`.
|
||||
|
||||
This is because uploaded files are sent as "form data".
|
||||
|
||||
## Import `File`
|
||||
|
||||
Import `File` and `UploadFile` from `fastapi`:
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
You can define files and form fields at the same time using `File` and `Form`.
|
||||
|
||||
!!! info
|
||||
To receive uploaded files and/or form data, first install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
|
||||
|
||||
E.g. `pip install python-multipart`.
|
||||
|
||||
## Import `File` and `Form`
|
||||
|
||||
```Python hl_lines="1"
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
When you need to receive form fields instead of JSON, you can use `Form`.
|
||||
|
||||
!!! info
|
||||
To use forms, first install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
|
||||
|
||||
E.g. `pip install python-multipart`.
|
||||
|
||||
## Import `Form`
|
||||
|
||||
Import `Form` from `fastapi`:
|
||||
|
||||
31
docs/tutorial/response-change-status-code.md
Normal file
31
docs/tutorial/response-change-status-code.md
Normal file
@@ -0,0 +1,31 @@
|
||||
You probably read before that you can set a <a href="https://fastapi.tiangolo.com/tutorial/response-status-code/" target="_blank">default Response Status Code</a>.
|
||||
|
||||
But in some cases you need to return a different status code than the default.
|
||||
|
||||
## Use case
|
||||
|
||||
For example, imagine that you want to return an HTTP status code of "OK" `200` by default.
|
||||
|
||||
But if the data didn't exist, you want to create it, and return an HTTP status code of "CREATED" `201`.
|
||||
|
||||
But you still want to be able to filter and convert the data you return with a `response_model`.
|
||||
|
||||
For those cases, you can use a `Response` parameter.
|
||||
|
||||
## Use a `Response` parameter
|
||||
|
||||
You can declare a parameter of type `Response` in your *path operation function* (as you can do for cookies and headers).
|
||||
|
||||
And then you can set the `status_code` in that *temporal* response object.
|
||||
|
||||
```Python hl_lines="2 11 14"
|
||||
{!./src/response_change_status_code/tutorial001.py!}
|
||||
```
|
||||
|
||||
And then you can return any object you need, as you normally would (a `dict`, a database model, etc).
|
||||
|
||||
And if you declared a `response_model`, it will still be used to filter and convert the object you returned.
|
||||
|
||||
**FastAPI** will use that *temporal* response to extract the status code (also cookies and headers), and will put them in the final response that contains the value you returned, filtered by any `response_model`.
|
||||
|
||||
You can also declare the `Response` parameter in dependencies, and set the status code in them. But have in mind that the last one to be set will win.
|
||||
@@ -1,4 +1,24 @@
|
||||
You can create (set) Cookies in your response.
|
||||
## Use a `Response` parameter
|
||||
|
||||
You can declare a parameter of type `Response` in your *path operation function*, the same way you can declare a `Request` parameter.
|
||||
|
||||
And then you can set headers in that *temporal* response object.
|
||||
|
||||
```Python hl_lines="2 8 9"
|
||||
{!./src/response_cookies/tutorial002.py!}
|
||||
```
|
||||
|
||||
And then you can return any object you need, as you normally would (a `dict`, a database model, etc).
|
||||
|
||||
And if you declared a `response_model`, it will still be used to filter and convert the object you returned.
|
||||
|
||||
**FastAPI** will use that *temporal* response to extract the cookies (also headers and status code), and will put them in the final response that contains the value you returned, filtered by any `response_model`.
|
||||
|
||||
You can also declare the `Response` parameter in dependencies, and set cookies (and headers) in them.
|
||||
|
||||
## Return a `Response` directly
|
||||
|
||||
You can also create cookies when returning a `Response` directly in your code.
|
||||
|
||||
To do that, you can create a response as described in <a href="https://fastapi.tiangolo.com/tutorial/response-directly/" target="_blank">Return a Response directly</a>.
|
||||
|
||||
@@ -8,6 +28,13 @@ Then set Cookies in it, and then return it:
|
||||
{!./src/response_cookies/tutorial001.py!}
|
||||
```
|
||||
|
||||
## More info
|
||||
!!! tip
|
||||
Have in mind that if you return a response directly instead of using the `Response` parameter, FastAPI will return it directly.
|
||||
|
||||
So, you will have to make sure your data is of the correct type. E.g. it is compatible with JSON, if you are returning a `JSONResponse`.
|
||||
|
||||
And also that you are not sending any data that should have been filtered by a `response_model`.
|
||||
|
||||
### More info
|
||||
|
||||
To see all the available parameters and options, check the <a href="https://www.starlette.io/responses/#set-cookie" target="_blank">documentation in Starlette</a>.
|
||||
|
||||
@@ -56,7 +56,7 @@ You could put your XML content in a string, put it in a Starlette Response, and
|
||||
|
||||
When you return a `Response` directly its data is not validated, converted (serialized), nor documented automatically.
|
||||
|
||||
But you can still <a href="tutorial/additional-responses/" target="_blank">document it</a>.
|
||||
But you can still <a href="https://fastapi.tiangolo.com/tutorial/additional-responses/" target="_blank">document it</a>.
|
||||
|
||||
In the next sections you will see how to use/declare these custom `Response`s while still having automatic data conversion, documentation, etc.
|
||||
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
You can add headers to your response.
|
||||
## Use a `Response` parameter
|
||||
|
||||
You can declare a parameter of type `Response` in your *path operation function* (as you can do for cookies), the same way you can declare a `Request` parameter.
|
||||
|
||||
And then you can set headers in that *temporal* response object.
|
||||
|
||||
```Python hl_lines="2 8 9"
|
||||
{!./src/response_headers/tutorial002.py!}
|
||||
```
|
||||
|
||||
And then you can return any object you need, as you normally would (a `dict`, a database model, etc).
|
||||
|
||||
And if you declared a `response_model`, it will still be used to filter and convert the object you returned.
|
||||
|
||||
**FastAPI** will use that *temporal* response to extract the headers (also cookies and status code), and will put them in the final response that contains the value you returned, filtered by any `response_model`.
|
||||
|
||||
You can also declare the `Response` parameter in dependencies, and set headers (and cookies) in them.
|
||||
|
||||
## Return a `Response` directly
|
||||
|
||||
You can also add headers when you return a `Response` directly.
|
||||
|
||||
Create a response as described in <a href="https://fastapi.tiangolo.com/tutorial/response-directly/" target="_blank">Return a Response directly</a> and pass the headers as an additional parameter:
|
||||
|
||||
@@ -6,7 +26,8 @@ Create a response as described in <a href="https://fastapi.tiangolo.com/tutorial
|
||||
{!./src/response_headers/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Have in mind that custom proprietary headers can be added <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" target="_blank">using the 'X-' prefix</a>.
|
||||
## Custom Headers
|
||||
|
||||
But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your <a href="https://fastapi.tiangolo.com/tutorial/cors/" target="_blank">CORS configurations</a>, using the parameter `expose_headers` documented in <a href="https://www.starlette.io/middleware/#corsmiddleware" target="_blank">Starlette's CORS docs</a>.
|
||||
Have in mind that custom proprietary headers can be added <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" target="_blank">using the 'X-' prefix</a>.
|
||||
|
||||
But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your <a href="https://fastapi.tiangolo.com/tutorial/cors/" target="_blank">CORS configurations</a>, using the parameter `expose_headers` documented in <a href="https://www.starlette.io/middleware/#corsmiddleware" target="_blank">Starlette's CORS docs</a>.
|
||||
|
||||
@@ -93,7 +93,7 @@ Your response model could have default values, like:
|
||||
```
|
||||
|
||||
* `description: str = None` has a default of `None`.
|
||||
* `tax: float = None` has a default of `None`.
|
||||
* `tax: float = 10.5` has a default of `10.5`.
|
||||
* `tags: List[str] = []` has a default of an empty list: `[]`.
|
||||
|
||||
but you might want to omit them from the result if they were not actually stored.
|
||||
|
||||
@@ -47,7 +47,6 @@ In short:
|
||||
!!! 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:
|
||||
@@ -69,3 +68,7 @@ You can use the convenience variables from `starlette.status`.
|
||||
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">
|
||||
|
||||
## Changing the default
|
||||
|
||||
Later, in a more advanced part of the tutorial/user guide, you will see how to <a href="https://fastapi.tiangolo.com/tutorial/response-change-status-code/" target="_blank">return a different status code than the default</a> you are declaring here.
|
||||
|
||||
@@ -24,6 +24,13 @@ Copy the example in a file `main.py`:
|
||||
|
||||
## Run it
|
||||
|
||||
!!! info
|
||||
First install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
|
||||
|
||||
E.g. `pip install python-multipart`.
|
||||
|
||||
This is because **OAuth2** uses "form data" for sending the `username` and `password`.
|
||||
|
||||
Run the example with:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -12,8 +12,8 @@ Then, when you type that username and password, the browser sends them in the he
|
||||
|
||||
## Simple HTTP Basic Auth
|
||||
|
||||
* Import `HTTPBAsic` and `HTTPBasicCredentials`.
|
||||
* Create a "`security` scheme" using `HTTPBAsic`.
|
||||
* Import `HTTPBasic` and `HTTPBasicCredentials`.
|
||||
* Create a "`security` scheme" using `HTTPBasic`.
|
||||
* Use that `security` with a dependency in your *path operation*.
|
||||
* It returns an object of type `HTTPBasicCredentials`:
|
||||
* It contains the `username` and `password` sent.
|
||||
|
||||
@@ -156,7 +156,7 @@ Then you could add permissions about that entity, like "drive" (for the car) or
|
||||
|
||||
And then, you could give that JWT token to a user (or bot), and he could use it to perform those actions (drive the car, or edit the blog post) without even needing to have an account, just with the JWT token your API generated for that.
|
||||
|
||||
Using these ideas, JWT can be used for way more sophisticate scenarios.
|
||||
Using these ideas, JWT can be used for way more sophisticated scenarios.
|
||||
|
||||
In those cases, several of those entities could have the same ID, let's say `foo` (a user `foo`, a car `foo`, and a blog post `foo`).
|
||||
|
||||
|
||||
@@ -176,9 +176,9 @@ For this, we use `security_scopes.scopes`, that contains a `list` with all these
|
||||
|
||||
Let's review again this dependency tree and the scopes.
|
||||
|
||||
As the other dependency `get_current_active_user` has as a sub-dependency this `get_current_user`, the scope `"me"` declared at `get_current_active_user` will be included in the `security_scopes.scopes` `list` inside of `get_current_user`.
|
||||
As the `get_current_active_user` dependency has as a sub-dependency on `get_current_user`, the scope `"me"` declared at `get_current_active_user` will be included in the list of required scopes in the `security_scopes.scopes` passed to `get_current_user`.
|
||||
|
||||
And as the *path operation* itself also declares a scope `"items"`, it will also be part of this `list` `security_scopes.scopes` in `get_current_user`.
|
||||
The *path operation* itself also declares a scope, `"items"`, so this will also be in the list of `security_scopes.scopes` passed to `get_current_user`.
|
||||
|
||||
Here's how the hierarchy of dependencies and scopes looks like:
|
||||
|
||||
@@ -247,4 +247,4 @@ The most secure is the code flow, but is more complex to implement as it require
|
||||
|
||||
## `Security` in decorator `dependencies`
|
||||
|
||||
The same way you can define a `list` of <a href="https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-decorator/" target="_blank">`Depends` in the decorator's `dependencies` parameter</a>, you could also use `Security` with `scopes` there.
|
||||
The same way you can define a `list` of <a href="https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/" target="_blank">`Depends` in the decorator's `dependencies` parameter</a>, you could also use `Security` with `scopes` there.
|
||||
|
||||
@@ -25,45 +25,101 @@ Later, for your production application, you might want to use a database server
|
||||
|
||||
The **FastAPI** specific code is as small as always.
|
||||
|
||||
## Import SQLAlchemy components
|
||||
## ORMs
|
||||
|
||||
For now, don't pay attention to the rest, only the imports:
|
||||
**FastAPI** works with any database and any style of library to talk to the database.
|
||||
|
||||
```Python hl_lines="2 3 4"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
A common pattern is to use an "ORM": an "object-relational mapping" library.
|
||||
|
||||
An ORM has tools to convert ("*map*") between *objects* in code and database tables ("*relations*").
|
||||
|
||||
With an ORM, you normally create a class that represents a table in a SQL database, each attribute of the class represents a column, with a name and a type.
|
||||
|
||||
For example a class `Pet` could represent a SQL table `pets`.
|
||||
|
||||
And each *instance* object of that class represents a row in the database.
|
||||
|
||||
For example an object `orion_cat` (an instance of `Pet`) could have an attribute `orion_cat.type`, for the column `type`. And the value of that attribute could be, e.g. `"cat"`.
|
||||
|
||||
These ORMs also have tools to make the connections or relations between tables or entities.
|
||||
|
||||
This way, you could also have an attribute `orion_cat.owner` and the owner would contain the data for this pet's owner, taken from the table *owners*.
|
||||
|
||||
So, `orion_cat.owner.name` could be the name (from the `name` column in the `owners` table) of this pet's owner.
|
||||
|
||||
It could have a value like `"Arquilian"`.
|
||||
|
||||
And the ORM will do all the work to get the information from the corresponding table *owners* when you try to access it from your pet object.
|
||||
|
||||
Common ORMs are for example: Django-ORM (part of the Django framework), SQLAlchemy ORM (part of SQLAlchemy, independent of framework) and Peewee (independent of framework), among others.
|
||||
|
||||
Here we will see how to work with **SQLAlchemy ORM**.
|
||||
|
||||
The same way, you could use Peewee or any other.
|
||||
|
||||
## File structure
|
||||
|
||||
For these examples, let's say you have a directory named `my_super_project` that contains a sub-directory called `sql_app` with a structure like this:
|
||||
|
||||
```
|
||||
├── sql_app
|
||||
│ ├── __init__.py
|
||||
│ ├── crud.py
|
||||
│ ├── database.py
|
||||
│ ├── main.py
|
||||
│ ├── models.py
|
||||
│ ├── schemas.py
|
||||
```
|
||||
|
||||
## Define the database
|
||||
The file `__init__.py` is just an empty file, but it tells Python that `sql_app` with all its modules (Python files) is a package.
|
||||
|
||||
Define the database that SQLAlchemy should "connect" to:
|
||||
Now let's see what each file/module does.
|
||||
|
||||
```Python hl_lines="9"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
## Create the SQLAlchemy parts
|
||||
|
||||
Let's refer to the file `sql_app/database.py`.
|
||||
|
||||
### Import the SQLAlchemy parts
|
||||
|
||||
```Python hl_lines="1 2 3"
|
||||
{!./src/sql_databases/sql_app/database.py!}
|
||||
```
|
||||
|
||||
### Create a database URL for SQLAlchemy
|
||||
|
||||
```Python hl_lines="5 6"
|
||||
{!./src/sql_databases/sql_app/database.py!}
|
||||
```
|
||||
|
||||
In this example, we are "connecting" to a SQLite database (opening a file with the SQLite database).
|
||||
|
||||
The file will be located at the same directory in the file `test.db`. That's why the last part is `./test.db`.
|
||||
The file will be located at the same directory in the file `test.db`.
|
||||
|
||||
That's why the last part is `./test.db`.
|
||||
|
||||
If you were using a **PostgreSQL** database instead, you would just have to uncomment the line:
|
||||
|
||||
```Python
|
||||
SQLALCHEMY_DATABASE_URI = "postgresql://user:password@postgresserver/db"
|
||||
SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"
|
||||
```
|
||||
|
||||
...and adapt it with your database data and credentials (equivalently for MySQL, MariaDB or any other).
|
||||
|
||||
!!! tip
|
||||
|
||||
|
||||
This is the main line that you would have to modify if you wanted to use a different database.
|
||||
|
||||
## Create the SQLAlchemy `engine`
|
||||
### Create the SQLAlchemy `engine`
|
||||
|
||||
```Python hl_lines="12 13 14"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
The first step is to create a SQLAlchemy "engine".
|
||||
|
||||
We will later use this `engine` in other places.
|
||||
|
||||
```Python hl_lines="8 9 10"
|
||||
{!./src/sql_databases/sql_app/database.py!}
|
||||
```
|
||||
|
||||
### Note
|
||||
#### Note
|
||||
|
||||
The argument:
|
||||
|
||||
@@ -78,25 +134,302 @@ connect_args={"check_same_thread": False}
|
||||
That argument `check_same_thread` is there mainly to be able to run the tests that cover this example.
|
||||
|
||||
|
||||
## Create a `SessionLocal` class
|
||||
### Create a `SessionLocal` class
|
||||
|
||||
Each instance of the `SessionLocal` class will have a connection to the database.
|
||||
Each instance of the `SessionLocal` class will be a database session. The class itself is not a database session yet.
|
||||
|
||||
This object (class) is not a connection to the database yet, but once we create an instance of this class, that instance will have the actual connection to the database.
|
||||
But once we create an instance of the `SessionLocal` class, this instance will be the actual database session.
|
||||
|
||||
We name it `SessionLocal` to distinguish it from the `Session` we are importing from SQLAlchemy.
|
||||
|
||||
We will use `Session` to declare types later and to get better editor support and completion.
|
||||
We will use `Session` (the one imported from SQLAlchemy) later.
|
||||
|
||||
For now, create the `SessionLocal`:
|
||||
To create the `SessionLocal` class, use the function `sessionmaker`:
|
||||
|
||||
```Python hl_lines="15"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```Python hl_lines="11"
|
||||
{!./src/sql_databases/sql_app/database.py!}
|
||||
```
|
||||
|
||||
## Create a middleware to handle sessions
|
||||
### Create a `Base` class
|
||||
|
||||
Now let's temporarily jump to the end of the file, to use the `SessionLocal` class we created above.
|
||||
Now we will use the function `declarative_base()` that returns a class.
|
||||
|
||||
Later we will inherit from this class to create each of the database models or classes (the ORM models):
|
||||
|
||||
```Python hl_lines="13"
|
||||
{!./src/sql_databases/sql_app/database.py!}
|
||||
```
|
||||
|
||||
## Create the database models
|
||||
|
||||
Let's now see the file `sql_app/models.py`.
|
||||
|
||||
### Create SQLAlchemy models from the `Base` class
|
||||
|
||||
We will use this `Base` class we created before to create the SQLAlchemy models.
|
||||
|
||||
!!! tip
|
||||
SQLAlchemy uses the term "**model**" to refer to these classes and instances that interact with the database.
|
||||
|
||||
But Pydantic also uses the term "**model**" to refer to something different, the data validation, conversion, and documentation classes and instances.
|
||||
|
||||
Import `Base` from `database` (the file `database.py` from above).
|
||||
|
||||
Create classes that inherit from it.
|
||||
|
||||
These classes are the SQLAlchemy models.
|
||||
|
||||
```Python hl_lines="4 7 8 18 19"
|
||||
{!./src/sql_databases/sql_app/models.py!}
|
||||
```
|
||||
|
||||
The `__tablename__` attribute tells SQLAlchemy the name of the table to use in the database for each of these models.
|
||||
|
||||
### Create model attributes/columns
|
||||
|
||||
Now create all the model (class) attributes.
|
||||
|
||||
Each of these attributes represents a column in its corresponding database table.
|
||||
|
||||
We use `Column` from SQLAlchemy as the default value.
|
||||
|
||||
And we pass a SQLAlchemy class "type", as `Integer`, `String`, and `Boolean`, that defines the type in the database, as an argument.
|
||||
|
||||
```Python hl_lines="1 10 11 12 13 21 22 23 24"
|
||||
{!./src/sql_databases/sql_app/models.py!}
|
||||
```
|
||||
|
||||
### Create the relationships
|
||||
|
||||
Now create the relationships.
|
||||
|
||||
For this, we use `relationship` provided by SQLAlchemy ORM.
|
||||
|
||||
This will become, more or less, a "magic" attribute that will contain the values from other tables related to this one.
|
||||
|
||||
```Python hl_lines="2 15 26"
|
||||
{!./src/sql_databases/sql_app/models.py!}
|
||||
```
|
||||
|
||||
When accessing the attribute `items` in a `User`, as in `my_user.items`, it will have a list of `Item` SQLAlchemy models (from the `items` table) that have a foreign key pointing to this record in the `users` table.
|
||||
|
||||
When you access `my_user.items`, SQLAlchemy will actually go and fetch the items from the database in the `items` table and populate them here.
|
||||
|
||||
And when accessing the attribute `owner` in an `Item`, it will contain a `User` SQLAlchemy model from the `users` table. It will use the `owner_id` attribute/column with its foreign key to know which record to get from the `users` table.
|
||||
|
||||
## Create the Pydantic models
|
||||
|
||||
Now let's check the file `sql_app/schemas.py`.
|
||||
|
||||
!!! tip
|
||||
To avoid confusion between the SQLAlchemy *models* and the Pydantic *models*, we will have the file `models.py` with the SQLAlchemy models, and the file `schemas.py` with the Pydantic models.
|
||||
|
||||
These Pydantic models define more or less a "schema" (a valid data shape).
|
||||
|
||||
So this will help us avoiding confusion while using both.
|
||||
|
||||
### Create initial Pydantic *models* / schemas
|
||||
|
||||
Create an `ItemBase` and `UserBase` Pydantic *models* (or let's say "schemas") to have common attributes while creating or reading data.
|
||||
|
||||
And create an `ItemCreate` and `UserCreate` that inherit from them (so they will have the same attributes), plus any additional data (attributes) needed for creation.
|
||||
|
||||
So, the user will also have a `password` when creating it.
|
||||
|
||||
But for security, the `password` won't be in other Pydantic *models*, for example, it won't be sent from the API when reading a user.
|
||||
|
||||
```Python hl_lines="3 6 7 8 11 12 23 24 27 28"
|
||||
{!./src/sql_databases/sql_app/schemas.py!}
|
||||
```
|
||||
|
||||
#### SQLAlchemy style and Pydantic style
|
||||
|
||||
Notice that SQLAlchemy *models* define attributes using `=`, and pass the type as a parameter to `Column`, like in:
|
||||
|
||||
```Python
|
||||
name = Column(String)
|
||||
```
|
||||
|
||||
while Pydantic *models* declare the types using `:`, the new type annotation syntax/type hints:
|
||||
|
||||
```Python
|
||||
name: str
|
||||
```
|
||||
|
||||
Have it in mind, so you don't get confused when using `=` and `:` with them.
|
||||
|
||||
### Create Pydantic *models* / schemas for reading / returning
|
||||
|
||||
Now create Pydantic *models* (schemas) that will be used when reading data, when returning it from the API.
|
||||
|
||||
For example, before creating an item, we don't know what will be the ID assigned to it, but when reading it (when returning it from the API) we will already know its ID.
|
||||
|
||||
The same way, when reading a user, we can now declare that `items` will contain the items that belong to this user.
|
||||
|
||||
Not only the IDs of those items, but all the data that we defined in the Pydantic *model* for reading items: `Item`.
|
||||
|
||||
```Python hl_lines="15 16 17 31 32 33 34"
|
||||
{!./src/sql_databases/sql_app/schemas.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Notice that the `User`, the Pydantic *model* that will be used when reading a user (returning it from the API) doesn't include the `password`.
|
||||
|
||||
### Use Pydantic's `orm_mode`
|
||||
|
||||
Now, in the Pydantic *models* for reading, `Item` and `User`, add an internal `Config` class.
|
||||
|
||||
This <a href="https://pydantic-docs.helpmanual.io/#config" target="_blank">`Config`</a> class is used to provide configurations to Pydantic.
|
||||
|
||||
In the `Config` class, set the attribute `orm_mode = True`.
|
||||
|
||||
```Python hl_lines="15 19 20 31 36 37"
|
||||
{!./src/sql_databases/sql_app/schemas.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Notice it's assigning a value with `=`, like:
|
||||
|
||||
`orm_mode = True`
|
||||
|
||||
It doesn't use `:` as for the type declarations before.
|
||||
|
||||
This is setting a config value, not declaring a type.
|
||||
|
||||
Pydantic's `orm_mode` will tell the Pydantic *model* to read the data even if it is not a `dict`, but an ORM model (or any other arbitrary object with attributes).
|
||||
|
||||
This way, instead of only trying to get the `id` value from a `dict`, as in:
|
||||
|
||||
```Python
|
||||
id = data["id"]
|
||||
```
|
||||
|
||||
it will also try to get it from an attribute, as in:
|
||||
|
||||
```Python
|
||||
id = data.id
|
||||
```
|
||||
|
||||
And with this, the Pydantic *model* is compatible with ORMs, and you can just declare it in the `response_model` argument in your *path operations*.
|
||||
|
||||
You will be able to return a database model and it will read the data from it.
|
||||
|
||||
#### Technical Details about ORM mode
|
||||
|
||||
SQLAlchemy and many others are by default "lazy loading".
|
||||
|
||||
That means, for example, that they don't fetch the data for relationships from the database unless you try to access the attribute that would contain that data.
|
||||
|
||||
For example, accessing the attribute `items`:
|
||||
|
||||
```Python
|
||||
current_user.items
|
||||
```
|
||||
|
||||
would make SQLAlchemy go to the `items` table and get the items for this user, but not before.
|
||||
|
||||
Without `orm_mode`, if you returned a SQLAlchemy model from your *path operation*, it wouldn't include the relationship data.
|
||||
|
||||
Even if you declared those relationships in your Pydantic models.
|
||||
|
||||
But with ORM mode, as Pydantic itself will try to access the data it needs from attributes (instead of assuming a `dict`), you can declare the specific data you want to return and it will be able to go and get it, even from ORMs.
|
||||
|
||||
## CRUD utils
|
||||
|
||||
Now let's see the file `sql_app/crud.py`.
|
||||
|
||||
In this file we will have reusable functions to interact with the data in the database.
|
||||
|
||||
**CRUD** comes from: **C**reate, **R**ead, **U**pdate, and **D**elete.
|
||||
|
||||
...although in this example we are only creating and reading.
|
||||
|
||||
### Read data
|
||||
|
||||
Import `Session` from `sqlalchemy.orm`, this will allow you to declare the type of the `db` parameters and have better type checks and completion in your functions.
|
||||
|
||||
Import `models` (the SQLAlchemy models) and `schemas` (the Pydantic *models* / schemas).
|
||||
|
||||
Create utility functions to:
|
||||
|
||||
* Read a single user by ID and by email.
|
||||
* Read multiple users.
|
||||
* Read a single item.
|
||||
|
||||
```Python hl_lines="1 3 6 7 10 11 14 15 27 28"
|
||||
{!./src/sql_databases/sql_app/crud.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
By creating functions that are only dedicated to interacting with the database (get a user or an item) independent of your path operation function, you can more easily reuse them in multiple parts and also add <abbr title="Automated tests, written in code, that check if another piece of code is working correctly.">unit tests</abbr> for them.
|
||||
|
||||
### Create data
|
||||
|
||||
Now create utility functions to create data.
|
||||
|
||||
The steps are:
|
||||
|
||||
* Create a SQLAlchemy model *instance* with your data.
|
||||
* `add` that instance object to your database session.
|
||||
* `commit` the changes to the database (so that they are saved).
|
||||
* `refresh` your instance (so that it contains any new data from the database, like the generated ID).
|
||||
|
||||
```Python hl_lines="18 19 20 21 22 23 24 31 32 33 34 35 36"
|
||||
{!./src/sql_databases/sql_app/crud.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
The SQLAlchemy model for `User` contains a `hashed_password` that should contain a secure hashed version of the password.
|
||||
|
||||
But as what the API client provides is the original password, you need to extract it and generate the hashed password in your application.
|
||||
|
||||
And then pass the `hashed_password` argument with the value to save.
|
||||
|
||||
!!! warning
|
||||
This example is not secure, the password is not hashed.
|
||||
|
||||
In a real life application you would need to hash the password and never save them in plaintext.
|
||||
|
||||
For more details, go back to the Security section in the tutorial.
|
||||
|
||||
Here we are focusing only on the tools and mechanics of databases.
|
||||
|
||||
!!! tip
|
||||
Instead of passing each of the keyword arguments to `Item` and reading each one of them from the Pydantic *model*, we are generating a `dict` with the Pydantic *model*'s data with:
|
||||
|
||||
`item.dict()`
|
||||
|
||||
and then we are passing the `dict`'s key-value pairs as the keyword arguments to the SQLAlchemy `Item`, with:
|
||||
|
||||
`Item(**item.dict())`
|
||||
|
||||
And then we pass the extra keyword argument `owner_id` that is not provided by the Pydantic *model*, with:
|
||||
|
||||
`Item(**item.dict(), owner_id=user_id)`
|
||||
|
||||
## Main **FastAPI** app
|
||||
|
||||
And now in the file `sql_app/main.py` let's integrate and use all the other parts we created before.
|
||||
|
||||
### Create the database tables
|
||||
|
||||
In a very simplistic way, create the database tables:
|
||||
|
||||
```Python hl_lines="11"
|
||||
{!./src/sql_databases/sql_app/main.py!}
|
||||
```
|
||||
|
||||
#### Alembic Note
|
||||
|
||||
Normally you would probably initialize your database (create tables, etc) with <a href="https://alembic.sqlalchemy.org/en/latest/" target="_blank">Alembic</a>.
|
||||
|
||||
And you would also use Alembic for "migrations" (that's its main job).
|
||||
|
||||
A "migration" is the set of steps needed whenever you change the structure of your SQLAlchemy models, add a new attribute, etc. to replicate those changes in the database, add a new column, a new table, etc.
|
||||
|
||||
### Create a middleware to handle sessions
|
||||
|
||||
Now use the `SessionLocal` class we created in the `sql_app/databases.py` file.
|
||||
|
||||
We need to have an independent database session/connection (`SessionLocal`) per request, use the same session through all the request and then close it after the request is finished.
|
||||
|
||||
@@ -108,8 +441,8 @@ A "middleware" is a function that is always executed for each request, and have
|
||||
|
||||
This middleware (just a function) will create a new SQLAlchemy `SessionLocal` for each request, add it to the request and then close it once the request is finished.
|
||||
|
||||
```Python hl_lines="68 69 70 71 72 73 74 75 76"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```Python hl_lines="16 17 18 19 20 21 22 23 24"
|
||||
{!./src/sql_databases/sql_app/main.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
@@ -117,15 +450,15 @@ This middleware (just a function) will create a new SQLAlchemy `SessionLocal` fo
|
||||
|
||||
And then we close it in the `finally` block.
|
||||
|
||||
This way we make sure the database session is always closed after the request. Even if there was an exception in the middle.
|
||||
This way we make sure the database session is always closed after the request. Even if there was an exception while processing the request.
|
||||
|
||||
### About `request.state`
|
||||
#### About `request.state`
|
||||
|
||||
<a href="https://www.starlette.io/requests/#other-state" target="_blank">`request.state` is a property of each Starlette `Request` object</a>, it is there to store arbitrary objects attached to the request itself, like the database session in this case.
|
||||
|
||||
For us in this case, it helps us ensuring a single session/database-connection is used through all the request, and then closed afterwards (in the middleware).
|
||||
For us in this case, it helps us ensuring a single database session is used through all the request, and then closed afterwards (in the middleware).
|
||||
|
||||
## Create a dependency
|
||||
### Create a dependency
|
||||
|
||||
To simplify the code, reduce repetition and get better editor support, we will create a dependency that returns this same database session from the request.
|
||||
|
||||
@@ -133,89 +466,21 @@ And when using the dependency in a path operation function, we declare it with t
|
||||
|
||||
This will then give us better editor support inside the path operation function, because the editor will know that the `db` parameter is of type `Session`.
|
||||
|
||||
```Python hl_lines="54 55 69"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```Python hl_lines="28 29"
|
||||
{!./src/sql_databases/sql_app/main.py!}
|
||||
```
|
||||
|
||||
!!! info "Technical Details"
|
||||
The parameter `db` is actually of type `SessionLocal`, but this class (created with `sessionmaker()`) is a "proxy" of a SQLAlchemy `Session`, so, the editor doesn't really know what methods are provided.
|
||||
|
||||
|
||||
But by declaring the type as `Session`, the editor now can know the available methods (`.add()`, `.query()`, `.commit()`, etc) and can provide better support (like completion). The type declaration doesn't affect the actual object.
|
||||
|
||||
## Create a `CustomBase` model
|
||||
### Create your **FastAPI** *path operations*
|
||||
|
||||
This is more of a trick to facilitate your life than something required.
|
||||
Now, finally, here's the standard **FastAPI** *path operations* code.
|
||||
|
||||
But by creating this `CustomBase` class and inheriting from it, your models will have automatic `__tablename__` attributes (that are required by SQLAlchemy).
|
||||
|
||||
That way you don't have to declare them explicitly in every model.
|
||||
|
||||
So, your models will behave very similarly to, for example, Flask-SQLAlchemy.
|
||||
|
||||
```Python hl_lines="18 19 20 21 22"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Create the SQLAlchemy `Base` model
|
||||
|
||||
```Python hl_lines="25"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Create your application data model
|
||||
|
||||
Now this is finally code specific to your app.
|
||||
|
||||
Here's a user model that will be a table in the database:
|
||||
|
||||
```Python hl_lines="28 29 30 31 32"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Initialize your application
|
||||
|
||||
In a very simplistic way, initialize your database (create the tables, etc) and make sure you have a first user:
|
||||
|
||||
```Python hl_lines="35 37 39 40 41 42 43 45"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
Notice that we close the session with `db_session.close()`.
|
||||
|
||||
We close this session because we only used it to create this first user.
|
||||
|
||||
Every new request will get its own new session.
|
||||
|
||||
### Note
|
||||
|
||||
Normally you would probably initialize your database (create tables, etc) with <a href="https://alembic.sqlalchemy.org/en/latest/" target="_blank">Alembic</a>.
|
||||
|
||||
And you would also use Alembic for migrations (that's its main job). For whenever you change the structure of your database, add a new column, a new table, etc.
|
||||
|
||||
The same way, you would probably make sure there's a first user in an external script that runs before your application, or as part of the application startup.
|
||||
|
||||
In this example we are doing those two operations in a very simple way, directly in the code, to focus on the main points.
|
||||
|
||||
Also, as all the functionality is self-contained in the same code, you can copy it and run it directly, and it will work as is.
|
||||
|
||||
|
||||
## Get a user
|
||||
|
||||
By creating a function that is only dedicated to getting your user from a `user_id` (or any other parameter) independent of your path operation function, you can more easily re-use it in multiple parts and also add <abbr title="Automated tests, written in code, that check if another piece of code is working correctly.">unit tests</abbr> for it:
|
||||
|
||||
```Python hl_lines="49 50"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Create your **FastAPI** code
|
||||
|
||||
Now, finally, here's the standard **FastAPI** code.
|
||||
|
||||
Create your app and path operation function:
|
||||
|
||||
```Python hl_lines="59 62 63 64 65"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```Python hl_lines="32 33 34 35 36 37 40 41 42 43 46 47 48 49 50 51 54 55 56 57 58 61 62 63 64 65"
|
||||
{!./src/sql_databases/sql_app/main.py!}
|
||||
```
|
||||
|
||||
We are creating the database session before each request, attaching it to the request, and then closing it afterwards.
|
||||
@@ -226,32 +491,45 @@ Then, in the dependency `get_db()` we are extracting the database session from t
|
||||
|
||||
And then we can create the dependency in the path operation function, to get that session directly.
|
||||
|
||||
With that, we can just call `get_user` directly from inside of the path operation function and use that session.
|
||||
With that, we can just call `crud.get_user` directly from inside of the path operation function and use that session.
|
||||
|
||||
Having this 3-step process (middleware, dependency, path operation) in this simple example might seem like an overkill. But imagine if you had 20 or 100 path operations, doing this, you would be reducing a lot of code repetition, and getting better support/checks/completion in all those path operation functions.
|
||||
Having this 3-step process (middleware, dependency, path operation) you get better support/checks/completion in all the path operation functions while reducing code repetition.
|
||||
|
||||
## Create the path operation function
|
||||
!!! tip
|
||||
Notice that the values you return are SQLAlchemy models, or lists of SQLAlchemy models.
|
||||
|
||||
Here we are using SQLAlchemy code inside of the path operation function, and in turn it will go and communicate with an external database.
|
||||
But as all the *path operations* have a `response_model` with Pydantic *models* / schemas using `orm_mode`, the data declared in your Pydantic models will be extracted from them and returned to the client, with all the normal filtering and validation.
|
||||
|
||||
!!! tip
|
||||
Also notice that there are `response_models` that have standard Python types like `List[schemas.Item]`.
|
||||
|
||||
But as the content/parameter of that `List` is a Pydantic *model* with `orm_mode`, the data will be retrieved and returned to the client as normally, without problems.
|
||||
|
||||
### About `def` vs `async def`
|
||||
|
||||
Here we are using SQLAlchemy code inside of the path operation function, and, in turn, it will go and communicate with an external database.
|
||||
|
||||
That could potentially require some "waiting".
|
||||
|
||||
But as SQLAlchemy doesn't have compatibility for using `await` directly, as would be with something like:
|
||||
|
||||
```Python
|
||||
user = await get_user(db_session, user_id=user_id)
|
||||
user = await db.query(User).first()
|
||||
```
|
||||
|
||||
...and instead we are using:
|
||||
|
||||
```Python
|
||||
user = get_user(db_session, user_id=user_id)
|
||||
user = db.query(User).first()
|
||||
```
|
||||
|
||||
Then we should declare the path operation without `async def`, just with a normal `def`:
|
||||
Then we should declare the path operation without `async def`, just with a normal `def`, as:
|
||||
|
||||
```Python hl_lines="63"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```Python hl_lines="2"
|
||||
@app.get("/users/{user_id}", response_model=schemas.User)
|
||||
def read_user(user_id: int, db: Session = Depends(get_db)):
|
||||
db_user = crud.get_user(db, user_id=user_id)
|
||||
...
|
||||
```
|
||||
|
||||
!!! note "Very Technical Details"
|
||||
@@ -261,7 +539,49 @@ Then we should declare the path operation without `async def`, just with a norma
|
||||
|
||||
Because we are using SQLAlchemy directly and we don't require any kind of plug-in for it to work with **FastAPI**, we could integrate database <abbr title="Automatically updating the database to have any new column we define in our models.">migrations</abbr> with <a href="https://alembic.sqlalchemy.org" target="_blank">Alembic</a> directly.
|
||||
|
||||
You would probably want to declare your database and models in a different file or set of files, this would allow Alembic to import it and use it without even needing to have **FastAPI** installed for the migrations.
|
||||
And as the code related to SQLAlchemy and the SQLAlchemy models lives in separate independent files, you would even be able to perform the migrations with Alembic without having to install FastAPI, Pydantic, or anything else.
|
||||
|
||||
The same way, you would be able to use the same SQLAlchemy models and utilities in other parts of your code that are not related to **FastAPI**.
|
||||
|
||||
For example, in a background task worker with <a href="http://www.celeryproject.org/" target="_blank">Celery</a>, <a href="https://python-rq.org/" target="_blank">RQ</a>, or <a href="https://arq-docs.helpmanual.io/" target="_blank">ARQ</a>.
|
||||
|
||||
## Review all the files
|
||||
|
||||
Remember you should have a directory named `my_super_project` that contains a sub-directory called `sql_app`.
|
||||
|
||||
`sql_app` should have the following files:
|
||||
|
||||
* `sql_app/__init__.py`: is an empty file.
|
||||
|
||||
* `sql_app/database.py`:
|
||||
|
||||
```Python hl_lines=""
|
||||
{!./src/sql_databases/sql_app/database.py!}
|
||||
```
|
||||
|
||||
* `sql_app/models.py`:
|
||||
|
||||
```Python hl_lines=""
|
||||
{!./src/sql_databases/sql_app/models.py!}
|
||||
```
|
||||
|
||||
* `sql_app/schemas.py`:
|
||||
|
||||
```Python hl_lines=""
|
||||
{!./src/sql_databases/sql_app/schemas.py!}
|
||||
```
|
||||
|
||||
* `sql_app/crud.py`:
|
||||
|
||||
```Python hl_lines=""
|
||||
{!./src/sql_databases/sql_app/crud.py!}
|
||||
```
|
||||
|
||||
* `sql_app/main.py`:
|
||||
|
||||
```Python hl_lines=""
|
||||
{!./src/sql_databases/sql_app/main.py!}
|
||||
```
|
||||
|
||||
## Check it
|
||||
|
||||
@@ -272,12 +592,12 @@ You can copy this code and use it as is.
|
||||
In fact, the code shown here is part of the tests. As most of the code in these docs.
|
||||
|
||||
|
||||
You can copy it, let's say, to a file `main.py`.
|
||||
You can copy it as is.
|
||||
|
||||
Then you can run it with Uvicorn:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --reload
|
||||
uvicorn sql_app.main:app --reload
|
||||
```
|
||||
|
||||
And then, you can open your browser at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
@@ -286,27 +606,6 @@ And you will be able to interact with your **FastAPI** application, reading data
|
||||
|
||||
<img src="/img/tutorial/sql-databases/image01.png">
|
||||
|
||||
## Response schema and security
|
||||
|
||||
This section has the minimum code to show how it works and how you can integrate SQLAlchemy with FastAPI.
|
||||
|
||||
But it is recommended that you also create a response model with Pydantic, as described in the section about <a href="/tutorial/extra-models/" target="_blank">Extra Models</a>.
|
||||
|
||||
That way you will document the schema of the responses of your API, and you will be able to limit/filter the returned data.
|
||||
|
||||
Limiting the returned data is important for security, as for example, you shouldn't be returning the `hashed_password` to the clients.
|
||||
|
||||
That's something that you can improve in this example application, here's the current response data:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"is_active": true,
|
||||
"hashed_password": "notreallyhashed",
|
||||
"email": "johndoe@example.com",
|
||||
"id": 1
|
||||
}
|
||||
```
|
||||
|
||||
## Interact with the database directly
|
||||
|
||||
If you want to explore the SQLite database (file) directly, independently of FastAPI, to debug its contents, add tables, columns, records, modify data, etc. you can use <a href="https://sqlitebrowser.org/" target="_blank">DB Browser for SQLite</a>.
|
||||
@@ -314,3 +613,5 @@ If you want to explore the SQLite database (file) directly, independently of Fas
|
||||
It will look like this:
|
||||
|
||||
<img src="/img/tutorial/sql-databases/image02.png">
|
||||
|
||||
You can also use an online SQLite browser like <a href="https://inloop.github.io/sqlite-viewer/" target="_blank">SQLite Viewer</a> or <a href="https://extendsclass.com/sqlite-browser.html" target="_blank">ExtendsClass</a>.
|
||||
|
||||
59
docs/tutorial/testing-dependencies.md
Normal file
59
docs/tutorial/testing-dependencies.md
Normal file
@@ -0,0 +1,59 @@
|
||||
## Overriding dependencies during testing
|
||||
|
||||
There are some scenarios where you might want to override a dependency during testing.
|
||||
|
||||
You don't want the original dependency to run (nor any of the sub-dependencies it might have).
|
||||
|
||||
Instead, you want to provide a different dependency that will be used only during tests (possibly only some specific tests), and will provide a value that can be used where the value of the original dependency was used.
|
||||
|
||||
### Use cases: external service
|
||||
|
||||
An example could be that you have an external authentication provider that you need to call.
|
||||
|
||||
You send it a token and it returns an authenticated user.
|
||||
|
||||
This provider might be charging you per request, and calling it might take some extra time than if you had a fixed mock user for tests.
|
||||
|
||||
You probably want to test the external provider once, but not necessarily call it for every test that runs.
|
||||
|
||||
In this case, you can override the dependency that calls that provider, and use a custom dependency that returns a mock user, only for your tests.
|
||||
|
||||
### Use case: testing database
|
||||
|
||||
Other example could be that you are using a specific database only for testing.
|
||||
|
||||
Your normal dependency would return a database session.
|
||||
|
||||
But then, after each test, you could want to rollback all the operations or remove data.
|
||||
|
||||
Or you could want to alter the data before the tests run, etc.
|
||||
|
||||
In this case, you could use a dependency override to return your *custom* database session instead of the one that would be used normally.
|
||||
|
||||
### Use the `app.dependency_overrides` attribute
|
||||
|
||||
For these cases, your **FastAPI** application has an attribute `app.dependency_overrides`, it is a simple `dict`.
|
||||
|
||||
To override a dependency for testing, you put as a key the original dependency (a function), and as the value, your dependency override (another function).
|
||||
|
||||
And then **FastAPI** will call that override instead of the original dependency.
|
||||
|
||||
```Python hl_lines="24 25 28"
|
||||
{!./src/dependency_testing/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
You can set a dependency override for a dependency used anywhere in your **FastAPI** application.
|
||||
|
||||
The original dependency could be used in a *path operation function*, a *path operation decorator* (when you don't use the return value), a `.include_router()` call, etc.
|
||||
|
||||
FastAPI will still be able to override it.
|
||||
|
||||
Then you can reset your overrides (remove them) by setting `app.dependency_overrides` to be an empty `dict`:
|
||||
|
||||
```Python
|
||||
app.dependency_overrides = {}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
If you want to override a dependency only during some tests, you can set the override at the beginning of the test (inside the test function) and reset it at the end (at the end of the test function).
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.26.0"
|
||||
__version__ = "0.41.0"
|
||||
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Type, Union
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Type, Union
|
||||
|
||||
from fastapi import routing
|
||||
from fastapi.encoders import DictIntStrAny, SetIntStr
|
||||
from fastapi.exception_handlers import (
|
||||
http_exception_handler,
|
||||
request_validation_exception_handler,
|
||||
@@ -14,6 +15,7 @@ from fastapi.openapi.docs import (
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from fastapi.params import Depends
|
||||
from starlette.applications import Starlette
|
||||
from starlette.datastructures import State
|
||||
from starlette.exceptions import ExceptionMiddleware, HTTPException
|
||||
from starlette.middleware.errors import ServerErrorMiddleware
|
||||
from starlette.requests import Request
|
||||
@@ -32,13 +34,19 @@ class FastAPI(Starlette):
|
||||
version: str = "0.1.0",
|
||||
openapi_url: Optional[str] = "/openapi.json",
|
||||
openapi_prefix: str = "",
|
||||
default_response_class: Type[Response] = JSONResponse,
|
||||
docs_url: Optional[str] = "/docs",
|
||||
redoc_url: Optional[str] = "/redoc",
|
||||
swagger_ui_oauth2_redirect_url: Optional[str] = "/docs/oauth2-redirect",
|
||||
swagger_ui_init_oauth: Optional[dict] = None,
|
||||
**extra: Dict[str, Any],
|
||||
) -> None:
|
||||
self.default_response_class = default_response_class
|
||||
self._debug = debug
|
||||
self.router: routing.APIRouter = routing.APIRouter(routes)
|
||||
self.state = State()
|
||||
self.router: routing.APIRouter = routing.APIRouter(
|
||||
routes, dependency_overrides_provider=self
|
||||
)
|
||||
self.exception_middleware = ExceptionMiddleware(self.router, debug=debug)
|
||||
self.error_middleware = ServerErrorMiddleware(
|
||||
self.exception_middleware, debug=debug
|
||||
@@ -52,7 +60,9 @@ class FastAPI(Starlette):
|
||||
self.docs_url = docs_url
|
||||
self.redoc_url = redoc_url
|
||||
self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
|
||||
self.swagger_ui_init_oauth = swagger_ui_init_oauth
|
||||
self.extra = extra
|
||||
self.dependency_overrides: Dict[Callable, Callable] = {}
|
||||
|
||||
self.openapi_version = "3.0.2"
|
||||
|
||||
@@ -92,6 +102,7 @@ class FastAPI(Starlette):
|
||||
openapi_url=openapi_url,
|
||||
title=self.title + " - Swagger UI",
|
||||
oauth2_redirect_url=self.swagger_ui_oauth2_redirect_url,
|
||||
init_oauth=self.swagger_ui_init_oauth,
|
||||
)
|
||||
|
||||
self.add_route(self.docs_url, swagger_ui_html, include_in_schema=False)
|
||||
@@ -127,7 +138,7 @@ class FastAPI(Starlette):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
dependencies: Sequence[Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
@@ -135,12 +146,12 @@ class FastAPI(Starlette):
|
||||
deprecated: bool = None,
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> None:
|
||||
self.router.add_api_route(
|
||||
@@ -149,7 +160,7 @@ class FastAPI(Starlette):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -162,7 +173,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -173,7 +184,7 @@ class FastAPI(Starlette):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
dependencies: Sequence[Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
@@ -181,12 +192,12 @@ class FastAPI(Starlette):
|
||||
deprecated: bool = None,
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@@ -196,7 +207,7 @@ class FastAPI(Starlette):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -209,7 +220,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
return func
|
||||
@@ -234,8 +245,9 @@ class FastAPI(Starlette):
|
||||
*,
|
||||
prefix: str = "",
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
dependencies: Sequence[Depends] = None,
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
default_response_class: Optional[Type[Response]] = None,
|
||||
) -> None:
|
||||
self.router.include_router(
|
||||
router,
|
||||
@@ -243,6 +255,8 @@ class FastAPI(Starlette):
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
responses=responses or {},
|
||||
default_response_class=default_response_class
|
||||
or self.default_response_class,
|
||||
)
|
||||
|
||||
def get(
|
||||
@@ -252,19 +266,19 @@ class FastAPI(Starlette):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
dependencies: Sequence[Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.get(
|
||||
@@ -272,7 +286,7 @@ class FastAPI(Starlette):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -284,7 +298,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -295,19 +309,19 @@ class FastAPI(Starlette):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
dependencies: Sequence[Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.put(
|
||||
@@ -315,7 +329,7 @@ class FastAPI(Starlette):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -327,7 +341,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -338,19 +352,19 @@ class FastAPI(Starlette):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
dependencies: Sequence[Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.post(
|
||||
@@ -358,7 +372,7 @@ class FastAPI(Starlette):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -370,7 +384,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -381,19 +395,19 @@ class FastAPI(Starlette):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
dependencies: Sequence[Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.delete(
|
||||
@@ -401,7 +415,7 @@ class FastAPI(Starlette):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -413,7 +427,7 @@ class FastAPI(Starlette):
|
||||
operation_id=operation_id,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -424,19 +438,19 @@ class FastAPI(Starlette):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
dependencies: Sequence[Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.options(
|
||||
@@ -444,7 +458,7 @@ class FastAPI(Starlette):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -456,7 +470,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -467,19 +481,19 @@ class FastAPI(Starlette):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
dependencies: Sequence[Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.head(
|
||||
@@ -487,7 +501,7 @@ class FastAPI(Starlette):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -499,7 +513,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -510,19 +524,19 @@ class FastAPI(Starlette):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
dependencies: Sequence[Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.patch(
|
||||
@@ -530,7 +544,7 @@ class FastAPI(Starlette):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -542,7 +556,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -553,19 +567,19 @@ class FastAPI(Starlette):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[Depends] = None,
|
||||
dependencies: Sequence[Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.trace(
|
||||
@@ -573,7 +587,7 @@ class FastAPI(Starlette):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -585,6 +599,6 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -27,9 +27,12 @@ class Dependant:
|
||||
call: Callable = None,
|
||||
request_param_name: str = None,
|
||||
websocket_param_name: str = None,
|
||||
response_param_name: str = None,
|
||||
background_tasks_param_name: str = None,
|
||||
security_scopes_param_name: str = None,
|
||||
security_scopes: List[str] = None,
|
||||
use_cache: bool = True,
|
||||
path: str = None,
|
||||
) -> None:
|
||||
self.path_params = path_params or []
|
||||
self.query_params = query_params or []
|
||||
@@ -40,8 +43,14 @@ class Dependant:
|
||||
self.security_requirements = security_schemes or []
|
||||
self.request_param_name = request_param_name
|
||||
self.websocket_param_name = websocket_param_name
|
||||
self.response_param_name = response_param_name
|
||||
self.background_tasks_param_name = background_tasks_param_name
|
||||
self.security_scopes = security_scopes
|
||||
self.security_scopes_param_name = security_scopes_param_name
|
||||
self.name = name
|
||||
self.call = call
|
||||
self.use_cache = use_cache
|
||||
# Store the path to be able to re-generate a dependable from it in overrides
|
||||
self.path = path
|
||||
# Save the cache key at creation to optimize performance
|
||||
self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or []))))
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
from copy import deepcopy
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
@@ -14,8 +12,8 @@ from typing import (
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import params
|
||||
from fastapi.dependencies.models import Dependant, SecurityRequirement
|
||||
@@ -23,34 +21,34 @@ from fastapi.security.base import SecurityBase
|
||||
from fastapi.security.oauth2 import OAuth2, SecurityScopes
|
||||
from fastapi.security.open_id_connect_url import OpenIdConnect
|
||||
from fastapi.utils import get_path_param_names
|
||||
from pydantic import BaseConfig, Schema, create_model
|
||||
from pydantic import BaseConfig, BaseModel, Schema, create_model
|
||||
from pydantic.error_wrappers import ErrorWrapper
|
||||
from pydantic.errors import MissingError
|
||||
from pydantic.fields import Field, Required, Shape
|
||||
from pydantic.schema import get_annotation_from_schema
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from pydantic.utils import ForwardRef, evaluate_forwardref, lenient_issubclass
|
||||
from starlette.background import BackgroundTasks
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
from starlette.websockets import WebSocket
|
||||
|
||||
param_supported_types = (
|
||||
str,
|
||||
int,
|
||||
float,
|
||||
bool,
|
||||
UUID,
|
||||
date,
|
||||
datetime,
|
||||
time,
|
||||
timedelta,
|
||||
Decimal,
|
||||
)
|
||||
|
||||
sequence_shapes = {Shape.LIST, Shape.SET, Shape.TUPLE}
|
||||
sequence_shapes = {
|
||||
Shape.LIST,
|
||||
Shape.SET,
|
||||
Shape.TUPLE,
|
||||
Shape.SEQUENCE,
|
||||
Shape.TUPLE_ELLIPS,
|
||||
}
|
||||
sequence_types = (list, set, tuple)
|
||||
sequence_shape_to_type = {Shape.LIST: list, Shape.SET: set, Shape.TUPLE: tuple}
|
||||
sequence_shape_to_type = {
|
||||
Shape.LIST: list,
|
||||
Shape.SET: set,
|
||||
Shape.TUPLE: tuple,
|
||||
Shape.SEQUENCE: list,
|
||||
Shape.TUPLE_ELLIPS: list,
|
||||
}
|
||||
|
||||
|
||||
def get_param_sub_dependant(
|
||||
@@ -98,7 +96,11 @@ def get_sub_dependant(
|
||||
security_scheme=dependency, scopes=use_scopes
|
||||
)
|
||||
sub_dependant = get_dependant(
|
||||
path=path, call=dependency, name=name, security_scopes=security_scopes
|
||||
path=path,
|
||||
call=dependency,
|
||||
name=name,
|
||||
security_scopes=security_scopes,
|
||||
use_cache=depends.use_cache,
|
||||
)
|
||||
if security_requirement:
|
||||
sub_dependant.security_requirements.append(security_requirement)
|
||||
@@ -106,7 +108,16 @@ def get_sub_dependant(
|
||||
return sub_dependant
|
||||
|
||||
|
||||
def get_flat_dependant(dependant: Dependant) -> Dependant:
|
||||
CacheKey = Tuple[Optional[Callable], Tuple[str, ...]]
|
||||
|
||||
|
||||
def get_flat_dependant(
|
||||
dependant: Dependant, *, skip_repeats: bool = False, visited: List[CacheKey] = None
|
||||
) -> Dependant:
|
||||
if visited is None:
|
||||
visited = []
|
||||
visited.append(dependant.cache_key)
|
||||
|
||||
flat_dependant = Dependant(
|
||||
path_params=dependant.path_params.copy(),
|
||||
query_params=dependant.query_params.copy(),
|
||||
@@ -114,9 +125,15 @@ def get_flat_dependant(dependant: Dependant) -> Dependant:
|
||||
cookie_params=dependant.cookie_params.copy(),
|
||||
body_params=dependant.body_params.copy(),
|
||||
security_schemes=dependant.security_requirements.copy(),
|
||||
use_cache=dependant.use_cache,
|
||||
path=dependant.path,
|
||||
)
|
||||
for sub_dependant in dependant.dependencies:
|
||||
flat_sub = get_flat_dependant(sub_dependant)
|
||||
if skip_repeats and sub_dependant.cache_key in visited:
|
||||
continue
|
||||
flat_sub = get_flat_dependant(
|
||||
sub_dependant, skip_repeats=skip_repeats, visited=visited
|
||||
)
|
||||
flat_dependant.path_params.extend(flat_sub.path_params)
|
||||
flat_dependant.query_params.extend(flat_sub.query_params)
|
||||
flat_dependant.header_params.extend(flat_sub.header_params)
|
||||
@@ -126,93 +143,151 @@ def get_flat_dependant(dependant: Dependant) -> Dependant:
|
||||
return flat_dependant
|
||||
|
||||
|
||||
def is_scalar_field(field: Field) -> bool:
|
||||
if not (
|
||||
field.shape == Shape.SINGLETON
|
||||
and not lenient_issubclass(field.type_, BaseModel)
|
||||
and not lenient_issubclass(field.type_, sequence_types + (dict,))
|
||||
and not isinstance(field.schema, params.Body)
|
||||
):
|
||||
return False
|
||||
if field.sub_fields:
|
||||
if not all(is_scalar_field(f) for f in field.sub_fields):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def is_scalar_sequence_field(field: Field) -> bool:
|
||||
if (field.shape in sequence_shapes) and not lenient_issubclass(
|
||||
field.type_, BaseModel
|
||||
):
|
||||
if field.sub_fields is not None:
|
||||
for sub_field in field.sub_fields:
|
||||
if not is_scalar_field(sub_field):
|
||||
return False
|
||||
return True
|
||||
if lenient_issubclass(field.type_, sequence_types):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_typed_signature(call: Callable) -> inspect.Signature:
|
||||
signature = inspect.signature(call)
|
||||
globalns = getattr(call, "__globals__", {})
|
||||
typed_params = [
|
||||
inspect.Parameter(
|
||||
name=param.name,
|
||||
kind=param.kind,
|
||||
default=param.default,
|
||||
annotation=get_typed_annotation(param, globalns),
|
||||
)
|
||||
for param in signature.parameters.values()
|
||||
]
|
||||
typed_signature = inspect.Signature(typed_params)
|
||||
return typed_signature
|
||||
|
||||
|
||||
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:
|
||||
annotation = param.annotation
|
||||
if isinstance(annotation, str):
|
||||
annotation = ForwardRef(annotation)
|
||||
annotation = evaluate_forwardref(annotation, globalns, globalns)
|
||||
return annotation
|
||||
|
||||
|
||||
def get_dependant(
|
||||
*, path: str, call: Callable, name: str = None, security_scopes: List[str] = None
|
||||
*,
|
||||
path: str,
|
||||
call: Callable,
|
||||
name: str = None,
|
||||
security_scopes: List[str] = None,
|
||||
use_cache: bool = True,
|
||||
) -> Dependant:
|
||||
path_param_names = get_path_param_names(path)
|
||||
endpoint_signature = inspect.signature(call)
|
||||
endpoint_signature = get_typed_signature(call)
|
||||
signature_params = endpoint_signature.parameters
|
||||
dependant = Dependant(call=call, name=name)
|
||||
for param_name in signature_params:
|
||||
param = signature_params[param_name]
|
||||
dependant = Dependant(call=call, name=name, path=path, use_cache=use_cache)
|
||||
for param_name, param in signature_params.items():
|
||||
if isinstance(param.default, params.Depends):
|
||||
sub_dependant = get_param_sub_dependant(
|
||||
param=param, path=path, security_scopes=security_scopes
|
||||
)
|
||||
dependant.dependencies.append(sub_dependant)
|
||||
for param_name in signature_params:
|
||||
param = signature_params[param_name]
|
||||
if (
|
||||
(param.default == param.empty) or isinstance(param.default, params.Path)
|
||||
) and (param_name in path_param_names):
|
||||
assert (
|
||||
lenient_issubclass(param.annotation, param_supported_types)
|
||||
or param.annotation == param.empty
|
||||
for param_name, param in signature_params.items():
|
||||
if isinstance(param.default, params.Depends):
|
||||
continue
|
||||
if add_non_field_param_to_dependency(param=param, dependant=dependant):
|
||||
continue
|
||||
param_field = get_param_field(param=param, default_schema=params.Query)
|
||||
if param_name in path_param_names:
|
||||
assert is_scalar_field(
|
||||
field=param_field
|
||||
), f"Path params must be of one of the supported types"
|
||||
add_param_to_fields(
|
||||
if isinstance(param.default, params.Path):
|
||||
ignore_default = False
|
||||
else:
|
||||
ignore_default = True
|
||||
param_field = get_param_field(
|
||||
param=param,
|
||||
dependant=dependant,
|
||||
default_schema=params.Path,
|
||||
force_type=params.ParamTypes.path,
|
||||
ignore_default=ignore_default,
|
||||
)
|
||||
elif (
|
||||
param.default == param.empty
|
||||
or param.default is None
|
||||
or isinstance(param.default, param_supported_types)
|
||||
) and (
|
||||
param.annotation == param.empty
|
||||
or lenient_issubclass(param.annotation, param_supported_types)
|
||||
):
|
||||
add_param_to_fields(
|
||||
param=param, dependant=dependant, default_schema=params.Query
|
||||
)
|
||||
elif isinstance(param.default, params.Param):
|
||||
if param.annotation != param.empty:
|
||||
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
|
||||
)
|
||||
elif lenient_issubclass(param.annotation, Request):
|
||||
dependant.request_param_name = param_name
|
||||
elif lenient_issubclass(param.annotation, WebSocket):
|
||||
dependant.websocket_param_name = param_name
|
||||
elif lenient_issubclass(param.annotation, BackgroundTasks):
|
||||
dependant.background_tasks_param_name = param_name
|
||||
elif lenient_issubclass(param.annotation, SecurityScopes):
|
||||
dependant.security_scopes_param_name = param_name
|
||||
elif not isinstance(param.default, params.Depends):
|
||||
add_param_to_body_fields(param=param, dependant=dependant)
|
||||
add_param_to_fields(field=param_field, dependant=dependant)
|
||||
elif is_scalar_field(field=param_field):
|
||||
add_param_to_fields(field=param_field, dependant=dependant)
|
||||
elif isinstance(
|
||||
param.default, (params.Query, params.Header)
|
||||
) and is_scalar_sequence_field(param_field):
|
||||
add_param_to_fields(field=param_field, dependant=dependant)
|
||||
else:
|
||||
assert isinstance(
|
||||
param_field.schema, params.Body
|
||||
), f"Param: {param_field.name} can only be a request body, using Body(...)"
|
||||
dependant.body_params.append(param_field)
|
||||
return dependant
|
||||
|
||||
|
||||
def add_param_to_fields(
|
||||
def add_non_field_param_to_dependency(
|
||||
*, param: inspect.Parameter, dependant: Dependant
|
||||
) -> Optional[bool]:
|
||||
if lenient_issubclass(param.annotation, Request):
|
||||
dependant.request_param_name = param.name
|
||||
return True
|
||||
elif lenient_issubclass(param.annotation, WebSocket):
|
||||
dependant.websocket_param_name = param.name
|
||||
return True
|
||||
elif lenient_issubclass(param.annotation, Response):
|
||||
dependant.response_param_name = param.name
|
||||
return True
|
||||
elif lenient_issubclass(param.annotation, BackgroundTasks):
|
||||
dependant.background_tasks_param_name = param.name
|
||||
return True
|
||||
elif lenient_issubclass(param.annotation, SecurityScopes):
|
||||
dependant.security_scopes_param_name = param.name
|
||||
return True
|
||||
return None
|
||||
|
||||
|
||||
def get_param_field(
|
||||
*,
|
||||
param: inspect.Parameter,
|
||||
dependant: Dependant,
|
||||
default_schema: Type[Schema] = params.Param,
|
||||
default_schema: Type[params.Param] = params.Param,
|
||||
force_type: params.ParamTypes = None,
|
||||
) -> None:
|
||||
ignore_default: bool = False,
|
||||
) -> Field:
|
||||
default_value = Required
|
||||
if not param.default == param.empty:
|
||||
had_schema = False
|
||||
if not param.default == param.empty and ignore_default is False:
|
||||
default_value = param.default
|
||||
if isinstance(default_value, params.Param):
|
||||
if isinstance(default_value, Schema):
|
||||
had_schema = True
|
||||
schema = default_value
|
||||
default_value = schema.default
|
||||
if getattr(schema, "in_", None) is None:
|
||||
if isinstance(schema, params.Param) and getattr(schema, "in_", None) is None:
|
||||
schema.in_ = default_schema.in_
|
||||
if force_type:
|
||||
schema.in_ = force_type
|
||||
schema.in_ = force_type # type: ignore
|
||||
else:
|
||||
schema = default_schema(default_value)
|
||||
required = default_value == Required
|
||||
@@ -234,43 +309,26 @@ def add_param_to_fields(
|
||||
class_validators={},
|
||||
schema=schema,
|
||||
)
|
||||
if schema.in_ == params.ParamTypes.path:
|
||||
if not had_schema and not is_scalar_field(field=field):
|
||||
field.schema = params.Body(schema.default)
|
||||
return field
|
||||
|
||||
|
||||
def add_param_to_fields(*, field: Field, dependant: Dependant) -> None:
|
||||
field.schema = cast(params.Param, field.schema)
|
||||
if field.schema.in_ == params.ParamTypes.path:
|
||||
dependant.path_params.append(field)
|
||||
elif schema.in_ == params.ParamTypes.query:
|
||||
elif field.schema.in_ == params.ParamTypes.query:
|
||||
dependant.query_params.append(field)
|
||||
elif schema.in_ == params.ParamTypes.header:
|
||||
elif field.schema.in_ == params.ParamTypes.header:
|
||||
dependant.header_params.append(field)
|
||||
else:
|
||||
assert (
|
||||
schema.in_ == params.ParamTypes.cookie
|
||||
), f"non-body parameters must be in path, query, header or cookie: {param.name}"
|
||||
field.schema.in_ == params.ParamTypes.cookie
|
||||
), f"non-body parameters must be in path, query, header or cookie: {field.name}"
|
||||
dependant.cookie_params.append(field)
|
||||
|
||||
|
||||
def add_param_to_body_fields(*, param: inspect.Parameter, dependant: Dependant) -> None:
|
||||
default_value = Required
|
||||
if not param.default == param.empty:
|
||||
default_value = param.default
|
||||
if isinstance(default_value, Schema):
|
||||
schema = default_value
|
||||
default_value = schema.default
|
||||
else:
|
||||
schema = Schema(default_value)
|
||||
required = default_value == Required
|
||||
annotation = get_annotation_from_schema(param.annotation, schema)
|
||||
field = Field(
|
||||
name=param.name,
|
||||
type_=annotation,
|
||||
default=None if required else default_value,
|
||||
alias=schema.alias or param.name,
|
||||
required=required,
|
||||
model_config=BaseConfig,
|
||||
class_validators={},
|
||||
schema=schema,
|
||||
)
|
||||
dependant.body_params.append(field)
|
||||
|
||||
|
||||
def is_coroutine_callable(call: Callable) -> bool:
|
||||
if inspect.isfunction(call):
|
||||
return asyncio.iscoroutinefunction(call)
|
||||
@@ -284,28 +342,82 @@ async def solve_dependencies(
|
||||
*,
|
||||
request: Union[Request, WebSocket],
|
||||
dependant: Dependant,
|
||||
body: Dict[str, Any] = None,
|
||||
body: Optional[Union[Dict[str, Any], FormData]] = None,
|
||||
background_tasks: BackgroundTasks = None,
|
||||
) -> Tuple[Dict[str, Any], List[ErrorWrapper], Optional[BackgroundTasks]]:
|
||||
response: Response = None,
|
||||
dependency_overrides_provider: Any = None,
|
||||
dependency_cache: Dict[Tuple[Callable, Tuple[str]], Any] = None,
|
||||
) -> Tuple[
|
||||
Dict[str, Any],
|
||||
List[ErrorWrapper],
|
||||
Optional[BackgroundTasks],
|
||||
Response,
|
||||
Dict[Tuple[Callable, Tuple[str]], Any],
|
||||
]:
|
||||
values: Dict[str, Any] = {}
|
||||
errors: List[ErrorWrapper] = []
|
||||
response = response or Response(
|
||||
content=None,
|
||||
status_code=None, # type: ignore
|
||||
headers=None,
|
||||
media_type=None,
|
||||
background=None,
|
||||
)
|
||||
dependency_cache = dependency_cache or {}
|
||||
sub_dependant: Dependant
|
||||
for sub_dependant in dependant.dependencies:
|
||||
sub_values, sub_errors, background_tasks = await solve_dependencies(
|
||||
sub_dependant.call = cast(Callable, sub_dependant.call)
|
||||
sub_dependant.cache_key = cast(
|
||||
Tuple[Callable, Tuple[str]], sub_dependant.cache_key
|
||||
)
|
||||
call = sub_dependant.call
|
||||
use_sub_dependant = sub_dependant
|
||||
if (
|
||||
dependency_overrides_provider
|
||||
and dependency_overrides_provider.dependency_overrides
|
||||
):
|
||||
original_call = sub_dependant.call
|
||||
call = getattr(
|
||||
dependency_overrides_provider, "dependency_overrides", {}
|
||||
).get(original_call, original_call)
|
||||
use_path: str = sub_dependant.path # type: ignore
|
||||
use_sub_dependant = get_dependant(
|
||||
path=use_path,
|
||||
call=call,
|
||||
name=sub_dependant.name,
|
||||
security_scopes=sub_dependant.security_scopes,
|
||||
)
|
||||
|
||||
solved_result = await solve_dependencies(
|
||||
request=request,
|
||||
dependant=sub_dependant,
|
||||
dependant=use_sub_dependant,
|
||||
body=body,
|
||||
background_tasks=background_tasks,
|
||||
response=response,
|
||||
dependency_overrides_provider=dependency_overrides_provider,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
sub_values, sub_errors, background_tasks, sub_response, sub_dependency_cache = (
|
||||
solved_result
|
||||
)
|
||||
sub_response = cast(Response, sub_response)
|
||||
response.headers.raw.extend(sub_response.headers.raw)
|
||||
if sub_response.status_code:
|
||||
response.status_code = sub_response.status_code
|
||||
dependency_cache.update(sub_dependency_cache)
|
||||
if sub_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)
|
||||
if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache:
|
||||
solved = dependency_cache[sub_dependant.cache_key]
|
||||
elif is_coroutine_callable(call):
|
||||
solved = await call(**sub_values)
|
||||
else:
|
||||
solved = await run_in_threadpool(sub_dependant.call, **sub_values)
|
||||
solved = await run_in_threadpool(call, **sub_values)
|
||||
if sub_dependant.name is not None:
|
||||
values[sub_dependant.name] = solved
|
||||
if sub_dependant.cache_key not in dependency_cache:
|
||||
dependency_cache[sub_dependant.cache_key] = solved
|
||||
path_values, path_errors = request_params_to_args(
|
||||
dependant.path_params, request.path_params
|
||||
)
|
||||
@@ -324,8 +436,8 @@ async def solve_dependencies(
|
||||
values.update(cookie_values)
|
||||
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
|
||||
body_values, body_errors = await request_body_to_args( # body_params checked above
|
||||
required_params=dependant.body_params, received_body=body
|
||||
)
|
||||
values.update(body_values)
|
||||
errors.extend(body_errors)
|
||||
@@ -337,11 +449,13 @@ async def solve_dependencies(
|
||||
if background_tasks is None:
|
||||
background_tasks = BackgroundTasks()
|
||||
values[dependant.background_tasks_param_name] = background_tasks
|
||||
if dependant.response_param_name:
|
||||
values[dependant.response_param_name] = response
|
||||
if dependant.security_scopes_param_name:
|
||||
values[dependant.security_scopes_param_name] = SecurityScopes(
|
||||
scopes=dependant.security_scopes
|
||||
)
|
||||
return values, errors, background_tasks
|
||||
return values, errors, background_tasks, response, dependency_cache
|
||||
|
||||
|
||||
def request_params_to_args(
|
||||
@@ -351,13 +465,13 @@ def request_params_to_args(
|
||||
values = {}
|
||||
errors = []
|
||||
for field in required_params:
|
||||
if field.shape in sequence_shapes and isinstance(
|
||||
if is_scalar_sequence_field(field) and isinstance(
|
||||
received_params, (QueryParams, Headers)
|
||||
):
|
||||
value = received_params.getlist(field.alias)
|
||||
value = received_params.getlist(field.alias) or field.default
|
||||
else:
|
||||
value = received_params.get(field.alias)
|
||||
schema: params.Param = field.schema
|
||||
schema = field.schema
|
||||
assert isinstance(schema, params.Param), "Params must be subclasses of Param"
|
||||
if value is None:
|
||||
if field.required:
|
||||
@@ -382,7 +496,8 @@ def request_params_to_args(
|
||||
|
||||
|
||||
async def request_body_to_args(
|
||||
required_params: List[Field], received_body: Dict[str, Any]
|
||||
required_params: List[Field],
|
||||
received_body: Optional[Union[Dict[str, Any], FormData]],
|
||||
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
|
||||
values = {}
|
||||
errors = []
|
||||
@@ -392,10 +507,14 @@ async def request_body_to_args(
|
||||
if len(required_params) == 1 and not embed:
|
||||
received_body = {field.alias: received_body}
|
||||
for field in required_params:
|
||||
if field.shape in sequence_shapes and isinstance(received_body, FormData):
|
||||
value = received_body.getlist(field.alias)
|
||||
else:
|
||||
value = received_body.get(field.alias)
|
||||
value: Any = None
|
||||
if received_body is not None:
|
||||
if field.shape in sequence_shapes and isinstance(
|
||||
received_body, FormData
|
||||
):
|
||||
value = received_body.getlist(field.alias)
|
||||
else:
|
||||
value = received_body.get(field.alias)
|
||||
if (
|
||||
value is None
|
||||
or (isinstance(field.schema, params.Form) and value == "")
|
||||
@@ -471,6 +590,8 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[Field]:
|
||||
for f in flat_dependant.body_params:
|
||||
BodyModel.__fields__[f.name] = get_schema_compatible_field(field=f)
|
||||
required = any(True for f in flat_dependant.body_params if f.required)
|
||||
|
||||
BodySchema_kwargs: Dict[str, Any] = dict(default=None)
|
||||
if any(isinstance(f.schema, params.File) for f in flat_dependant.body_params):
|
||||
BodySchema: Type[params.Body] = params.File
|
||||
elif any(isinstance(f.schema, params.Form) for f in flat_dependant.body_params):
|
||||
@@ -478,6 +599,14 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[Field]:
|
||||
else:
|
||||
BodySchema = params.Body
|
||||
|
||||
body_param_media_types = [
|
||||
getattr(f.schema, "media_type")
|
||||
for f in flat_dependant.body_params
|
||||
if isinstance(f.schema, params.Body)
|
||||
]
|
||||
if len(set(body_param_media_types)) == 1:
|
||||
BodySchema_kwargs["media_type"] = body_param_media_types[0]
|
||||
|
||||
field = Field(
|
||||
name="body",
|
||||
type_=BodyModel,
|
||||
@@ -486,6 +615,6 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[Field]:
|
||||
model_config=BaseConfig,
|
||||
class_validators={},
|
||||
alias="body",
|
||||
schema=BodySchema(None),
|
||||
schema=BodySchema(**BodySchema_kwargs),
|
||||
)
|
||||
return field
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
from enum import Enum
|
||||
from types import GeneratorType
|
||||
from typing import Any, List, Set
|
||||
from typing import Any, Dict, List, Set, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic.json import ENCODERS_BY_TYPE
|
||||
|
||||
SetIntStr = Set[Union[int, str]]
|
||||
DictIntStrAny = Dict[Union[int, str], Any]
|
||||
|
||||
|
||||
def jsonable_encoder(
|
||||
obj: Any,
|
||||
include: Set[str] = None,
|
||||
exclude: Set[str] = set(),
|
||||
include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
by_alias: bool = True,
|
||||
skip_defaults: bool = False,
|
||||
include_none: bool = True,
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
from typing import Any, Sequence
|
||||
|
||||
from pydantic import ValidationError
|
||||
from pydantic.error_wrappers import ErrorList
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.websockets import WebSocket
|
||||
|
||||
|
||||
class HTTPException(StarletteHTTPException):
|
||||
def __init__(
|
||||
self, status_code: int, detail: str = None, headers: dict = None
|
||||
self, status_code: int, detail: Any = None, headers: dict = None
|
||||
) -> None:
|
||||
super().__init__(status_code=status_code, detail=detail)
|
||||
self.headers = headers
|
||||
|
||||
|
||||
class RequestValidationError(ValidationError):
|
||||
pass
|
||||
def __init__(self, errors: Sequence[ErrorList]) -> None:
|
||||
super().__init__(errors, Request)
|
||||
|
||||
|
||||
class WebSocketRequestValidationError(ValidationError):
|
||||
pass
|
||||
def __init__(self, errors: Sequence[ErrorList]) -> None:
|
||||
super().__init__(errors, WebSocket)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
|
||||
@@ -11,10 +13,11 @@ def get_swagger_ui_html(
|
||||
swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css",
|
||||
swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png",
|
||||
oauth2_redirect_url: Optional[str] = None,
|
||||
init_oauth: Optional[dict] = None,
|
||||
) -> HTMLResponse:
|
||||
|
||||
html = f"""
|
||||
<! doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link type="text/css" rel="stylesheet" href="{swagger_css_url}">
|
||||
@@ -40,8 +43,16 @@ def get_swagger_ui_html(
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "BaseLayout"
|
||||
})
|
||||
layout: "BaseLayout",
|
||||
deepLinking: true
|
||||
})"""
|
||||
|
||||
if init_oauth:
|
||||
html += f"""
|
||||
ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})
|
||||
"""
|
||||
|
||||
html += """
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -55,6 +66,7 @@ def get_redoc_html(
|
||||
title: str,
|
||||
redoc_js_url: str = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js",
|
||||
redoc_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png",
|
||||
with_google_fonts: bool = True,
|
||||
) -> HTMLResponse:
|
||||
html = f"""
|
||||
<!DOCTYPE html>
|
||||
@@ -64,7 +76,12 @@ def get_redoc_html(
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
"""
|
||||
if with_google_fonts:
|
||||
html += """
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
||||
"""
|
||||
html += f"""
|
||||
<link rel="shortcut icon" href="{redoc_favicon_url}">
|
||||
<!--
|
||||
ReDoc doesn't change outer page styles
|
||||
@@ -87,7 +104,7 @@ def get_redoc_html(
|
||||
|
||||
def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse:
|
||||
html = """
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<body onload="run()">
|
||||
</body>
|
||||
|
||||
@@ -11,7 +11,7 @@ try:
|
||||
import email_validator
|
||||
|
||||
assert email_validator # make autoflake ignore the unused import
|
||||
from pydantic.types import EmailStr # type: ignore
|
||||
from pydantic.types import EmailStr
|
||||
except ImportError: # pragma: no cover
|
||||
logger.warning(
|
||||
"email-validator not installed, email fields will be treated as str.\n"
|
||||
@@ -122,7 +122,7 @@ class Schema(SchemaBase):
|
||||
not_: Optional[List[SchemaBase]] = PSchema(None, alias="not") # type: ignore
|
||||
items: Optional[SchemaBase] = None
|
||||
properties: Optional[Dict[str, SchemaBase]] = None
|
||||
additionalProperties: Optional[Union[SchemaBase, bool]] = None
|
||||
additionalProperties: Optional[Union[SchemaBase, bool]] = None # type: ignore
|
||||
|
||||
|
||||
class Example(BaseModel):
|
||||
@@ -149,9 +149,9 @@ class Encoding(BaseModel):
|
||||
|
||||
|
||||
class MediaType(BaseModel):
|
||||
schema_: Optional[Union[Schema, Reference]] = PSchema(
|
||||
schema_: Optional[Union[Schema, Reference]] = PSchema( # type: ignore
|
||||
None, alias="schema"
|
||||
) # type: ignore
|
||||
)
|
||||
example: Optional[Any] = None
|
||||
examples: Optional[Dict[str, Union[Example, Reference]]] = None
|
||||
encoding: Optional[Dict[str, Encoding]] = None
|
||||
@@ -165,9 +165,9 @@ class ParameterBase(BaseModel):
|
||||
style: Optional[str] = None
|
||||
explode: Optional[bool] = None
|
||||
allowReserved: Optional[bool] = None
|
||||
schema_: Optional[Union[Schema, Reference]] = PSchema(
|
||||
schema_: Optional[Union[Schema, Reference]] = PSchema( # type: ignore
|
||||
None, alias="schema"
|
||||
) # type: ignore
|
||||
)
|
||||
example: Optional[Any] = None
|
||||
examples: Optional[Dict[str, Union[Example, Reference]]] = None
|
||||
# Serialization rules for more complex scenarios
|
||||
@@ -210,10 +210,6 @@ class Response(BaseModel):
|
||||
links: Optional[Dict[str, Union[Link, Reference]]] = None
|
||||
|
||||
|
||||
class Responses(BaseModel):
|
||||
default: Response
|
||||
|
||||
|
||||
class Operation(BaseModel):
|
||||
tags: Optional[List[str]] = None
|
||||
summary: Optional[str] = None
|
||||
@@ -222,7 +218,7 @@ class Operation(BaseModel):
|
||||
operationId: Optional[str] = None
|
||||
parameters: Optional[List[Union[Parameter, Reference]]] = None
|
||||
requestBody: Optional[Union[RequestBody, Reference]] = None
|
||||
responses: Union[Responses, Dict[str, Response]]
|
||||
responses: Dict[str, Response]
|
||||
# Workaround OpenAPI recursive reference
|
||||
callbacks: Optional[Dict[str, Union[Dict[str, Any], Reference]]] = None
|
||||
deprecated: Optional[bool] = None
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type
|
||||
import http.client
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, cast
|
||||
|
||||
from fastapi import routing
|
||||
from fastapi.dependencies.models import Dependant
|
||||
@@ -7,9 +8,13 @@ from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX
|
||||
from fastapi.openapi.models import OpenAPI
|
||||
from fastapi.params import Body, Param
|
||||
from fastapi.utils import get_flat_models_from_routes, get_model_definitions
|
||||
from fastapi.utils import (
|
||||
generate_operation_id_for_path,
|
||||
get_flat_models_from_routes,
|
||||
get_model_definitions,
|
||||
)
|
||||
from pydantic.fields import Field
|
||||
from pydantic.schema import Schema, field_schema, get_model_name_map
|
||||
from pydantic.schema import field_schema, get_model_name_map
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.routing import BaseRoute
|
||||
@@ -38,9 +43,18 @@ validation_error_response_definition = {
|
||||
},
|
||||
}
|
||||
|
||||
status_code_ranges: Dict[str, str] = {
|
||||
"1XX": "Information",
|
||||
"2XX": "Success",
|
||||
"3XX": "Redirection",
|
||||
"4XX": "Client Error",
|
||||
"5XX": "Server Error",
|
||||
"DEFAULT": "Default Response",
|
||||
}
|
||||
|
||||
|
||||
def get_openapi_params(dependant: Dependant) -> List[Field]:
|
||||
flat_dependant = get_flat_dependant(dependant)
|
||||
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
|
||||
return (
|
||||
flat_dependant.path_params
|
||||
+ flat_dependant.query_params
|
||||
@@ -66,14 +80,11 @@ def get_openapi_security_definitions(flat_dependant: Dependant) -> Tuple[Dict, L
|
||||
|
||||
def get_openapi_operation_parameters(
|
||||
all_route_params: Sequence[Field]
|
||||
) -> Tuple[Dict[str, Dict], List[Dict[str, Any]]]:
|
||||
definitions: Dict[str, Dict] = {}
|
||||
) -> List[Dict[str, Any]]:
|
||||
parameters = []
|
||||
for param in all_route_params:
|
||||
schema: Param = param.schema
|
||||
if "ValidationError" not in definitions:
|
||||
definitions["ValidationError"] = validation_error_definition
|
||||
definitions["HTTPValidationError"] = validation_error_response_definition
|
||||
schema = param.schema
|
||||
schema = cast(Param, schema)
|
||||
parameter = {
|
||||
"name": param.alias,
|
||||
"in": schema.in_.value,
|
||||
@@ -85,24 +96,20 @@ def get_openapi_operation_parameters(
|
||||
if schema.deprecated:
|
||||
parameter["deprecated"] = schema.deprecated
|
||||
parameters.append(parameter)
|
||||
return definitions, parameters
|
||||
return parameters
|
||||
|
||||
|
||||
def get_openapi_operation_request_body(
|
||||
*, body_field: Field, model_name_map: Dict[Type, str]
|
||||
*, body_field: Optional[Field], model_name_map: Dict[Type, str]
|
||||
) -> Optional[Dict]:
|
||||
if not body_field:
|
||||
return None
|
||||
assert isinstance(body_field, Field)
|
||||
body_schema, _ = field_schema(
|
||||
body_schema, _, _ = field_schema(
|
||||
body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)
|
||||
schema: Schema = body_field.schema
|
||||
if isinstance(schema, Body):
|
||||
request_media_type = schema.media_type
|
||||
else:
|
||||
# Includes not declared media types (Schema)
|
||||
request_media_type = "application/json"
|
||||
body_field.schema = cast(Body, body_field.schema)
|
||||
request_media_type = body_field.schema.media_type
|
||||
required = body_field.required
|
||||
request_body_oai: Dict[str, Any] = {}
|
||||
if required:
|
||||
@@ -115,10 +122,7 @@ def generate_operation_id(*, route: routing.APIRoute, method: str) -> str:
|
||||
if route.operation_id:
|
||||
return route.operation_id
|
||||
path: str = route.path_format
|
||||
operation_id = route.name + path
|
||||
operation_id = operation_id.replace("{", "_").replace("}", "_").replace("/", "_")
|
||||
operation_id = operation_id + "_" + method.lower()
|
||||
return operation_id
|
||||
return generate_operation_id_for_path(name=route.name, path=path, method=method)
|
||||
|
||||
|
||||
def generate_operation_summary(*, route: routing.APIRoute, method: str) -> str:
|
||||
@@ -147,11 +151,15 @@ def get_openapi_path(
|
||||
security_schemes: Dict[str, Any] = {}
|
||||
definitions: Dict[str, Any] = {}
|
||||
assert route.methods is not None, "Methods must be a list"
|
||||
assert (
|
||||
route.response_class and route.response_class.media_type
|
||||
), "A response class with media_type is needed to generate OpenAPI"
|
||||
route_response_media_type: str = route.response_class.media_type
|
||||
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)
|
||||
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
|
||||
security_definitions, operation_security = get_openapi_security_definitions(
|
||||
flat_dependant=flat_dependant
|
||||
)
|
||||
@@ -160,10 +168,7 @@ def get_openapi_path(
|
||||
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)
|
||||
operation_parameters = get_openapi_operation_parameters(all_route_params)
|
||||
parameters.extend(operation_parameters)
|
||||
if parameters:
|
||||
operation["parameters"] = parameters
|
||||
@@ -173,11 +178,6 @@ def get_openapi_path(
|
||||
)
|
||||
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
|
||||
if route.responses:
|
||||
for (additional_status_code, response) in route.responses.items():
|
||||
assert isinstance(
|
||||
@@ -185,21 +185,27 @@ def get_openapi_path(
|
||||
), "An additional response must be a dict"
|
||||
field = route.response_fields.get(additional_status_code)
|
||||
if field:
|
||||
response_schema, _ = field_schema(
|
||||
response_schema, _, _ = field_schema(
|
||||
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)
|
||||
response.setdefault("content", {}).setdefault(
|
||||
"application/json", {}
|
||||
route_response_media_type, {}
|
||||
)["schema"] = response_schema
|
||||
response.setdefault("description", "Additional Response")
|
||||
operation.setdefault("responses", {})[
|
||||
str(additional_status_code)
|
||||
] = response
|
||||
status_text: Optional[str] = status_code_ranges.get(
|
||||
str(additional_status_code).upper()
|
||||
) or http.client.responses.get(int(additional_status_code))
|
||||
response.setdefault(
|
||||
"description", status_text or "Additional Response"
|
||||
)
|
||||
status_code_key = str(additional_status_code).upper()
|
||||
if status_code_key == "DEFAULT":
|
||||
status_code_key = "default"
|
||||
operation.setdefault("responses", {})[status_code_key] = response
|
||||
status_code = str(route.status_code)
|
||||
response_schema = {"type": "string"}
|
||||
if lenient_issubclass(route.response_class, JSONResponse):
|
||||
if route.response_field:
|
||||
response_schema, _ = field_schema(
|
||||
response_schema, _, _ = field_schema(
|
||||
route.response_field,
|
||||
model_name_map=model_name_map,
|
||||
ref_prefix=REF_PREFIX,
|
||||
@@ -211,11 +217,18 @@ def get_openapi_path(
|
||||
] = route.response_description
|
||||
operation.setdefault("responses", {}).setdefault(
|
||||
status_code, {}
|
||||
).setdefault("content", {}).setdefault(route.response_class.media_type, {})[
|
||||
).setdefault("content", {}).setdefault(route_response_media_type, {})[
|
||||
"schema"
|
||||
] = response_schema
|
||||
if all_route_params or route.body_field:
|
||||
operation["responses"][str(HTTP_422_UNPROCESSABLE_ENTITY)] = {
|
||||
|
||||
http422 = str(HTTP_422_UNPROCESSABLE_ENTITY)
|
||||
if (all_route_params or route.body_field) and not any(
|
||||
[
|
||||
status in operation["responses"]
|
||||
for status in [http422, "4XX", "default"]
|
||||
]
|
||||
):
|
||||
operation["responses"][http422] = {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
@@ -223,6 +236,13 @@ def get_openapi_path(
|
||||
}
|
||||
},
|
||||
}
|
||||
if "ValidationError" not in definitions:
|
||||
definitions.update(
|
||||
{
|
||||
"ValidationError": validation_error_definition,
|
||||
"HTTPValidationError": validation_error_response_definition,
|
||||
}
|
||||
)
|
||||
path[method.lower()] = operation
|
||||
return path, security_schemes, definitions
|
||||
|
||||
@@ -263,7 +283,7 @@ def get_openapi(
|
||||
if path_definitions:
|
||||
definitions.update(path_definitions)
|
||||
if definitions:
|
||||
components.setdefault("schemas", {}).update(definitions)
|
||||
components["schemas"] = {k: definitions[k] for k in sorted(definitions)}
|
||||
if components:
|
||||
output["components"] = components
|
||||
output["paths"] = paths
|
||||
|
||||
@@ -238,11 +238,13 @@ def File( # noqa: N802
|
||||
)
|
||||
|
||||
|
||||
def Depends(dependency: Callable = None) -> Any: # noqa: N802
|
||||
return params.Depends(dependency=dependency)
|
||||
def Depends( # noqa: N802
|
||||
dependency: Callable = None, *, use_cache: bool = True
|
||||
) -> Any:
|
||||
return params.Depends(dependency=dependency, use_cache=use_cache)
|
||||
|
||||
|
||||
def Security( # noqa: N802
|
||||
dependency: Callable = None, scopes: Sequence[str] = None
|
||||
dependency: Callable = None, *, scopes: Sequence[str] = None, use_cache: bool = True
|
||||
) -> Any:
|
||||
return params.Security(dependency=dependency, scopes=scopes)
|
||||
return params.Security(dependency=dependency, scopes=scopes, use_cache=use_cache)
|
||||
|
||||
@@ -308,11 +308,18 @@ class File(Form):
|
||||
|
||||
|
||||
class Depends:
|
||||
def __init__(self, dependency: Callable = None):
|
||||
def __init__(self, dependency: Callable = None, *, use_cache: bool = True):
|
||||
self.dependency = dependency
|
||||
self.use_cache = use_cache
|
||||
|
||||
|
||||
class Security(Depends):
|
||||
def __init__(self, dependency: Callable = None, scopes: Sequence[str] = None):
|
||||
def __init__(
|
||||
self,
|
||||
dependency: Callable = None,
|
||||
*,
|
||||
scopes: Sequence[str] = None,
|
||||
use_cache: bool = True,
|
||||
):
|
||||
super().__init__(dependency=dependency, use_cache=use_cache)
|
||||
self.scopes = scopes or []
|
||||
super().__init__(dependency=dependency)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Type, Union
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Type, Union
|
||||
|
||||
from fastapi import params
|
||||
from fastapi.dependencies.models import Dependant
|
||||
@@ -12,8 +11,9 @@ from fastapi.dependencies.utils import (
|
||||
get_parameterless_sub_dependant,
|
||||
solve_dependencies,
|
||||
)
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
|
||||
from fastapi.utils import create_cloned_field, generate_operation_id_for_path
|
||||
from pydantic import BaseConfig, BaseModel, Schema
|
||||
from pydantic.error_wrappers import ErrorWrapper, ValidationError
|
||||
from pydantic.fields import Field
|
||||
@@ -30,6 +30,7 @@ from starlette.routing import (
|
||||
websocket_session,
|
||||
)
|
||||
from starlette.status import WS_1008_POLICY_VIOLATION
|
||||
from starlette.types import ASGIApp
|
||||
from starlette.websockets import WebSocket
|
||||
|
||||
|
||||
@@ -37,28 +38,22 @@ def serialize_response(
|
||||
*,
|
||||
field: Field = None,
|
||||
response: Response,
|
||||
include: Set[str] = None,
|
||||
exclude: Set[str] = set(),
|
||||
include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
by_alias: bool = True,
|
||||
skip_defaults: bool = False,
|
||||
) -> Any:
|
||||
|
||||
encoded = jsonable_encoder(
|
||||
response,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
skip_defaults=skip_defaults,
|
||||
)
|
||||
if field:
|
||||
errors = []
|
||||
value, errors_ = field.validate(encoded, {}, loc=("response",))
|
||||
if skip_defaults and isinstance(response, BaseModel):
|
||||
response = response.dict(skip_defaults=skip_defaults)
|
||||
value, errors_ = field.validate(response, {}, loc=("response",))
|
||||
if isinstance(errors_, ErrorWrapper):
|
||||
errors.append(errors_)
|
||||
elif isinstance(errors_, list):
|
||||
errors.extend(errors_)
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
raise ValidationError(errors, field.type_)
|
||||
return jsonable_encoder(
|
||||
value,
|
||||
include=include,
|
||||
@@ -67,19 +62,20 @@ def serialize_response(
|
||||
skip_defaults=skip_defaults,
|
||||
)
|
||||
else:
|
||||
return encoded
|
||||
return jsonable_encoder(response)
|
||||
|
||||
|
||||
def get_app(
|
||||
def get_request_handler(
|
||||
dependant: Dependant,
|
||||
body_field: Field = None,
|
||||
status_code: int = 200,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_field: Field = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
dependency_overrides_provider: Any = None,
|
||||
) -> Callable:
|
||||
assert dependant.call is not None, "dependant.call must be a function"
|
||||
is_coroutine = asyncio.iscoroutinefunction(dependant.call)
|
||||
@@ -100,9 +96,13 @@ def get_app(
|
||||
raise HTTPException(
|
||||
status_code=400, detail="There was an error parsing the body"
|
||||
) from e
|
||||
values, errors, background_tasks = await solve_dependencies(
|
||||
request=request, dependant=dependant, body=body
|
||||
solved_result = await solve_dependencies(
|
||||
request=request,
|
||||
dependant=dependant,
|
||||
body=body,
|
||||
dependency_overrides_provider=dependency_overrides_provider,
|
||||
)
|
||||
values, errors, background_tasks, sub_response, _ = solved_result
|
||||
if errors:
|
||||
raise RequestValidationError(errors)
|
||||
else:
|
||||
@@ -123,38 +123,57 @@ def get_app(
|
||||
by_alias=response_model_by_alias,
|
||||
skip_defaults=response_model_skip_defaults,
|
||||
)
|
||||
return response_class(
|
||||
response = response_class(
|
||||
content=response_data,
|
||||
status_code=status_code,
|
||||
background=background_tasks,
|
||||
)
|
||||
response.headers.raw.extend(sub_response.headers.raw)
|
||||
if sub_response.status_code:
|
||||
response.status_code = sub_response.status_code
|
||||
return response
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def get_websocket_app(dependant: Dependant) -> Callable:
|
||||
def get_websocket_app(
|
||||
dependant: Dependant, dependency_overrides_provider: Any = None
|
||||
) -> Callable:
|
||||
async def app(websocket: WebSocket) -> None:
|
||||
values, errors, _ = await solve_dependencies(
|
||||
request=websocket, dependant=dependant
|
||||
solved_result = await solve_dependencies(
|
||||
request=websocket,
|
||||
dependant=dependant,
|
||||
dependency_overrides_provider=dependency_overrides_provider,
|
||||
)
|
||||
values, errors, _, _2, _3 = solved_result
|
||||
if errors:
|
||||
await websocket.close(code=WS_1008_POLICY_VIOLATION)
|
||||
raise WebSocketRequestValidationError(errors)
|
||||
assert dependant.call is not None, "dependant.call must me a function"
|
||||
assert dependant.call is not None, "dependant.call must be a function"
|
||||
await dependant.call(**values)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
class APIWebSocketRoute(routing.WebSocketRoute):
|
||||
def __init__(self, path: str, endpoint: Callable, *, name: str = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
path: str,
|
||||
endpoint: Callable,
|
||||
*,
|
||||
name: str = None,
|
||||
dependency_overrides_provider: Any = None,
|
||||
) -> None:
|
||||
self.path = path
|
||||
self.endpoint = endpoint
|
||||
self.name = get_name(endpoint) if name is None else name
|
||||
self.dependant = get_dependant(path=path, call=self.endpoint)
|
||||
self.app = websocket_session(get_websocket_app(dependant=self.dependant))
|
||||
regex = "^" + path + "$"
|
||||
regex = re.sub("{([a-zA-Z_][a-zA-Z0-9_]*)}", r"(?P<\1>[^/]+)", regex)
|
||||
self.app = websocket_session(
|
||||
get_websocket_app(
|
||||
dependant=self.dependant,
|
||||
dependency_overrides_provider=dependency_overrides_provider,
|
||||
)
|
||||
)
|
||||
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
|
||||
|
||||
|
||||
@@ -167,32 +186,36 @@ class APIRoute(routing.Route):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
dependencies: Sequence[params.Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
name: str = None,
|
||||
methods: List[str] = None,
|
||||
methods: Optional[Union[Set[str], List[str]]] = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
dependency_overrides_provider: Any = None,
|
||||
) -> None:
|
||||
assert path.startswith("/"), "Routed paths must always start with '/'"
|
||||
self.path = path
|
||||
self.endpoint = endpoint
|
||||
self.name = get_name(endpoint) if name is None else name
|
||||
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
|
||||
if methods is None:
|
||||
methods = ["GET"]
|
||||
self.methods = set([method.upper() for method in methods])
|
||||
self.unique_id = generate_operation_id_for_path(
|
||||
name=self.name, path=self.path_format, method=list(methods)[0]
|
||||
)
|
||||
self.response_model = response_model
|
||||
if self.response_model:
|
||||
assert lenient_issubclass(
|
||||
response_class, JSONResponse
|
||||
), "To declare a type the response must be a JSON response"
|
||||
response_name = "Response_" + self.name
|
||||
response_name = "Response_" + self.unique_id
|
||||
self.response_field: Optional[Field] = Field(
|
||||
name=response_name,
|
||||
type_=self.response_model,
|
||||
@@ -202,13 +225,30 @@ class APIRoute(routing.Route):
|
||||
model_config=BaseConfig,
|
||||
schema=Schema(None),
|
||||
)
|
||||
# Create a clone of the field, so that a Pydantic submodel is not returned
|
||||
# as is just because it's an instance of a subclass of a more limited class
|
||||
# e.g. UserInDB (containing hashed_password) could be a subclass of User
|
||||
# that doesn't have the hashed_password. But because it's a subclass, it
|
||||
# would pass the validation and be returned as is.
|
||||
# By being a new field, no inheritance will be passed as is. A new model
|
||||
# will be always created.
|
||||
self.secure_cloned_response_field: Optional[Field] = create_cloned_field(
|
||||
self.response_field
|
||||
)
|
||||
else:
|
||||
self.response_field = None
|
||||
self.secure_cloned_response_field = None
|
||||
self.status_code = status_code
|
||||
self.tags = tags or []
|
||||
self.dependencies = dependencies or []
|
||||
if dependencies:
|
||||
self.dependencies = list(dependencies)
|
||||
else:
|
||||
self.dependencies = []
|
||||
self.summary = summary
|
||||
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
|
||||
# if a "form feed" character (page break) is found in the description text,
|
||||
# truncate description text to the content preceding the first "form feed"
|
||||
self.description = self.description.split("\f")[0]
|
||||
self.response_description = response_description
|
||||
self.responses = responses or {}
|
||||
response_fields = {}
|
||||
@@ -219,7 +259,7 @@ class APIRoute(routing.Route):
|
||||
assert lenient_issubclass(
|
||||
model, BaseModel
|
||||
), "A response model must be a Pydantic model"
|
||||
response_name = f"Response_{additional_status_code}_{self.name}"
|
||||
response_name = f"Response_{additional_status_code}_{self.unique_id}"
|
||||
response_field = Field(
|
||||
name=response_name,
|
||||
type_=model,
|
||||
@@ -235,9 +275,6 @@ class APIRoute(routing.Route):
|
||||
else:
|
||||
self.response_fields = {}
|
||||
self.deprecated = deprecated
|
||||
if methods is None:
|
||||
methods = ["GET"]
|
||||
self.methods = methods
|
||||
self.operation_id = operation_id
|
||||
self.response_model_include = response_model_include
|
||||
self.response_model_exclude = response_model_exclude
|
||||
@@ -246,7 +283,6 @@ class APIRoute(routing.Route):
|
||||
self.include_in_schema = include_in_schema
|
||||
self.response_class = response_class
|
||||
|
||||
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"
|
||||
@@ -256,23 +292,40 @@ class APIRoute(routing.Route):
|
||||
0,
|
||||
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
|
||||
)
|
||||
self.body_field = get_body_field(dependant=self.dependant, name=self.name)
|
||||
self.app = request_response(
|
||||
get_app(
|
||||
dependant=self.dependant,
|
||||
body_field=self.body_field,
|
||||
status_code=self.status_code,
|
||||
response_class=self.response_class,
|
||||
response_field=self.response_field,
|
||||
response_model_include=self.response_model_include,
|
||||
response_model_exclude=self.response_model_exclude,
|
||||
response_model_by_alias=self.response_model_by_alias,
|
||||
response_model_skip_defaults=self.response_model_skip_defaults,
|
||||
)
|
||||
self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id)
|
||||
self.dependency_overrides_provider = dependency_overrides_provider
|
||||
self.app = request_response(self.get_route_handler())
|
||||
|
||||
def get_route_handler(self) -> Callable:
|
||||
return get_request_handler(
|
||||
dependant=self.dependant,
|
||||
body_field=self.body_field,
|
||||
status_code=self.status_code,
|
||||
response_class=self.response_class or JSONResponse,
|
||||
response_field=self.secure_cloned_response_field,
|
||||
response_model_include=self.response_model_include,
|
||||
response_model_exclude=self.response_model_exclude,
|
||||
response_model_by_alias=self.response_model_by_alias,
|
||||
response_model_skip_defaults=self.response_model_skip_defaults,
|
||||
dependency_overrides_provider=self.dependency_overrides_provider,
|
||||
)
|
||||
|
||||
|
||||
class APIRouter(routing.Router):
|
||||
def __init__(
|
||||
self,
|
||||
routes: List[routing.BaseRoute] = None,
|
||||
redirect_slashes: bool = True,
|
||||
default: ASGIApp = None,
|
||||
dependency_overrides_provider: Any = None,
|
||||
route_class: Type[APIRoute] = APIRoute,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
routes=routes, redirect_slashes=redirect_slashes, default=default
|
||||
)
|
||||
self.dependency_overrides_provider = dependency_overrides_provider
|
||||
self.route_class = route_class
|
||||
|
||||
def add_api_route(
|
||||
self,
|
||||
path: str,
|
||||
@@ -281,29 +334,31 @@ class APIRouter(routing.Router):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
dependencies: Sequence[params.Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
methods: List[str] = None,
|
||||
methods: Optional[Union[Set[str], List[str]]] = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
route_class_override: Optional[Type[APIRoute]] = None,
|
||||
) -> None:
|
||||
route = APIRoute(
|
||||
route_class = route_class_override or self.route_class
|
||||
route = route_class(
|
||||
path,
|
||||
endpoint=endpoint,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -318,6 +373,7 @@ class APIRouter(routing.Router):
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
dependency_overrides_provider=self.dependency_overrides_provider,
|
||||
)
|
||||
self.routes.append(route)
|
||||
|
||||
@@ -328,7 +384,7 @@ class APIRouter(routing.Router):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
dependencies: Sequence[params.Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
@@ -336,12 +392,12 @@ class APIRouter(routing.Router):
|
||||
deprecated: bool = None,
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@@ -351,7 +407,7 @@ class APIRouter(routing.Router):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -390,14 +446,23 @@ class APIRouter(routing.Router):
|
||||
*,
|
||||
prefix: str = "",
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
dependencies: Sequence[params.Depends] = None,
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
default_response_class: Optional[Type[Response]] = None,
|
||||
) -> None:
|
||||
if prefix:
|
||||
assert prefix.startswith("/"), "A path prefix must start with '/'"
|
||||
assert not prefix.endswith(
|
||||
"/"
|
||||
), "A path prefix must not end with '/', as the routes will start with '/'"
|
||||
else:
|
||||
for r in router.routes:
|
||||
path = getattr(r, "path")
|
||||
name = getattr(r, "name", "unknown")
|
||||
if path is not None and not path:
|
||||
raise Exception(
|
||||
f"Prefix and path cannot be both empty (path operation: {name})"
|
||||
)
|
||||
if responses is None:
|
||||
responses = {}
|
||||
for route in router.routes:
|
||||
@@ -409,7 +474,8 @@ class APIRouter(routing.Router):
|
||||
response_model=route.response_model,
|
||||
status_code=route.status_code,
|
||||
tags=(route.tags or []) + (tags or []),
|
||||
dependencies=(dependencies or []) + (route.dependencies or []),
|
||||
dependencies=list(dependencies or [])
|
||||
+ list(route.dependencies or []),
|
||||
summary=route.summary,
|
||||
description=route.description,
|
||||
response_description=route.response_description,
|
||||
@@ -422,14 +488,15 @@ class APIRouter(routing.Router):
|
||||
response_model_by_alias=route.response_model_by_alias,
|
||||
response_model_skip_defaults=route.response_model_skip_defaults,
|
||||
include_in_schema=route.include_in_schema,
|
||||
response_class=route.response_class,
|
||||
response_class=route.response_class or default_response_class,
|
||||
name=route.name,
|
||||
route_class_override=type(route),
|
||||
)
|
||||
elif isinstance(route, routing.Route):
|
||||
self.add_route(
|
||||
prefix + route.path,
|
||||
route.endpoint,
|
||||
methods=route.methods,
|
||||
methods=list(route.methods or []),
|
||||
include_in_schema=route.include_in_schema,
|
||||
name=route.name,
|
||||
)
|
||||
@@ -449,28 +516,27 @@ class APIRouter(routing.Router):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
dependencies: Sequence[params.Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
|
||||
return self.api_route(
|
||||
path=path,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -494,19 +560,19 @@ class APIRouter(routing.Router):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
dependencies: Sequence[params.Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -514,7 +580,7 @@ class APIRouter(routing.Router):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -538,19 +604,19 @@ class APIRouter(routing.Router):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
dependencies: Sequence[params.Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -558,7 +624,7 @@ class APIRouter(routing.Router):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -582,19 +648,19 @@ class APIRouter(routing.Router):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
dependencies: Sequence[params.Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -602,7 +668,7 @@ class APIRouter(routing.Router):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -626,19 +692,19 @@ class APIRouter(routing.Router):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
dependencies: Sequence[params.Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -646,7 +712,7 @@ class APIRouter(routing.Router):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -670,19 +736,19 @@ class APIRouter(routing.Router):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
dependencies: Sequence[params.Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -690,7 +756,7 @@ class APIRouter(routing.Router):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -714,19 +780,19 @@ class APIRouter(routing.Router):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
dependencies: Sequence[params.Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -734,7 +800,7 @@ class APIRouter(routing.Router):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
@@ -758,19 +824,19 @@ class APIRouter(routing.Router):
|
||||
response_model: Type[Any] = None,
|
||||
status_code: int = 200,
|
||||
tags: List[str] = None,
|
||||
dependencies: List[params.Depends] = None,
|
||||
dependencies: Sequence[params.Depends] = None,
|
||||
summary: str = None,
|
||||
description: str = None,
|
||||
response_description: str = "Successful Response",
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -778,7 +844,7 @@ class APIRouter(routing.Router):
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=tags or [],
|
||||
dependencies=dependencies or [],
|
||||
dependencies=dependencies,
|
||||
summary=summary,
|
||||
description=description,
|
||||
response_description=response_description,
|
||||
|
||||
@@ -54,7 +54,7 @@ class APIKeyCookie(APIKeyBase):
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
api_key: str = request.cookies.get(self.model.name)
|
||||
api_key = request.cookies.get(self.model.name)
|
||||
if not api_key:
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -56,7 +56,9 @@ class HTTPBasic(HTTPBase):
|
||||
self.realm = realm
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[HTTPBasicCredentials]:
|
||||
async def __call__( # type: ignore
|
||||
self, request: Request
|
||||
) -> Optional[HTTPBasicCredentials]:
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if self.realm:
|
||||
@@ -112,10 +114,13 @@ class HTTPBearer(HTTPBase):
|
||||
else:
|
||||
return None
|
||||
if scheme.lower() != "bearer":
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
detail="Invalid authentication credentials",
|
||||
)
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
detail="Invalid authentication credentials",
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import List, Optional
|
||||
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.openapi.models import OAuth2 as OAuth2Model, OAuthFlows as OAuthFlowsModel
|
||||
from fastapi.params import Form
|
||||
from fastapi.param_functions import Form
|
||||
from fastapi.security.base import SecurityBase
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from starlette.requests import Request
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import re
|
||||
from typing import Any, Dict, List, Sequence, Set, Type
|
||||
from dataclasses import is_dataclass
|
||||
from typing import Any, Dict, List, Sequence, Set, Type, cast
|
||||
|
||||
from fastapi import routing
|
||||
from fastapi.openapi.constants import REF_PREFIX
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseConfig, BaseModel, Schema, create_model
|
||||
from pydantic.fields import Field
|
||||
from pydantic.schema import get_flat_models_from_fields, model_process_schema
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from starlette.routing import BaseRoute
|
||||
|
||||
|
||||
def get_flat_models_from_routes(
|
||||
routes: Sequence[Type[BaseRoute]]
|
||||
) -> Set[Type[BaseModel]]:
|
||||
def get_flat_models_from_routes(routes: Sequence[BaseRoute]) -> Set[Type[BaseModel]]:
|
||||
body_fields_from_routes: List[Field] = []
|
||||
responses_from_routes: List[Field] = []
|
||||
for route in routes:
|
||||
@@ -28,7 +28,7 @@ def get_flat_models_from_routes(
|
||||
if route.response_fields:
|
||||
responses_from_routes.extend(route.response_fields.values())
|
||||
flat_models = get_flat_models_from_fields(
|
||||
body_fields_from_routes + responses_from_routes
|
||||
body_fields_from_routes + responses_from_routes, known_models=set()
|
||||
)
|
||||
return flat_models
|
||||
|
||||
@@ -38,7 +38,7 @@ def get_model_definitions(
|
||||
) -> Dict[str, Any]:
|
||||
definitions: Dict[str, Dict] = {}
|
||||
for model in flat_models:
|
||||
m_schema, m_definitions = model_process_schema(
|
||||
m_schema, m_definitions, m_nested_models = model_process_schema(
|
||||
model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)
|
||||
definitions.update(m_definitions)
|
||||
@@ -49,3 +49,57 @@ def get_model_definitions(
|
||||
|
||||
def get_path_param_names(path: str) -> Set[str]:
|
||||
return {item.strip("{}") for item in re.findall("{[^}]*}", path)}
|
||||
|
||||
|
||||
def create_cloned_field(field: Field) -> Field:
|
||||
original_type = field.type_
|
||||
if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"):
|
||||
original_type = original_type.__pydantic_model__ # type: ignore
|
||||
use_type = original_type
|
||||
if lenient_issubclass(original_type, BaseModel):
|
||||
original_type = cast(Type[BaseModel], original_type)
|
||||
use_type = create_model(
|
||||
original_type.__name__,
|
||||
__config__=original_type.__config__,
|
||||
__validators__=original_type.__validators__, # type: ignore
|
||||
)
|
||||
for f in original_type.__fields__.values():
|
||||
use_type.__fields__[f.name] = f
|
||||
new_field = Field(
|
||||
name=field.name,
|
||||
type_=use_type,
|
||||
class_validators={},
|
||||
default=None,
|
||||
required=False,
|
||||
model_config=BaseConfig,
|
||||
schema=Schema(None),
|
||||
)
|
||||
new_field.has_alias = field.has_alias
|
||||
new_field.alias = field.alias
|
||||
new_field.class_validators = field.class_validators
|
||||
new_field.default = field.default
|
||||
new_field.required = field.required
|
||||
new_field.model_config = field.model_config
|
||||
new_field.schema = field.schema
|
||||
new_field.allow_none = field.allow_none
|
||||
new_field.validate_always = field.validate_always
|
||||
if field.sub_fields:
|
||||
new_field.sub_fields = [
|
||||
create_cloned_field(sub_field) for sub_field in field.sub_fields
|
||||
]
|
||||
if field.key_field:
|
||||
new_field.key_field = create_cloned_field(field.key_field)
|
||||
new_field.validators = field.validators
|
||||
new_field.whole_pre_validators = field.whole_pre_validators
|
||||
new_field.whole_post_validators = field.whole_post_validators
|
||||
new_field.parse_json = field.parse_json
|
||||
new_field.shape = field.shape
|
||||
new_field._populate_validators()
|
||||
return new_field
|
||||
|
||||
|
||||
def generate_operation_id_for_path(*, name: str, path: str, method: str) -> str:
|
||||
operation_id = name + path
|
||||
operation_id = operation_id.replace("{", "_").replace("}", "_").replace("/", "_")
|
||||
operation_id = operation_id + "_" + method.lower()
|
||||
return operation_id
|
||||
|
||||
@@ -51,6 +51,7 @@ nav:
|
||||
- Additional Responses in OpenAPI: 'tutorial/additional-responses.md'
|
||||
- Response Cookies: 'tutorial/response-cookies.md'
|
||||
- Response Headers: 'tutorial/response-headers.md'
|
||||
- Response - Change Status Code: 'tutorial/response-change-status-code.md'
|
||||
- Dependencies:
|
||||
- First Steps: 'tutorial/dependencies/first-steps.md'
|
||||
- Classes as Dependencies: 'tutorial/dependencies/classes-as-dependencies.md'
|
||||
@@ -80,7 +81,9 @@ nav:
|
||||
- GraphQL: 'tutorial/graphql.md'
|
||||
- WebSockets: 'tutorial/websockets.md'
|
||||
- 'Events: startup - shutdown': 'tutorial/events.md'
|
||||
- Custom Request and APIRoute class: 'tutorial/custom-request-and-route.md'
|
||||
- Testing: 'tutorial/testing.md'
|
||||
- Testing Dependencies with Overrides: 'tutorial/testing-dependencies.md'
|
||||
- Debugging: 'tutorial/debugging.md'
|
||||
- Extending OpenAPI: 'tutorial/extending-openapi.md'
|
||||
- Concurrency and async / await: 'async.md'
|
||||
@@ -88,6 +91,7 @@ nav:
|
||||
- Project Generation - Template: 'project-generation.md'
|
||||
- Alternatives, Inspiration and Comparisons: 'alternatives.md'
|
||||
- History, Design and Future: 'history-design-future.md'
|
||||
- External Links and Articles: 'external-links.md'
|
||||
- Benchmarks: 'benchmarks.md'
|
||||
- Help FastAPI - Get Help: 'help-fastapi.md'
|
||||
- Development - Contributing: 'contributing.md'
|
||||
|
||||
@@ -19,8 +19,8 @@ classifiers = [
|
||||
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
||||
]
|
||||
requires = [
|
||||
"starlette >=0.11.1,<=0.12.0",
|
||||
"pydantic >=0.17,<=0.26.0"
|
||||
"starlette >=0.12.9,<=0.12.9",
|
||||
"pydantic >=0.32.2,<=0.32.2"
|
||||
]
|
||||
description-file = "README.md"
|
||||
requires-python = ">=3.6"
|
||||
@@ -39,6 +39,7 @@ test = [
|
||||
"email_validator",
|
||||
"sqlalchemy",
|
||||
"databases[sqlite]",
|
||||
"orjson"
|
||||
]
|
||||
doc = [
|
||||
"mkdocs",
|
||||
|
||||
6
scripts/format-imports.sh
Executable file
6
scripts/format-imports.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh -e
|
||||
set -x
|
||||
|
||||
# Sort imports one per line, so autoflake can remove unused imports
|
||||
isort --recursive --force-single-line-imports --thirdparty fastapi --apply fastapi tests docs/src
|
||||
sh ./scripts/format.sh
|
||||
@@ -3,6 +3,6 @@
|
||||
set -e
|
||||
set -x
|
||||
|
||||
mypy fastapi --disallow-untyped-defs --follow-imports=skip
|
||||
mypy fastapi --disallow-untyped-defs
|
||||
black fastapi tests --check
|
||||
isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --check-only --thirdparty fastapi fastapi tests
|
||||
|
||||
40
tests/test_additional_responses_bad.py
Normal file
40
tests/test_additional_responses_bad.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/a", responses={"hello": {"description": "Not a valid additional response"}})
|
||||
async def a():
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/a": {
|
||||
"get": {
|
||||
"responses": {
|
||||
# this is how one would imagine the openapi schema to be
|
||||
# but since the key is not valid, openapi.utils.get_openapi will raise ValueError
|
||||
"hello": {"description": "Not a valid additional response"},
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
},
|
||||
"summary": "A",
|
||||
"operationId": "a_a_get",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
with pytest.raises(ValueError):
|
||||
client.get("/openapi.json")
|
||||
100
tests/test_additional_responses_custom_validationerror.py
Normal file
100
tests/test_additional_responses_custom_validationerror.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import typing
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class JsonApiResponse(JSONResponse):
|
||||
media_type = "application/vnd.api+json"
|
||||
|
||||
|
||||
class Error(BaseModel):
|
||||
status: str
|
||||
title: str
|
||||
|
||||
|
||||
class JsonApiError(BaseModel):
|
||||
errors: typing.List[Error]
|
||||
|
||||
|
||||
@app.get(
|
||||
"/a/{id}",
|
||||
response_class=JsonApiResponse,
|
||||
responses={422: {"description": "Error", "model": JsonApiError}},
|
||||
)
|
||||
async def a(id):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/a/{id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"422": {
|
||||
"description": "Error",
|
||||
"content": {
|
||||
"application/vnd.api+json": {
|
||||
"schema": {"$ref": "#/components/schemas/JsonApiError"}
|
||||
}
|
||||
},
|
||||
},
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/vnd.api+json": {"schema": {}}},
|
||||
},
|
||||
},
|
||||
"summary": "A",
|
||||
"operationId": "a_a__id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Id"},
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Error": {
|
||||
"title": "Error",
|
||||
"required": ["status", "title"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {"title": "Status", "type": "string"},
|
||||
"title": {"title": "Title", "type": "string"},
|
||||
},
|
||||
},
|
||||
"JsonApiError": {
|
||||
"title": "JsonApiError",
|
||||
"required": ["errors"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"errors": {
|
||||
"title": "Errors",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/Error"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
@@ -1,20 +1,21 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from sql_databases.tutorial001 import app
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/a/{id}")
|
||||
async def a(id):
|
||||
pass # pragma: no cover
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/{user_id}": {
|
||||
"/a/{id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
@@ -25,14 +26,18 @@ openapi_schema = {
|
||||
}
|
||||
},
|
||||
},
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
},
|
||||
"summary": "Read User",
|
||||
"operationId": "read_user_users__user_id__get",
|
||||
"summary": "A",
|
||||
"operationId": "a_a__id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "User_Id", "type": "integer"},
|
||||
"name": "user_id",
|
||||
"schema": {"title": "Id"},
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
@@ -71,18 +76,10 @@ openapi_schema = {
|
||||
}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_first_user():
|
||||
response = client.get("/users/1")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"is_active": True,
|
||||
"hashed_password": "notreallyhashed",
|
||||
"email": "johndoe@example.com",
|
||||
"id": 1,
|
||||
}
|
||||
117
tests/test_additional_responses_response_class.py
Normal file
117
tests/test_additional_responses_response_class.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import typing
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class JsonApiResponse(JSONResponse):
|
||||
media_type = "application/vnd.api+json"
|
||||
|
||||
|
||||
class Error(BaseModel):
|
||||
status: str
|
||||
title: str
|
||||
|
||||
|
||||
class JsonApiError(BaseModel):
|
||||
errors: typing.List[Error]
|
||||
|
||||
|
||||
@app.get(
|
||||
"/a",
|
||||
response_class=JsonApiResponse,
|
||||
responses={500: {"description": "Error", "model": JsonApiError}},
|
||||
)
|
||||
async def a():
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
@app.get("/b", responses={500: {"description": "Error", "model": Error}})
|
||||
async def b():
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/a": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"500": {
|
||||
"description": "Error",
|
||||
"content": {
|
||||
"application/vnd.api+json": {
|
||||
"schema": {"$ref": "#/components/schemas/JsonApiError"}
|
||||
}
|
||||
},
|
||||
},
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/vnd.api+json": {"schema": {}}},
|
||||
},
|
||||
},
|
||||
"summary": "A",
|
||||
"operationId": "a_a_get",
|
||||
}
|
||||
},
|
||||
"/b": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"500": {
|
||||
"description": "Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Error"}
|
||||
}
|
||||
},
|
||||
},
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
},
|
||||
"summary": "B",
|
||||
"operationId": "b_b_get",
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Error": {
|
||||
"title": "Error",
|
||||
"required": ["status", "title"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {"title": "Status", "type": "string"},
|
||||
"title": {"title": "Title", "type": "string"},
|
||||
},
|
||||
},
|
||||
"JsonApiError": {
|
||||
"title": "JsonApiError",
|
||||
"required": ["errors"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"errors": {
|
||||
"title": "Errors",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/Error"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
@@ -10,12 +10,25 @@ async def a():
|
||||
return "a"
|
||||
|
||||
|
||||
@router.get("/b", responses={502: {"description": "Error 2"}})
|
||||
@router.get(
|
||||
"/b",
|
||||
responses={
|
||||
502: {"description": "Error 2"},
|
||||
"4XX": {"description": "Error with range, upper"},
|
||||
},
|
||||
)
|
||||
async def b():
|
||||
return "b"
|
||||
|
||||
|
||||
@router.get("/c", responses={501: {"description": "Error 3"}})
|
||||
@router.get(
|
||||
"/c",
|
||||
responses={
|
||||
"400": {"description": "Error with str"},
|
||||
"5xx": {"description": "Error with range, lower"},
|
||||
"default": {"description": "A default response"},
|
||||
},
|
||||
)
|
||||
async def c():
|
||||
return "c"
|
||||
|
||||
@@ -43,6 +56,7 @@ openapi_schema = {
|
||||
"get": {
|
||||
"responses": {
|
||||
"502": {"description": "Error 2"},
|
||||
"4XX": {"description": "Error with range, upper"},
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
@@ -55,11 +69,13 @@ openapi_schema = {
|
||||
"/c": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"501": {"description": "Error 3"},
|
||||
"400": {"description": "Error with str"},
|
||||
"5XX": {"description": "Error with range, lower"},
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"default": {"description": "A default response"},
|
||||
},
|
||||
"summary": "C",
|
||||
"operationId": "c_c_get",
|
||||
|
||||
114
tests/test_custom_route_class.py
Normal file
114
tests/test_custom_route_class.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import pytest
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class APIRouteA(APIRoute):
|
||||
x_type = "A"
|
||||
|
||||
|
||||
class APIRouteB(APIRoute):
|
||||
x_type = "B"
|
||||
|
||||
|
||||
class APIRouteC(APIRoute):
|
||||
x_type = "C"
|
||||
|
||||
|
||||
router_a = APIRouter(route_class=APIRouteA)
|
||||
router_b = APIRouter(route_class=APIRouteB)
|
||||
router_c = APIRouter(route_class=APIRouteC)
|
||||
|
||||
|
||||
@router_a.get("/")
|
||||
def get_a():
|
||||
return {"msg": "A"}
|
||||
|
||||
|
||||
@router_b.get("/")
|
||||
def get_b():
|
||||
return {"msg": "B"}
|
||||
|
||||
|
||||
@router_c.get("/")
|
||||
def get_c():
|
||||
return {"msg": "C"}
|
||||
|
||||
|
||||
router_b.include_router(router=router_c, prefix="/c")
|
||||
router_a.include_router(router=router_b, prefix="/b")
|
||||
app.include_router(router=router_a, prefix="/a")
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/a/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Get A",
|
||||
"operationId": "get_a_a__get",
|
||||
}
|
||||
},
|
||||
"/a/b/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Get B",
|
||||
"operationId": "get_b_a_b__get",
|
||||
}
|
||||
},
|
||||
"/a/b/c/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Get C",
|
||||
"operationId": "get_c_a_b_c__get",
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_status,expected_response",
|
||||
[
|
||||
("/a", 200, {"msg": "A"}),
|
||||
("/a/b", 200, {"msg": "B"}),
|
||||
("/a/b/c", 200, {"msg": "C"}),
|
||||
("/openapi.json", 200, openapi_schema),
|
||||
],
|
||||
)
|
||||
def test_get_path(path, expected_status, expected_response):
|
||||
response = client.get(path)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
|
||||
|
||||
def test_route_classes():
|
||||
routes = {}
|
||||
r: APIRoute
|
||||
for r in app.router.routes:
|
||||
routes[r.path] = r
|
||||
assert routes["/a/"].x_type == "A"
|
||||
assert routes["/a/b/"].x_type == "B"
|
||||
assert routes["/a/b/c/"].x_type == "C"
|
||||
216
tests/test_default_response_class.py
Normal file
216
tests/test_default_response_class.py
Normal file
@@ -0,0 +1,216 @@
|
||||
from typing import Any
|
||||
|
||||
import orjson
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
class ORJSONResponse(JSONResponse):
|
||||
media_type = "application/x-orjson"
|
||||
|
||||
def render(self, content: Any) -> bytes:
|
||||
return orjson.dumps(content)
|
||||
|
||||
|
||||
class OverrideResponse(JSONResponse):
|
||||
media_type = "application/x-override"
|
||||
|
||||
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
router_a = APIRouter()
|
||||
router_a_a = APIRouter()
|
||||
router_a_b_override = APIRouter() # Overrides default class
|
||||
router_b_override = APIRouter() # Overrides default class
|
||||
router_b_a = APIRouter()
|
||||
router_b_a_c_override = APIRouter() # Overrides default class again
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def get_root():
|
||||
return {"msg": "Hello World"}
|
||||
|
||||
|
||||
@app.get("/override", response_class=PlainTextResponse)
|
||||
def get_path_override():
|
||||
return "Hello World"
|
||||
|
||||
|
||||
@router_a.get("/")
|
||||
def get_a():
|
||||
return {"msg": "Hello A"}
|
||||
|
||||
|
||||
@router_a.get("/override", response_class=PlainTextResponse)
|
||||
def get_a_path_override():
|
||||
return "Hello A"
|
||||
|
||||
|
||||
@router_a_a.get("/")
|
||||
def get_a_a():
|
||||
return {"msg": "Hello A A"}
|
||||
|
||||
|
||||
@router_a_a.get("/override", response_class=PlainTextResponse)
|
||||
def get_a_a_path_override():
|
||||
return "Hello A A"
|
||||
|
||||
|
||||
@router_a_b_override.get("/")
|
||||
def get_a_b():
|
||||
return "Hello A B"
|
||||
|
||||
|
||||
@router_a_b_override.get("/override", response_class=HTMLResponse)
|
||||
def get_a_b_path_override():
|
||||
return "Hello A B"
|
||||
|
||||
|
||||
@router_b_override.get("/")
|
||||
def get_b():
|
||||
return "Hello B"
|
||||
|
||||
|
||||
@router_b_override.get("/override", response_class=HTMLResponse)
|
||||
def get_b_path_override():
|
||||
return "Hello B"
|
||||
|
||||
|
||||
@router_b_a.get("/")
|
||||
def get_b_a():
|
||||
return "Hello B A"
|
||||
|
||||
|
||||
@router_b_a.get("/override", response_class=HTMLResponse)
|
||||
def get_b_a_path_override():
|
||||
return "Hello B A"
|
||||
|
||||
|
||||
@router_b_a_c_override.get("/")
|
||||
def get_b_a_c():
|
||||
return "Hello B A C"
|
||||
|
||||
|
||||
@router_b_a_c_override.get("/override", response_class=OverrideResponse)
|
||||
def get_b_a_c_path_override():
|
||||
return {"msg": "Hello B A C"}
|
||||
|
||||
|
||||
router_b_a.include_router(
|
||||
router_b_a_c_override, prefix="/c", default_response_class=HTMLResponse
|
||||
)
|
||||
router_b_override.include_router(router_b_a, prefix="/a")
|
||||
router_a.include_router(router_a_a, prefix="/a")
|
||||
router_a.include_router(
|
||||
router_a_b_override, prefix="/b", default_response_class=PlainTextResponse
|
||||
)
|
||||
app.include_router(router_a, prefix="/a")
|
||||
app.include_router(
|
||||
router_b_override, prefix="/b", default_response_class=PlainTextResponse
|
||||
)
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
orjson_type = "application/x-orjson"
|
||||
text_type = "text/plain; charset=utf-8"
|
||||
html_type = "text/html; charset=utf-8"
|
||||
override_type = "application/x-override"
|
||||
|
||||
|
||||
def test_app():
|
||||
with client:
|
||||
response = client.get("/")
|
||||
assert response.json() == {"msg": "Hello World"}
|
||||
assert response.headers["content-type"] == orjson_type
|
||||
|
||||
|
||||
def test_app_override():
|
||||
with client:
|
||||
response = client.get("/override")
|
||||
assert response.content == b"Hello World"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a():
|
||||
with client:
|
||||
response = client.get("/a")
|
||||
assert response.json() == {"msg": "Hello A"}
|
||||
assert response.headers["content-type"] == orjson_type
|
||||
|
||||
|
||||
def test_router_a_override():
|
||||
with client:
|
||||
response = client.get("/a/override")
|
||||
assert response.content == b"Hello A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_a():
|
||||
with client:
|
||||
response = client.get("/a/a")
|
||||
assert response.json() == {"msg": "Hello A A"}
|
||||
assert response.headers["content-type"] == orjson_type
|
||||
|
||||
|
||||
def test_router_a_a_override():
|
||||
with client:
|
||||
response = client.get("/a/a/override")
|
||||
assert response.content == b"Hello A A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_b():
|
||||
with client:
|
||||
response = client.get("/a/b")
|
||||
assert response.content == b"Hello A B"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_b_override():
|
||||
with client:
|
||||
response = client.get("/a/b/override")
|
||||
assert response.content == b"Hello A B"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b():
|
||||
with client:
|
||||
response = client.get("/b")
|
||||
assert response.content == b"Hello B"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_b_override():
|
||||
with client:
|
||||
response = client.get("/b/override")
|
||||
assert response.content == b"Hello B"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a():
|
||||
with client:
|
||||
response = client.get("/b/a")
|
||||
assert response.content == b"Hello B A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_b_a_override():
|
||||
with client:
|
||||
response = client.get("/b/a/override")
|
||||
assert response.content == b"Hello B A"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a_c():
|
||||
with client:
|
||||
response = client.get("/b/a/c")
|
||||
assert response.content == b"Hello B A C"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a_c_override():
|
||||
with client:
|
||||
response = client.get("/b/a/c/override")
|
||||
assert response.json() == {"msg": "Hello B A C"}
|
||||
assert response.headers["content-type"] == override_type
|
||||
206
tests/test_default_response_class_router.py
Normal file
206
tests/test_default_response_class_router.py
Normal file
@@ -0,0 +1,206 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
class OverrideResponse(JSONResponse):
|
||||
media_type = "application/x-override"
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
router_a = APIRouter()
|
||||
router_a_a = APIRouter()
|
||||
router_a_b_override = APIRouter() # Overrides default class
|
||||
router_b_override = APIRouter() # Overrides default class
|
||||
router_b_a = APIRouter()
|
||||
router_b_a_c_override = APIRouter() # Overrides default class again
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def get_root():
|
||||
return {"msg": "Hello World"}
|
||||
|
||||
|
||||
@app.get("/override", response_class=PlainTextResponse)
|
||||
def get_path_override():
|
||||
return "Hello World"
|
||||
|
||||
|
||||
@router_a.get("/")
|
||||
def get_a():
|
||||
return {"msg": "Hello A"}
|
||||
|
||||
|
||||
@router_a.get("/override", response_class=PlainTextResponse)
|
||||
def get_a_path_override():
|
||||
return "Hello A"
|
||||
|
||||
|
||||
@router_a_a.get("/")
|
||||
def get_a_a():
|
||||
return {"msg": "Hello A A"}
|
||||
|
||||
|
||||
@router_a_a.get("/override", response_class=PlainTextResponse)
|
||||
def get_a_a_path_override():
|
||||
return "Hello A A"
|
||||
|
||||
|
||||
@router_a_b_override.get("/")
|
||||
def get_a_b():
|
||||
return "Hello A B"
|
||||
|
||||
|
||||
@router_a_b_override.get("/override", response_class=HTMLResponse)
|
||||
def get_a_b_path_override():
|
||||
return "Hello A B"
|
||||
|
||||
|
||||
@router_b_override.get("/")
|
||||
def get_b():
|
||||
return "Hello B"
|
||||
|
||||
|
||||
@router_b_override.get("/override", response_class=HTMLResponse)
|
||||
def get_b_path_override():
|
||||
return "Hello B"
|
||||
|
||||
|
||||
@router_b_a.get("/")
|
||||
def get_b_a():
|
||||
return "Hello B A"
|
||||
|
||||
|
||||
@router_b_a.get("/override", response_class=HTMLResponse)
|
||||
def get_b_a_path_override():
|
||||
return "Hello B A"
|
||||
|
||||
|
||||
@router_b_a_c_override.get("/")
|
||||
def get_b_a_c():
|
||||
return "Hello B A C"
|
||||
|
||||
|
||||
@router_b_a_c_override.get("/override", response_class=OverrideResponse)
|
||||
def get_b_a_c_path_override():
|
||||
return {"msg": "Hello B A C"}
|
||||
|
||||
|
||||
router_b_a.include_router(
|
||||
router_b_a_c_override, prefix="/c", default_response_class=HTMLResponse
|
||||
)
|
||||
router_b_override.include_router(router_b_a, prefix="/a")
|
||||
router_a.include_router(router_a_a, prefix="/a")
|
||||
router_a.include_router(
|
||||
router_a_b_override, prefix="/b", default_response_class=PlainTextResponse
|
||||
)
|
||||
app.include_router(router_a, prefix="/a")
|
||||
app.include_router(
|
||||
router_b_override, prefix="/b", default_response_class=PlainTextResponse
|
||||
)
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
json_type = "application/json"
|
||||
text_type = "text/plain; charset=utf-8"
|
||||
html_type = "text/html; charset=utf-8"
|
||||
override_type = "application/x-override"
|
||||
|
||||
|
||||
def test_app():
|
||||
with client:
|
||||
response = client.get("/")
|
||||
assert response.json() == {"msg": "Hello World"}
|
||||
assert response.headers["content-type"] == json_type
|
||||
|
||||
|
||||
def test_app_override():
|
||||
with client:
|
||||
response = client.get("/override")
|
||||
assert response.content == b"Hello World"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a():
|
||||
with client:
|
||||
response = client.get("/a")
|
||||
assert response.json() == {"msg": "Hello A"}
|
||||
assert response.headers["content-type"] == json_type
|
||||
|
||||
|
||||
def test_router_a_override():
|
||||
with client:
|
||||
response = client.get("/a/override")
|
||||
assert response.content == b"Hello A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_a():
|
||||
with client:
|
||||
response = client.get("/a/a")
|
||||
assert response.json() == {"msg": "Hello A A"}
|
||||
assert response.headers["content-type"] == json_type
|
||||
|
||||
|
||||
def test_router_a_a_override():
|
||||
with client:
|
||||
response = client.get("/a/a/override")
|
||||
assert response.content == b"Hello A A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_b():
|
||||
with client:
|
||||
response = client.get("/a/b")
|
||||
assert response.content == b"Hello A B"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_b_override():
|
||||
with client:
|
||||
response = client.get("/a/b/override")
|
||||
assert response.content == b"Hello A B"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b():
|
||||
with client:
|
||||
response = client.get("/b")
|
||||
assert response.content == b"Hello B"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_b_override():
|
||||
with client:
|
||||
response = client.get("/b/override")
|
||||
assert response.content == b"Hello B"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a():
|
||||
with client:
|
||||
response = client.get("/b/a")
|
||||
assert response.content == b"Hello B A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_b_a_override():
|
||||
with client:
|
||||
response = client.get("/b/a/override")
|
||||
assert response.content == b"Hello B A"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a_c():
|
||||
with client:
|
||||
response = client.get("/b/a/c")
|
||||
assert response.content == b"Hello B A C"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a_c_override():
|
||||
with client:
|
||||
response = client.get("/b/a/c/override")
|
||||
assert response.json() == {"msg": "Hello B A C"}
|
||||
assert response.headers["content-type"] == override_type
|
||||
68
tests/test_dependency_cache.py
Normal file
68
tests/test_dependency_cache.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from fastapi import Depends, FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
counter_holder = {"counter": 0}
|
||||
|
||||
|
||||
async def dep_counter():
|
||||
counter_holder["counter"] += 1
|
||||
return counter_holder["counter"]
|
||||
|
||||
|
||||
async def super_dep(count: int = Depends(dep_counter)):
|
||||
return count
|
||||
|
||||
|
||||
@app.get("/counter/")
|
||||
async def get_counter(count: int = Depends(dep_counter)):
|
||||
return {"counter": count}
|
||||
|
||||
|
||||
@app.get("/sub-counter/")
|
||||
async def get_sub_counter(
|
||||
subcount: int = Depends(super_dep), count: int = Depends(dep_counter)
|
||||
):
|
||||
return {"counter": count, "subcounter": subcount}
|
||||
|
||||
|
||||
@app.get("/sub-counter-no-cache/")
|
||||
async def get_sub_counter_no_cache(
|
||||
subcount: int = Depends(super_dep),
|
||||
count: int = Depends(dep_counter, use_cache=False),
|
||||
):
|
||||
return {"counter": count, "subcounter": subcount}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_normal_counter():
|
||||
counter_holder["counter"] = 0
|
||||
response = client.get("/counter/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"counter": 1}
|
||||
response = client.get("/counter/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"counter": 2}
|
||||
|
||||
|
||||
def test_sub_counter():
|
||||
counter_holder["counter"] = 0
|
||||
response = client.get("/sub-counter/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"counter": 1, "subcounter": 1}
|
||||
response = client.get("/sub-counter/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"counter": 2, "subcounter": 2}
|
||||
|
||||
|
||||
def test_sub_counter_no_cache():
|
||||
counter_holder["counter"] = 0
|
||||
response = client.get("/sub-counter-no-cache/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"counter": 2, "subcounter": 1}
|
||||
response = client.get("/sub-counter-no-cache/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"counter": 4, "subcounter": 3}
|
||||
313
tests/test_dependency_overrides.py
Normal file
313
tests/test_dependency_overrides.py
Normal file
@@ -0,0 +1,313 @@
|
||||
import pytest
|
||||
from fastapi import APIRouter, Depends, FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def common_parameters(q: str, skip: int = 0, limit: int = 100):
|
||||
return {"q": q, "skip": skip, "limit": limit}
|
||||
|
||||
|
||||
@app.get("/main-depends/")
|
||||
async def main_depends(commons: dict = Depends(common_parameters)):
|
||||
return {"in": "main-depends", "params": commons}
|
||||
|
||||
|
||||
@app.get("/decorator-depends/", dependencies=[Depends(common_parameters)])
|
||||
async def decorator_depends():
|
||||
return {"in": "decorator-depends"}
|
||||
|
||||
|
||||
@router.get("/router-depends/")
|
||||
async def router_depends(commons: dict = Depends(common_parameters)):
|
||||
return {"in": "router-depends", "params": commons}
|
||||
|
||||
|
||||
@router.get("/router-decorator-depends/", dependencies=[Depends(common_parameters)])
|
||||
async def router_decorator_depends():
|
||||
return {"in": "router-decorator-depends"}
|
||||
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
async def overrider_dependency_simple(q: str = None):
|
||||
return {"q": q, "skip": 5, "limit": 10}
|
||||
|
||||
|
||||
async def overrider_sub_dependency(k: str):
|
||||
return {"k": k}
|
||||
|
||||
|
||||
async def overrider_dependency_with_sub(msg: dict = Depends(overrider_sub_dependency)):
|
||||
return msg
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url,status_code,expected",
|
||||
[
|
||||
(
|
||||
"/main-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/main-depends/?q=foo",
|
||||
200,
|
||||
{"in": "main-depends", "params": {"q": "foo", "skip": 0, "limit": 100}},
|
||||
),
|
||||
(
|
||||
"/main-depends/?q=foo&skip=100&limit=200",
|
||||
200,
|
||||
{"in": "main-depends", "params": {"q": "foo", "skip": 100, "limit": 200}},
|
||||
),
|
||||
(
|
||||
"/decorator-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
("/decorator-depends/?q=foo", 200, {"in": "decorator-depends"}),
|
||||
(
|
||||
"/decorator-depends/?q=foo&skip=100&limit=200",
|
||||
200,
|
||||
{"in": "decorator-depends"},
|
||||
),
|
||||
(
|
||||
"/router-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/router-depends/?q=foo",
|
||||
200,
|
||||
{"in": "router-depends", "params": {"q": "foo", "skip": 0, "limit": 100}},
|
||||
),
|
||||
(
|
||||
"/router-depends/?q=foo&skip=100&limit=200",
|
||||
200,
|
||||
{"in": "router-depends", "params": {"q": "foo", "skip": 100, "limit": 200}},
|
||||
),
|
||||
(
|
||||
"/router-decorator-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
("/router-decorator-depends/?q=foo", 200, {"in": "router-decorator-depends"}),
|
||||
(
|
||||
"/router-decorator-depends/?q=foo&skip=100&limit=200",
|
||||
200,
|
||||
{"in": "router-decorator-depends"},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_normal_app(url, status_code, expected):
|
||||
response = client.get(url)
|
||||
assert response.status_code == status_code
|
||||
assert response.json() == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url,status_code,expected",
|
||||
[
|
||||
(
|
||||
"/main-depends/",
|
||||
200,
|
||||
{"in": "main-depends", "params": {"q": None, "skip": 5, "limit": 10}},
|
||||
),
|
||||
(
|
||||
"/main-depends/?q=foo",
|
||||
200,
|
||||
{"in": "main-depends", "params": {"q": "foo", "skip": 5, "limit": 10}},
|
||||
),
|
||||
(
|
||||
"/main-depends/?q=foo&skip=100&limit=200",
|
||||
200,
|
||||
{"in": "main-depends", "params": {"q": "foo", "skip": 5, "limit": 10}},
|
||||
),
|
||||
("/decorator-depends/", 200, {"in": "decorator-depends"}),
|
||||
(
|
||||
"/router-depends/",
|
||||
200,
|
||||
{"in": "router-depends", "params": {"q": None, "skip": 5, "limit": 10}},
|
||||
),
|
||||
(
|
||||
"/router-depends/?q=foo",
|
||||
200,
|
||||
{"in": "router-depends", "params": {"q": "foo", "skip": 5, "limit": 10}},
|
||||
),
|
||||
(
|
||||
"/router-depends/?q=foo&skip=100&limit=200",
|
||||
200,
|
||||
{"in": "router-depends", "params": {"q": "foo", "skip": 5, "limit": 10}},
|
||||
),
|
||||
("/router-decorator-depends/", 200, {"in": "router-decorator-depends"}),
|
||||
],
|
||||
)
|
||||
def test_override_simple(url, status_code, expected):
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_simple
|
||||
response = client.get(url)
|
||||
assert response.status_code == status_code
|
||||
assert response.json() == expected
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url,status_code,expected",
|
||||
[
|
||||
(
|
||||
"/main-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/main-depends/?q=foo",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
("/main-depends/?k=bar", 200, {"in": "main-depends", "params": {"k": "bar"}}),
|
||||
(
|
||||
"/decorator-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/decorator-depends/?q=foo",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
("/decorator-depends/?k=bar", 200, {"in": "decorator-depends"}),
|
||||
(
|
||||
"/router-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/router-depends/?q=foo",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/router-depends/?k=bar",
|
||||
200,
|
||||
{"in": "router-depends", "params": {"k": "bar"}},
|
||||
),
|
||||
(
|
||||
"/router-decorator-depends/",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"/router-decorator-depends/?q=foo",
|
||||
422,
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "k"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
("/router-decorator-depends/?k=bar", 200, {"in": "router-decorator-depends"}),
|
||||
],
|
||||
)
|
||||
def test_override_with_sub(url, status_code, expected):
|
||||
app.dependency_overrides[common_parameters] = overrider_dependency_with_sub
|
||||
response = client.get(url)
|
||||
assert response.status_code == status_code
|
||||
assert response.json() == expected
|
||||
app.dependency_overrides = {}
|
||||
23
tests/test_duplicate_models_openapi.py
Normal file
23
tests/test_duplicate_models_openapi.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def test_get_openapi():
|
||||
app = FastAPI()
|
||||
|
||||
class Model(BaseModel):
|
||||
pass
|
||||
|
||||
class Model2(BaseModel):
|
||||
a: Model
|
||||
|
||||
class Model3(BaseModel):
|
||||
c: Model
|
||||
d: Model2
|
||||
|
||||
@app.get("/", response_model=Model3)
|
||||
def f():
|
||||
pass # pragma: no cover
|
||||
|
||||
openapi = app.openapi()
|
||||
assert isinstance(openapi, dict)
|
||||
33
tests/test_empty_router.py
Normal file
33
tests/test_empty_router.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import pytest
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_empty():
|
||||
return ["OK"]
|
||||
|
||||
|
||||
app.include_router(router, prefix="/prefix")
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_use_empty():
|
||||
with client:
|
||||
response = client.get("/prefix")
|
||||
assert response.json() == ["OK"]
|
||||
|
||||
response = client.get("/prefix/")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_include_empty():
|
||||
# if both include and router.path are empty - it should raise exception
|
||||
with pytest.raises(Exception):
|
||||
app.include_router(router)
|
||||
136
tests/test_infer_param_optionality.py
Normal file
136
tests/test_infer_param_optionality.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
user_router = APIRouter()
|
||||
item_router = APIRouter()
|
||||
|
||||
|
||||
@user_router.get("/")
|
||||
def get_users():
|
||||
return [{"user_id": "u1"}, {"user_id": "u2"}]
|
||||
|
||||
|
||||
@user_router.get("/{user_id}")
|
||||
def get_user(user_id: str):
|
||||
return {"user_id": user_id}
|
||||
|
||||
|
||||
@item_router.get("/")
|
||||
def get_items(user_id: str = None):
|
||||
if user_id is None:
|
||||
return [{"item_id": "i1", "user_id": "u1"}, {"item_id": "i2", "user_id": "u2"}]
|
||||
else:
|
||||
return [{"item_id": "i2", "user_id": user_id}]
|
||||
|
||||
|
||||
@item_router.get("/{item_id}")
|
||||
def get_item(item_id: str, user_id: str = None):
|
||||
if user_id is None:
|
||||
return {"item_id": item_id}
|
||||
else:
|
||||
return {"item_id": item_id, "user_id": user_id}
|
||||
|
||||
|
||||
app.include_router(user_router, prefix="/users")
|
||||
app.include_router(item_router, prefix="/items")
|
||||
|
||||
app.include_router(item_router, prefix="/users/{user_id}/items")
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_get_users():
|
||||
"""Check that /users returns expected data"""
|
||||
response = client.get("/users")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"user_id": "u1"}, {"user_id": "u2"}]
|
||||
|
||||
|
||||
def test_get_user():
|
||||
"""Check that /users/{user_id} returns expected data"""
|
||||
response = client.get("/users/abc123")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"user_id": "abc123"}
|
||||
|
||||
|
||||
def test_get_items_1():
|
||||
"""Check that /items returns expected data"""
|
||||
response = client.get("/items")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [
|
||||
{"item_id": "i1", "user_id": "u1"},
|
||||
{"item_id": "i2", "user_id": "u2"},
|
||||
]
|
||||
|
||||
|
||||
def test_get_items_2():
|
||||
"""Check that /items returns expected data with user_id specified"""
|
||||
response = client.get("/items?user_id=abc123")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"item_id": "i2", "user_id": "abc123"}]
|
||||
|
||||
|
||||
def test_get_item_1():
|
||||
"""Check that /items/{item_id} returns expected data"""
|
||||
response = client.get("/items/item01")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "item01"}
|
||||
|
||||
|
||||
def test_get_item_2():
|
||||
"""Check that /items/{item_id} returns expected data with user_id specified"""
|
||||
response = client.get("/items/item01?user_id=abc123")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "item01", "user_id": "abc123"}
|
||||
|
||||
|
||||
def test_get_users_items():
|
||||
"""Check that /users/{user_id}/items returns expected data"""
|
||||
response = client.get("/users/abc123/items")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"item_id": "i2", "user_id": "abc123"}]
|
||||
|
||||
|
||||
def test_get_users_item():
|
||||
"""Check that /users/{user_id}/items returns expected data"""
|
||||
response = client.get("/users/abc123/items/item01")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "item01", "user_id": "abc123"}
|
||||
|
||||
|
||||
def test_schema_1():
|
||||
"""Check that the user_id is a required path parameter under /users"""
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
r = response.json()
|
||||
|
||||
d = {
|
||||
"required": True,
|
||||
"schema": {"title": "User_Id", "type": "string"},
|
||||
"name": "user_id",
|
||||
"in": "path",
|
||||
}
|
||||
|
||||
assert d in r["paths"]["/users/{user_id}"]["get"]["parameters"]
|
||||
assert d in r["paths"]["/users/{user_id}/items/"]["get"]["parameters"]
|
||||
|
||||
|
||||
def test_schema_2():
|
||||
"""Check that the user_id is an optional query parameter under /items"""
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
r = response.json()
|
||||
|
||||
d = {
|
||||
"required": False,
|
||||
"schema": {"title": "User_Id", "type": "string"},
|
||||
"name": "user_id",
|
||||
"in": "query",
|
||||
}
|
||||
|
||||
assert d in r["paths"]["/items/{item_id}"]["get"]["parameters"]
|
||||
assert d in r["paths"]["/items/"]["get"]["parameters"]
|
||||
77
tests/test_invalid_path_param.py
Normal file
77
tests/test_invalid_path_param.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def test_invalid_sequence():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
class Item(BaseModel):
|
||||
title: str
|
||||
|
||||
@app.get("/items/{id}")
|
||||
def read_items(id: List[Item]):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_tuple():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
class Item(BaseModel):
|
||||
title: str
|
||||
|
||||
@app.get("/items/{id}")
|
||||
def read_items(id: Tuple[Item, Item]):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_dict():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
class Item(BaseModel):
|
||||
title: str
|
||||
|
||||
@app.get("/items/{id}")
|
||||
def read_items(id: Dict[str, Item]):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_simple_list():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/items/{id}")
|
||||
def read_items(id: list):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_simple_tuple():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/items/{id}")
|
||||
def read_items(id: tuple):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_simple_set():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/items/{id}")
|
||||
def read_items(id: set):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_simple_dict():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/items/{id}")
|
||||
def read_items(id: dict):
|
||||
pass # pragma: no cover
|
||||
53
tests/test_invalid_sequence_param.py
Normal file
53
tests/test_invalid_sequence_param.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def test_invalid_sequence():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
class Item(BaseModel):
|
||||
title: str
|
||||
|
||||
@app.get("/items/")
|
||||
def read_items(q: List[Item] = Query(None)):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_tuple():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
class Item(BaseModel):
|
||||
title: str
|
||||
|
||||
@app.get("/items/")
|
||||
def read_items(q: Tuple[Item, Item] = Query(None)):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_dict():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
class Item(BaseModel):
|
||||
title: str
|
||||
|
||||
@app.get("/items/")
|
||||
def read_items(q: Dict[str, Item] = Query(None)):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_simple_dict():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
class Item(BaseModel):
|
||||
title: str
|
||||
|
||||
@app.get("/items/")
|
||||
def read_items(q: dict = Query(None)):
|
||||
pass # pragma: no cover
|
||||
@@ -54,3 +54,14 @@ def test_strings_in_custom_redoc():
|
||||
body_content = html.body.decode()
|
||||
assert redoc_js_url in body_content
|
||||
assert redoc_favicon_url in body_content
|
||||
|
||||
|
||||
def test_google_fonts_in_generated_redoc():
|
||||
body_with_google_fonts = get_redoc_html(
|
||||
openapi_url="/docs", title="title"
|
||||
).body.decode()
|
||||
assert "fonts.googleapis.com" in body_with_google_fonts
|
||||
body_without_google_fonts = get_redoc_html(
|
||||
openapi_url="/docs", title="title", with_google_fonts=False
|
||||
).body.decode()
|
||||
assert "fonts.googleapis.com" not in body_without_google_fonts
|
||||
|
||||
0
tests/test_modules_same_name_body/__init__.py
Normal file
0
tests/test_modules_same_name_body/__init__.py
Normal file
0
tests/test_modules_same_name_body/app/__init__.py
Normal file
0
tests/test_modules_same_name_body/app/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user