Compare commits

...

32 Commits

Author SHA1 Message Date
Sebastián Ramírez
8e3a7699a3 🔖 Release 0.19.0 2019-04-26 13:48:30 +04:00
Sebastián Ramírez
8998ccaffb 📝 Update release notes 2019-04-26 13:45:38 +04:00
Sebastián Ramírez
2b7f201a44 📝 Add docs about returning a response directly and encoder (#184) 2019-04-26 13:40:23 +04:00
Sebastián Ramírez
192ebba2a2 ♻️ Rename parameter content_type to response_class (#183) 2019-04-26 13:11:16 +04:00
Sebastián Ramírez
8880c4cb03 🔖 Release 0.18.0 2019-04-22 21:08:43 +04:00
Sebastián Ramírez
6324be684f 📝 Update release notes 2019-04-21 22:31:43 +04:00
Sebastián Ramírez
c705685394 Add docs for HTTP Basic Auth and tests (#177) 2019-04-21 22:30:58 +04:00
Sebastián Ramírez
945f401d8e 📝 Update release notes 2019-04-21 21:46:00 +04:00
Sebastián Ramírez
f216d340ec Add automatic header handling for HTTP Basic Auth (#175)
*  Add automatic header handling for HTTP Basic Auth

* 🎨 Remove obsolete comment
2019-04-21 21:44:25 +04:00
Sebastián Ramírez
a4558e7053 📝 Update release notes 2019-04-21 20:21:53 +04:00
Sebastián Ramírez
298f8478e2 🔒 Fix development dependencies security (#174) 2019-04-21 20:20:25 +04:00
Sebastián Ramírez
b86d163470 📝 Rename additional response OpenAPI declarations 2019-04-21 20:13:26 +04:00
Sebastián Ramírez
9e2d37b89c 📝 Update release notes 2019-04-21 19:57:07 +04:00
Sebastián Ramírez
97adadd9e1 📝 Add docs for middleware (#173) 2019-04-21 19:56:20 +04:00
Sebastián Ramírez
26e3dffb37 🚀 Deploy when tagged using Python 3.6 2019-04-20 22:16:07 +04:00
Sebastián Ramírez
aa7b4bd101 🔖 0.17.0 2019-04-20 22:12:55 +04:00
Sebastián Ramírez
ffc4c716c0 🚀 Make Flit publish from CI (#170) 2019-04-20 22:09:35 +04:00
Sebastián Ramírez
ef7b6e8eaf 📝 Update Release Notes 2019-04-20 21:15:03 +04:00
Sebastián Ramírez
596243f4a5 Add docs about CORS (#169) 2019-04-20 21:13:01 +04:00
Sebastián Ramírez
766bf1c5aa 📝 Update release notes 2019-04-20 20:31:44 +04:00
Sebastián Ramírez
9e748dbca4 By default, encode by alias (#168) 2019-04-20 20:29:54 +04:00
Sebastián Ramírez
cefe6cf92c 🔖 Release version 0.16.0 2019-04-16 23:28:13 +04:00
Sebastián Ramírez
be3953499f 📝 Update release notes 2019-04-16 23:27:25 +04:00
Sebastián Ramírez
546d233dec ♻️ Update Pydantic usage, types, values, minor structure changes (#164) 2019-04-16 23:26:09 +04:00
Sebastián Ramírez
61dd36a945 Upgrade docstring Markdown parsing (#163)
*  Upgrade docstring Markdown parsing

* 📝 Update release notes
2019-04-16 22:49:18 +04:00
Sebastián Ramírez
27f9d55c3e 📝 Update release notes 2019-04-16 22:43:59 +04:00
euri10
906cc60f65 ⬆️ Upgrade Pydantic to 0.23 (#160)
* Add websocket to APIRouter

* Upgrade pydantic to v0.23.0

* Forgot pyproject.toml

* ⬆️ Upgrade some Pipfile.lock dependencies
2019-04-16 22:42:00 +04:00
Sebastián Ramírez
69afaf256f 📝 Update release notes 2019-04-16 22:21:32 +04:00
Daniel Michaels
4ab349a2a8 ✏️ fixed small typo /tutorial/extra-models.md (#159) 2019-04-16 22:20:03 +04:00
Sebastián Ramírez
9c258107b4 📝 Update release notes 2019-04-16 22:18:42 +04:00
hayata-yamamoto
29a4f90bcd 📝 fix URL examples in Tutorial: Query Parameters (#157)
* modify tutorial

* modify item_id
2019-04-16 22:16:16 +04:00
Sebastián Ramírez
361fd00777 📝 Add note about Swagger UI and multi-part uploads 2019-04-14 22:24:31 +04:00
45 changed files with 756 additions and 229 deletions

View File

@@ -20,6 +20,7 @@ after_script:
deploy:
provider: script
script: bash scripts/trigger-docker.sh
script: bash scripts/deploy.sh
on:
branch: master
tags: true
python: "3.6"

View File

@@ -26,7 +26,7 @@ uvicorn = "*"
[packages]
starlette = "==0.11.1"
pydantic = "==0.21.0"
pydantic = "==0.23.0"
databases = {extras = ["sqlite"],version = "*"}
[requires]

135
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "24b3b7b88d3cbe671ddbe296e64c15f8558f0e5d5df977200119872a363aac13"
"sha256": "02367d250c6327eac80dfcd8e5ccfa49bcdca0332bc757d527c3db27643baa0d"
},
"pipfile-spec": 6,
"requires": {
@@ -41,10 +41,10 @@
"sqlite"
],
"hashes": [
"sha256:da819f7e00dc7d8c2f0585ec53aa49bae63b366f800506097db2e87972a4d44f"
"sha256:d365cff2035c5177ef5fd8c5abf6671da01189521da64848a01251c870daf48f"
],
"index": "pypi",
"version": "==0.2.1"
"version": "==0.2.2"
},
"dataclasses": {
"hashes": [
@@ -56,23 +56,38 @@
},
"immutables": {
"hashes": [
"sha256:f958ba15745e30d3a38e3c9fcead8496037135bb21c78c0f925c104abba3a6fa"
"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:93fa585402e7c8c01623ea8af6ca23363e8b4c6a020b7a2de9e99fa29d642d50",
"sha256:eb441dd50779347a450494c437db3ecbb13c1f3854497df879662782af516c5c"
"sha256:1205cd1213e8acee40a9ad7160b24de74484fd79ec3f09150b255896a3f506ab",
"sha256:58b71804e9a6b4e1ccf8b3dbbca8c0f9cf4b494e5bea219a96e2e2ecb5af688e"
],
"index": "pypi",
"version": "==0.21.0"
"version": "==0.23.0"
},
"sqlalchemy": {
"hashes": [
"sha256:d5432832f91d200c3d8b473a266d59442d825f9ea744c467e68c5d9a9479fbce"
"sha256:91c54ca8345008fceaec987e10924bf07dcab36c442925357e5a467b36a38319"
],
"version": "==1.3.2"
"version": "==1.3.3"
},
"starlette": {
"hashes": [
@@ -205,10 +220,10 @@
},
"defusedxml": {
"hashes": [
"sha256:24d7f2f94f7f3cb6061acb215685e5125fbcdc40a857eff9de22518820b0a4f4",
"sha256:702a91ade2968a82beb0db1e0766a6a273f33d4616a6ce8cde475d8e09853b20"
"sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93",
"sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"
],
"version": "==0.5.0"
"version": "==0.6.0"
},
"dnspython": {
"hashes": [
@@ -266,6 +281,7 @@
"hashes": [
"sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc"
],
"markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'pypy'",
"version": "==0.0.13"
},
"idna": {
@@ -441,11 +457,11 @@
},
"mkdocs-material": {
"hashes": [
"sha256:8f0a5217c24bd8635c0bda2a0ee4f91766448e9e3dd6429f1111dd992327345e",
"sha256:c2c6ef6b3e3ab4744a45d03a276e1eb106c91abf610d180d148613fd1a525c7c"
"sha256:8a572f4b3358b9c0e11af8ae319ba4f3747ebb61e2393734d875133b0d2f7891",
"sha256:91210776db541283dd4b7beb5339c190aa69de78ad661aa116a8aa97dd73c803"
],
"index": "pypi",
"version": "==4.1.1"
"version": "==4.1.2"
},
"more-itertools": {
"hashes": [
@@ -457,20 +473,20 @@
},
"mypy": {
"hashes": [
"sha256:03261a04ace27250cf14f1301969e2cc36ad0343dd437e60007ce42f06ddbaff",
"sha256:6a7923e90dd8f8b8e762327e3a4dd814f0bc5581a627010f4e2ec72d906ada0f",
"sha256:6a7c2b16ff7dee1cd4a913641d6a8da0cd386be812524f41427ea25f8fe337a6",
"sha256:7480db0bc2bb473547c8d519ea549de9f9654170e6f5b34310094ebe5ee1c9dc",
"sha256:863774c896f2cdc62a0e2252e9ba7aaeb7da04c0296f47c82b125dce3437c580",
"sha256:9a990cf039891a83ee90f130256cc06d09c0793242ea38d0fe33fdc449507123",
"sha256:b03573d0cd8c051aa9ef7f47d564cf44bbc5e91e89a7a078b3ca904b3da8855a",
"sha256:b10b16d9aa7a01266f14260344fb25849ef0d508c512a916043f77987489aeff",
"sha256:b1eab82221c3cc94bf22152e701b3efc9d64f60fac4cab20969a0427e5a78261",
"sha256:e663d4424531dc99fb85c947df8a4a107442f53f20a4e0bcefaa1d21c87e1563",
"sha256:ffac30f3fa2c9e10118cbb0faa0b7da7edb6e3c24a4048a15446a1f3409884e3"
"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.700"
"version": "==0.701"
},
"mypy-extensions": {
"hashes": [
@@ -508,7 +524,8 @@
},
"parso": {
"hashes": [
"sha256:17cc2d7a945eb42c3569d4564cdf49bde221bc2b552af3eca9c1aad517dcdd33"
"sha256:17cc2d7a945eb42c3569d4564cdf49bde221bc2b552af3eca9c1aad517dcdd33",
"sha256:2e9574cb12e7112a87253e14e2c380ce312060269d04bd018478a3c92ea9a376"
],
"version": "==0.4.0"
},
@@ -599,11 +616,11 @@
},
"pytest": {
"hashes": [
"sha256:13c5e9fb5ec5179995e9357111ab089af350d788cbc944c628f3cde72285809b",
"sha256:f21d2f1fb8200830dcbb5d8ec466a9c9120e20d8b53c7585d180125cce1d297a"
"sha256:3773f4c235918987d51daf1db66d51c99fac654c81d6f2f709a046ab446d5e5d",
"sha256:b7802283b70ca24d7119b32915efa7c409982f59913c1a6c0640aacf118b95f5"
],
"index": "pypi",
"version": "==4.4.0"
"version": "==4.4.1"
},
"pytest-cov": {
"hashes": [
@@ -710,9 +727,9 @@
},
"sqlalchemy": {
"hashes": [
"sha256:d5432832f91d200c3d8b473a266d59442d825f9ea744c467e68c5d9a9479fbce"
"sha256:91c54ca8345008fceaec987e10924bf07dcab36c442925357e5a467b36a38319"
],
"version": "==1.3.2"
"version": "==1.3.3"
},
"terminado": {
"hashes": [
@@ -756,27 +773,28 @@
},
"typed-ast": {
"hashes": [
"sha256:035a54ede6ce1380599b2ce57844c6554666522e376bd111eb940fbc7c3dad23",
"sha256:037c35f2741ce3a9ac0d55abfcd119133cbd821fffa4461397718287092d9d15",
"sha256:049feae7e9f180b64efacbdc36b3af64a00393a47be22fa9cb6794e68d4e73d3",
"sha256:19228f7940beafc1ba21a6e8e070e0b0bfd1457902a3a81709762b8b9039b88d",
"sha256:2ea681e91e3550a30c2265d2916f40a5f5d89b59469a20f3bad7d07adee0f7a6",
"sha256:3a6b0a78af298d82323660df5497bcea0f0a4a25a0b003afd0ce5af049bd1f60",
"sha256:5385da8f3b801014504df0852bf83524599df890387a3c2b17b7caa3d78b1773",
"sha256:606d8afa07eef77280c2bf84335e24390055b478392e1975f96286d99d0cb424",
"sha256:69245b5b23bbf7fb242c9f8f08493e9ecd7711f063259aefffaeb90595d62287",
"sha256:6f6d839ab09830d59b7fa8fb6917023d8cb5498ee1f1dbd82d37db78eb76bc99",
"sha256:730888475f5ac0e37c1de4bd05eeb799fdb742697867f524dc8a4cd74bcecc23",
"sha256:9819b5162ffc121b9e334923c685b0d0826154e41dfe70b2ede2ce29034c71d8",
"sha256:9e60ef9426efab601dd9aa120e4ff560f4461cf8442e9c0a2b92548d52800699",
"sha256:af5fbdde0690c7da68e841d7fc2632345d570768ea7406a9434446d7b33b0ee1",
"sha256:b64efdbdf3bbb1377562c179f167f3bf301251411eb5ac77dec6b7d32bcda463",
"sha256:bac5f444c118aeb456fac1b0b5d14c6a71ea2a42069b09c176f75e9bd4c186f6",
"sha256:bda9068aafb73859491e13b99b682bd299c1b5fd50644d697533775828a28ee0",
"sha256:d659517ca116e6750101a1326107d3479028c5191f0ecee3c7203c50f5b915b0",
"sha256:eddd3fb1f3e0f82e5915a899285a39ee34ce18fd25d89582bc89fc9fb16cd2c6"
"sha256:04894d268ba6eab7e093d43107869ad49e7b5ef40d1a94243ea49b352061b200",
"sha256:16616ece19daddc586e499a3d2f560302c11f122b9c692bc216e821ae32aa0d0",
"sha256:252fdae740964b2d3cdfb3f84dcb4d6247a48a6abe2579e8029ab3be3cdc026c",
"sha256:2af80a373af123d0b9f44941a46df67ef0ff7a60f95872412a145f4500a7fc99",
"sha256:2c88d0a913229a06282b285f42a31e063c3bf9071ff65c5ea4c12acb6977c6a7",
"sha256:2ea99c029ebd4b5a308d915cc7fb95b8e1201d60b065450d5d26deb65d3f2bc1",
"sha256:3d2e3ab175fc097d2a51c7a0d3fda442f35ebcc93bb1d7bd9b95ad893e44c04d",
"sha256:4766dd695548a15ee766927bf883fb90c6ac8321be5a60c141f18628fb7f8da8",
"sha256:56b6978798502ef66625a2e0f80cf923da64e328da8bbe16c1ff928c70c873de",
"sha256:5cddb6f8bce14325b2863f9d5ac5c51e07b71b462361fd815d1d7706d3a9d682",
"sha256:644ee788222d81555af543b70a1098f2025db38eaa99226f3a75a6854924d4db",
"sha256:64cf762049fc4775efe6b27161467e76d0ba145862802a65eefc8879086fc6f8",
"sha256:68c362848d9fb71d3c3e5f43c09974a0ae319144634e7a47db62f0f2a54a7fa7",
"sha256:6c1f3c6f6635e611d58e467bf4371883568f0de9ccc4606f17048142dec14a1f",
"sha256:b213d4a02eec4ddf622f4d2fbc539f062af3788d1f332f028a2e19c42da53f15",
"sha256:bb27d4e7805a7de0e35bd0cb1411bc85f807968b2b0539597a49a23b00a622ae",
"sha256:c9d414512eaa417aadae7758bc118868cd2396b0e6138c1dd4fda96679c079d3",
"sha256:f0937165d1e25477b01081c4763d2d9cdc3b18af69cb259dd4f640c9b900fe5e",
"sha256:fb96a6e2c11059ecf84e6741a319f93f683e440e341d4489c9b161eca251cf2a",
"sha256:fc71d2d6ae56a091a8d94f33ec9d0f2001d1cb1db423d8b4355debfe9ce689b7"
],
"version": "==1.3.1"
"version": "==1.3.4"
},
"ujson": {
"hashes": [
@@ -787,17 +805,17 @@
},
"urllib3": {
"hashes": [
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
"sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0",
"sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3"
],
"version": "==1.24.1"
"version": "==1.24.2"
},
"uvicorn": {
"hashes": [
"sha256:d96fb442d9ce9c1dba67360035161d392970b8e6b0ed797d2cefed24abfd78bc"
"sha256:181d47abddedd0f6e23eaeed97976bdce9ea1dbff0ec12385309cf4835783f6a"
],
"index": "pypi",
"version": "==0.7.0b2"
"version": "==0.7.0"
},
"uvloop": {
"hashes": [
@@ -812,6 +830,7 @@
"sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f",
"sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe"
],
"markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'pypy'",
"version": "==0.12.2"
},
"wcwidth": {

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -1,5 +1,45 @@
## Next release
## 0.19.0
* Rename *path operation decorator* parameter `content_type` to `response_class`. PR <a href="https://github.com/tiangolo/fastapi/pull/183" target="_blank">#183</a>.
* Add docs:
* How to use the `jsonable_encoder` in <a href="https://fastapi.tiangolo.com/tutorial/tutorial/encoder/" target="_blank">JSON compatible encoder</a>.
* How to <a href="https://fastapi.tiangolo.com/tutorial/tutorial/response-directly/" target="_blank">Return a Response directly</a>.
* Update how to use a <a href="https://fastapi.tiangolo.com/tutorial/tutorial/custom-response/" target="_blank">Custom Response Class</a>.
* PR <a href="https://github.com/tiangolo/fastapi/pull/184" target="_blank">#184</a>.
## 0.18.0
* Add docs for <a href="https://fastapi.tiangolo.com/tutorial/custom-response/" target="_blank">HTTP Basic Auth</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/177" target="_blank">#177</a>.
* Upgrade HTTP Basic Auth handling with automatic headers (automatic browser login prompt). PR <a href="https://github.com/tiangolo/fastapi/pull/175" target="_blank">#175</a>.
* Update dependencies for security. PR <a href="https://github.com/tiangolo/fastapi/pull/174" target="_blank">#174</a>.
* Add docs for <a href="https://fastapi.tiangolo.com/tutorial/middleware/" target="_blank">Middleware</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/173" target="_blank">#173</a>.
## 0.17.0
* Make Flit publish from CI. PR <a href="https://github.com/tiangolo/fastapi/pull/170" target="_blank">#170</a>.
* Add documentation about handling <a href="https://fastapi.tiangolo.com/tutorial/cors/" target="_blank">CORS (Cross-Origin Resource Sharing)</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/169" target="_blank">#169</a>.
* By default, encode by alias. This allows using Pydantic `alias` parameters working by default. PR <a href="https://github.com/tiangolo/fastapi/pull/168" target="_blank">#168</a>.
## 0.16.0
* Upgrade *path operation* `doctsring` parsing to support proper Markdown descriptions. New documentation at <a href="https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#description-from-docstring" target="_blank">Path Operation Configuration</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/163" target="_blank">#163</a>.
* Refactor internal usage of Pydantic to use correct data types. PR <a href="https://github.com/tiangolo/fastapi/pull/164" target="_blank">#164</a>.
* Upgrade Pydantic to version `0.23`. PR <a href="https://github.com/tiangolo/fastapi/pull/160" target="_blank">#160</a> by <a href="https://github.com/euri10" target="_blank">@euri10</a>.
* Fix typo in Tutorial about Extra Models. PR <a href="https://github.com/tiangolo/fastapi/pull/159" target="_blank">#159</a> by <a href="https://github.com/danielmichaels" target="_blank">@danielmichaels</a>.
* Fix <a href="https://fastapi.tiangolo.com/tutorial/query-params/" target="_blank">Query Parameters</a> URL examples in docs. PR <a href="https://github.com/tiangolo/fastapi/pull/157" target="_blank">#157</a> by <a href="https://github.com/hayata-yamamoto" target="_blank">@hayata-yamamoto</a>.
## 0.15.0
* Add support for multiple file uploads (as a single form field). New docs at: <a href="https://fastapi.tiangolo.com/tutorial/request-files/#multiple-file-uploads" target="_blank">Multiple file uploads</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/158" target="_blank">#158</a>.

View File

@@ -0,0 +1,19 @@
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
app = FastAPI()
origins = [
"http://localhost.tiangolo.com",
"https://localhost.tiangolo.com",
"http:localhost",
"http:localhost:8080",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

View File

@@ -4,6 +4,6 @@ from starlette.responses import UJSONResponse
app = FastAPI()
@app.get("/items/", content_type=UJSONResponse)
@app.get("/items/", response_class=UJSONResponse)
async def read_items():
return [{"item_id": "Foo"}]

View File

@@ -4,7 +4,7 @@ from starlette.responses import HTMLResponse
app = FastAPI()
@app.get("/items/", content_type=HTMLResponse)
@app.get("/items/", response_class=HTMLResponse)
async def read_items():
return """
<html>

View File

@@ -18,6 +18,6 @@ def generate_html_response():
return HTMLResponse(content=html_content, status_code=200)
@app.get("/items/", content_type=HTMLResponse)
@app.get("/items/", response_class=HTMLResponse)
async def read_items():
return generate_html_response()

View File

@@ -0,0 +1,22 @@
from datetime import datetime
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
fake_db = {}
class Item(BaseModel):
title: str
timestamp: datetime
description: str = None
app = FastAPI()
@app.put("/items/{id}")
def update_item(id: str, item: Item):
json_compatible_item_data = jsonable_encoder(item)
fake_db[id] = json_compatible_item_data

View File

@@ -0,0 +1,15 @@
import time
from fastapi import FastAPI
from starlette.requests import Request
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response

View File

@@ -18,11 +18,11 @@ class Item(BaseModel):
async def create_item(*, item: Item):
"""
Create an item with all the information:
* name: each item must have a name
* description: a long description
* price: required
* tax: if the item doesn't have tax, you can omit this
* tags: a set of unique tag strings for this item
- **name**: each item must have a name
- **description**: a long description
- **price**: required
- **tax**: if the item doesn't have tax, you can omit this
- **tags**: a set of unique tag strings for this item
"""
return item

View File

@@ -23,11 +23,11 @@ class Item(BaseModel):
async def create_item(*, item: Item):
"""
Create an item with all the information:
* name: each item must have a name
* description: a long description
* price: required
* tax: if the item doesn't have tax, you can omit this
* tags: a set of unique tag strings for this item
- **name**: each item must have a name
- **description**: a long description
- **price**: required
- **tax**: if the item doesn't have tax, you can omit this
- **tags**: a set of unique tag strings for this item
"""
return item

View File

@@ -0,0 +1,21 @@
from datetime import datetime
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from starlette.responses import JSONResponse
class Item(BaseModel):
title: str
timestamp: datetime
description: str = None
app = FastAPI()
@app.put("/items/{id}")
def update_item(id: str, item: Item):
json_compatible_item_data = jsonable_encoder(item)
return JSONResponse(content=json_compatible_item_data)

View File

@@ -0,0 +1,20 @@
from fastapi import FastAPI
from starlette.responses import Response
app = FastAPI()
@app.get("/legacy/")
def get_legacy_data():
data = """
<?xml version="1.0"?>
<shampoo>
<Header>
Apply shampoo here.
<Header>
<Body>
You'll have to use soap here.
</Body>
</shampoo>
"""
return Response(content=data, media_type="application/xml")

View File

@@ -0,0 +1,11 @@
from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
security = HTTPBasic()
@app.get("/users/me")
def read_current_user(credentials: HTTPBasicCredentials = Depends(security)):
return {"username": credentials.username, "password": credentials.password}

View File

@@ -0,0 +1,22 @@
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from starlette.status import HTTP_401_UNAUTHORIZED
app = FastAPI()
security = HTTPBasic()
def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
if credentials.username != "foo" or credentials.password != "password":
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/users/me")
def read_current_user(username: str = Depends(get_current_username)):
return {"username": username}

55
docs/tutorial/cors.md Normal file
View File

@@ -0,0 +1,55 @@
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank">CORS or "Cross-Origin Resource Sharing"</a> refers to the situations when a frontend running in a browser has JavaScript code that communicates with a backend, and the backend is in a different "origin" than the frontend.
## Origin
An origin is the combination of protocol (`http`, `https`), domain (`myapp.com`, `localhost`, `localhost.tiangolo.com`), and port (`80`, `443`, `8080`).
So, all these are different origins:
* `http://localhost`
* `https://localhost`
* `http://localhost:8080`
Even if they are all in `localhost`, they use different protocols or ports, so, they are different "origins".
## Steps
So, let's say you have a frontend running in your browser at `http://localhost:8080`, and its JavaScript is trying to communicate with a backend running at `http://localhost` (because we don't specify a port, the browser will assume the default port `80`).
Then, the browser will send an HTTP `OPTIONS` request to the backend, and if the backend sends the appropriate headers authorizing the communication from this different origin (`http://localhost:8080`) then the browser will let the JavaScript in the frontend send its request to the backend.
To achieve this, the backend must have a list of "allowed origins".
In this case, it would have to include `http://localhost:8080` for the frontend to work correctly.
## Wildcards
It's also possible to declare the list as `"*"` (a "wildcard") to say that all are allowed.
But that will only allow certain types of communication, excluding everything that involves credentials: Cookies, Authorization headers like those used with Bearer Tokens, etc.
So, for everything to work correctly, it's better to specify explicitly the allowed origins.
## Use `CORSMiddleware`
You can configure it in your **FastAPI** application using Starlette's <a href="https://www.starlette.io/middleware/#corsmiddleware" target="_blank">`CORSMiddleware`</a>.
* Import it form Starlette.
* Create a list of allowed origins (as strings).
* Add it as a "middleware" to your **FastAPI** application.
You can also specify if your backend allows:
* Credentials (Authorization headers, Cookies, etc).
* Specific HTTP methods (`POST`, `PUT`) or all of them with the wildcard `"*"`.
* Specific HTTP headers or all of them with the wildcard `"*"`.
```Python hl_lines="2 6 7 8 9 10 11 13 14 15 16 17 18 19"
{!./src/cors/tutorial001.py!}
```
## More info
For more details of what you can specify in `CORSMiddleware`, check <a href="https://www.starlette.io/middleware/#corsmiddleware" target="_blank">Starlette's `CORSMiddleware` docs</a>.
For more info about <abbr title="Cross-Origin Resource Sharing">CORS</abbr>, check the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank">Mozilla CORS documentation</a>.

View File

@@ -1,98 +1,88 @@
!!! warning
This is a rather advanced topic.
If you are starting with **FastAPI**, you might not need this.
By default, **FastAPI** will return the responses using Starlette's `JSONResponse`.
But you can override it.
You can override it by returning a `Response` directly, <a href="https://fastapi.tiangolo.com/tutorial/response-directly/" target="_blank">as seen in a previous section</a>.
But if you return a `Response` directly, the data won't be automatically converted, and the documentation won't be automatically generated (for example, including the specific "media type", in the HTTP header `Content-Type`).
But you can also declare the `Response` that you want to be used, in the *path operation decorator*.
The contents that you return from your *path operation function* will be put inside of that `Response`.
And if that `Response` has a JSON media type (`application/json`), like is the case with the `JSONResponse` and `UJSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*.
## Use `UJSONResponse`
For example, if you are squeezing performance, you can use `ujson` and set the response to be Starlette's `UJSONResponse`.
For example, if you are squeezing performance, you can install and use `ujson` and set the response to be Starlette's `UJSONResponse`.
### Import `UJSONResponse`
Import the `Response` class (sub-class) you want to use and declare it in the *path operation decorator*.
```Python hl_lines="2"
```Python hl_lines="2 7"
{!./src/custom_response/tutorial001.py!}
```
!!! note
Notice that you import it directly from `starlette.responses`, not from `fastapi`.
### Make your path operation use it
Make your path operation use `UJSONResponse` as the response class using the parameter `content_type`:
```Python hl_lines="7"
{!./src/custom_response/tutorial001.py!}
```
!!! info
The parameter is called `content_type` because it will also be used to define the "media type" of the response.
The parameter `response_class` will also be used to define the "media type" of the response.
And will be documented as such in OpenAPI.
In this case, the HTTP header `Content-Type` will be set to `application/json`.
And it will be documented as such in OpenAPI.
## HTML Response
To return a response with HTML directly from **FastAPI**, use `HTMLResponse`.
### Import `HTMLResponse`
* Import `HTMLResponse`.
* Pass `HTMLResponse` as the parameter `content_type` of your path operation.
```Python hl_lines="2"
```Python hl_lines="2 7"
{!./src/custom_response/tutorial002.py!}
```
!!! note
Notice that you import it directly from `starlette.responses`, not from `fastapi`.
### Define your `content_type` class
Pass `HTMLResponse` as the parameter `content_type` of your path operation:
```Python hl_lines="7"
{!./src/custom_response/tutorial002.py!}
```
!!! info
The parameter is called `content_type` because it will also be used to define the "media type" of the response.
The parameter `response_class` will also be used to define the "media type" of the response.
In this case, the HTTP header `Content-Type` will be set to `text/html`.
And it will be documented as such in OpenAPI.
### Return a Starlette `Response`
You can also override the response directly in your path operation.
If you return an object that is an instance of Starlette's `Response`, it will be used as the response directly.
As seen in <a href="https://fastapi.tiangolo.com/tutorial/response-directly/" target="_blank">another section</a>, you can also override the response directly in your path operation, by returning it.
The same example from above, returning an `HTMLResponse`, could look like:
```Python hl_lines="7"
```Python hl_lines="2 7 19"
{!./src/custom_response/tutorial003.py!}
```
!!! info
Of course, the `Content-Type` header will come from the the `Response` object your returned.
!!! warning
A `Response` returned directly by your path operation function won't be documented in OpenAPI and won't be visible in the automatic interactive docs.
A `Response` returned directly by your path operation function won't be documented in OpenAPI (for example, the `Content-Type` won't be documented) and won't be visible in the automatic interactive docs.
!!! info
Of course, the actual `Content-Type` header, status code, etc, will come from the `Response` object your returned.
### Document in OpenAPI and override `Response`
If you want to override the response from inside of the function but at the same time document the "media type" in OpenAPI, you can use the `content_type` parameter AND return a `Response` object.
If you want to override the response from inside of the function but at the same time document the "media type" in OpenAPI, you can use the `response_class` parameter AND return a `Response` object.
The `content_type` class will then be used only to document the OpenAPI path operation, but your `Response` will be used as is.
The `response_class` will then be used only to document the OpenAPI path operation, but your `Response` will be used as is.
#### Return an `HTMLResponse` directly
For example, it could be something like:
```Python hl_lines="7 23"
```Python hl_lines="7 23 21"
{!./src/custom_response/tutorial004.py!}
```
@@ -100,16 +90,10 @@ In this example, the function `generate_html_response()` already generates a Sta
By returning the result of calling `generate_html_response()`, you are already returning a `Response` that will override the default **FastAPI** behavior.
#### Declare `HTMLResponse` as `content_type`
But by declaring it also in the path operation decorator:
```Python hl_lines="21"
{!./src/custom_response/tutorial004.py!}
```
#### OpenAPI knows how to document it
...**FastAPI** will be able to document it in OpenAPI and in the interactive docs as HTML with `text/html`:
But as you passed the `HTMLResponse` in the `response_class`, **FastAPI** will know how to document it in OpenAPI and the interactive docs as HTML with `text/html`:
<img src="/img/tutorial/custom-response/image01.png">
## Additional documentation
You can also declare the media type and many other details in OpenAPI using `responses`: <a href="https://fastapi.tiangolo.com/tutorial/additional-responses/" target="_blank">Additional Responses in OpenAPI</a>.

32
docs/tutorial/encoder.md Normal file
View File

@@ -0,0 +1,32 @@
There are some cases where you might need to convert a data type (like a Pydantic model) to something compatible with JSON (like a `dict`, `list`, etc).
For example, if you need to store it in a database.
For that, **FastAPI** provides a `jsonable_encoder()` function.
## Using the `jsonable_encoder`
Let's imagine that you have a database `fake_db` that only receives JSON compatible data.
For example, it doesn't receive `datetime` objects, as those are not compatible with JSON.
So, a `datetime` object would have to be converted to a `str` containing the data in <a href="https://en.wikipedia.org/wiki/ISO_8601" target="_blank">ISO format</a>.
The same way, this database wouldn't receive a Pydantic model (an object with attributes), only a `dict`.
You can use `jsonable_encoder` for that.
It receives an object, like a Pydantic model, and returns a JSON compatible version:
```Python hl_lines="4 21"
{!./src/encoder/tutorial001.py!}
```
In this example, it would convert the Pydantic model to a `dict`, and the `datetime` to a `str`.
The result of calling it is something that can be encoded with the Python standard <a href="https://docs.python.org/3/library/json.html#json.dumps" target="_blank">`json.dumps()`</a>.
It doesn't return a large `str` containing the data in JSON format (as a string). It returns a Python standard data structure (e.g. a `dict`) with values and sub-values that are all compatible with JSON.
!!! note
`jsonable_encoder` is actually used by **FastAPI** internally to convert data. But it is useful in many other scenarios.

View File

@@ -3,7 +3,7 @@ Continuing with the previous example, it will be common to have more than one re
This is especially the case for user models, because:
* The **input model** needs to be able to have a password.
* The **output model** should do not have a password.
* The **output model** should not have a password.
* The **database model** would probably need to have a hashed password.
!!! danger

View File

@@ -0,0 +1,54 @@
You can add middleware to **FastAPI** applications.
A "middleware" is a function that works with every **request** before it is processed by any specific *path operation*. And also with every **response** before returning it.
* It takes each **request** that comes to your application.
* It can then do something to that **request** or run any needed code.
* Then it passes the **request** to be processed by the rest of the application (by some *path operation*).
* It then takes the **response** generated by the application (by some *path operation*).
* It can do something to that **response** or run any needed code.
* Then it returns the **response**.
## Create a middleware
To create a middleware you use the decorator `@app.middleware("http")` on top of a function.
The middleware function receives:
* The `request`.
* A function `call_next` that will receive the `request` as a parameter.
* This function will pass the `request` to the corresponding *path operation*.
* Then it returns the `response` generated by the corresponding *path operation*.
* You can then modify further the `response` before returning it.
```Python hl_lines="9 10 12 15"
{!./src/middleware/tutorial001.py!}
```
!!! tip
This technique is used in the tutorial about <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">SQL (Relational) Databases</a>.
### Before and after the `response`
You can add code to be run with the `request`, before any *path operation* receives it.
And also after the `response` is generated, before returning it.
For example, you could add a custom header `X-Process-Time` containing the time in seconds that it took to process the request and generate a response:
```Python hl_lines="11 13 14"
{!./src/middleware/tutorial001.py!}
```
## Starlette's Middleware
You can also add any other <a href="https://www.starlette.io/middleware/" target="_blank">Starlette Middleware</a>.
These are classes instead of plain functions.
Including:
* `CORSMiddleware` (described in the next section).
* `GZipMiddleware`.
* `SentryMiddleware`.
* ...and others.

View File

@@ -42,6 +42,8 @@ You can add a `summary` and `description`:
As descriptions tend to be long and cover multiple lines, you can declare the path operation description in the function <abbr title="a multi-line string as the first expression inside a function (not assigned to any variable) used for documentation">docstring</abbr> and **FastAPI** will read it from there.
You can write <a href="https://en.wikipedia.org/wiki/Markdown" target="_blank">Markdown</a> in the docstring, it will be interpreted and displayed correctly (taking into account docstring indentation).
```Python hl_lines="19 20 21 22 23 24 25 26 27"
{!./src/path_operation_configuration/tutorial004.py!}
```
@@ -50,9 +52,6 @@ It will be used in the interactive docs:
<img src="/img/tutorial/path-operation-configuration/image02.png">
!!! info
OpenAPI specifies that descriptions can be written in Markdown syntax, but the interactive documentation systems included still don't support it at the time of writing this, although they have it in their plans.
## Response description
You can specify the response description with the parameter `response_description`:

View File

@@ -81,31 +81,31 @@ You can also declare `bool` types, and they will be converted:
In this case, if you go to:
```
http://127.0.0.1:8000/items/?short=1
http://127.0.0.1:8000/items/foo?short=1
```
or
```
http://127.0.0.1:8000/items/?short=True
http://127.0.0.1:8000/items/foo?short=True
```
or
```
http://127.0.0.1:8000/items/?short=true
http://127.0.0.1:8000/items/foo?short=true
```
or
```
http://127.0.0.1:8000/items/?short=on
http://127.0.0.1:8000/items/foo?short=on
```
or
```
http://127.0.0.1:8000/items/?short=yes
http://127.0.0.1:8000/items/foo?short=yes
```
or any other case variation (uppercase, first letter in uppercase, etc), your function will see the parameter `short` with a `bool` value of `True`. Otherwise as `False`.

View File

@@ -121,6 +121,13 @@ To use that, declare a `List` of `bytes` or `UploadFile`:
You will receive, as declared, a `list` of `bytes` or `UploadFile`s.
!!! note
Notice that, as of 2019-04-14, Swagger UI doesn't support multiple file uploads in the same form field. For more information, check <a href="https://github.com/swagger-api/swagger-ui/issues/4276" target="_blank">#4276</a> and <a href="https://github.com/swagger-api/swagger-ui/issues/3641" target="_blank">#3641</a>.
Nevertheless, **FastAPI** is already compatible with it, using the standard OpenAPI.
So, whenever Swagger UI supports multi-file uploads, or any other tools that supports OpenAPI, they will be compatible with **FastAPI**.
## Recap
Use `File` to declare files to be uploaded as input parameters (as form data).

View File

@@ -0,0 +1,63 @@
When you create a **FastAPI** *path operation* you can normally return any data from it: a `dict`, a `list`, a Pydantic model, a database model, etc.
By default, **FastAPI** would automatically convert that return value to JSON using the <a href="https://fastapi.tiangolo.com/tutorial/encoder/" target="_blank">`jsonable_encoder`</a>.
Then, behind the scenes, it would put that JSON-compatible data (e.g. a `dict`) inside of a Starlette `JSONResponse` that would be used to send the response to the client.
But you can return a `JSONResponse` directly from your *path operations*.
It might be useful, for example, to return custom headers or cookies.
## Starlette `Response`
In fact, you can return any <a href="https://www.starlette.io/responses/" target="_blank">Starlette `Response`</a> or any sub-class of it.
!!! tip
`JSONResponse` itself is a sub-class of `Response`.
And when you return a Starlette `Response`, **FastAPI** will pass it directly.
It won't do any data conversion with Pydantic models, it won't convert the contents to any type, etc.
This gives you a lot of flexibility. You can return any data type, override any data declaration or validation, etc.
## Using the `jsonable_encoder` in a `Response`
Because **FastAPI** doesn't do any change to a `Response` you return, you have to make sure it's contents are ready for it.
For example, you cannot put a Pydantic model in a `JSONResponse` without first converting it to a `dict` with all the data types (like `datetime`, `UUID`, etc) converted to JSON-compatible types.
For those cases, you can use the `jsonable_encoder` to convert your data before passing it to a response:
```Python hl_lines="4 6 20 21"
{!./src/response_directly/tutorial001.py!}
```
!!! note
Notice that you import it directly from `starlette.responses`, not from `fastapi`.
## Returning a custom `Response`
The example above shows all the parts you need, but it's not very useful yet, as you could have just returned the `item` directly, and **FastAPI** would put it in a `JSONResponse` for you, converting it to a `dict`, etc. All that by default.
Now, let's see how you could use that to return a custom response.
Let's say you want to return a response that is not available in the default <a href="https://www.starlette.io/responses/" target="_blank">Starlette `Response`s</a>.
Let's say that you want to return <a href="https://en.wikipedia.org/wiki/XML" target="_blank">XML</a>.
You could put your XML content in a string, put it in a Starlette Response, and return it:
```Python hl_lines="2 20"
{!./src/response_directly/tutorial002.py!}
```
## Notes
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>.
In the next sections you will see how to use/declare these custom `Response`s while still having automatic data conversion, documentation, etc.
You will also see how to use them to set response Headers and Cookies.

View File

@@ -0,0 +1,40 @@
For the simplest cases, you can use HTTP Basic Auth.
In HTTP Basic Auth, the application expects a header that contains a username and a password.
If it doesn't receive it, it returns an HTTP 401 "Unauthorized" error.
And returns a header `WWW-Authenticate` with a value of `Basic`, and an optional `realm` parameter.
That tells the browser to show the integrated prompt for a username and password.
Then, when you type that username and password, the browser sends them in the header automatically.
## Simple HTTP Basic Auth
* Import `HTTPBAsic` and `HTTPBasicCredentials`.
* Create a "`security` scheme" using `HTTPBAsic`.
* Use that `security` with a dependency in your *path operation*.
* It returns an object of type `HTTPBasicCredentials`:
* It contains the `username` and `password` sent.
```Python hl_lines="2 6 10"
{!./src/security/tutorial006.py!}
```
When you try to open the URL for the first time (or click the "Execute" button in the docs) the browser will ask you for your username and password:
<img src="/img/tutorial/security/image12.png">
## Check the username
Here's a more complete example.
Use a dependency to check if the username and password are correct.
If the credentials are incorrect, return an `HTTPException` with a status code 401 (the same returned when no credentials are provided) and add the header `WWW-Authenticate` to make the browser show the login prompt again:
```Python hl_lines="10 11 12 13 14 15 16 17 21"
{!./src/security/tutorial007.py!}
```

View File

@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.15.0"
__version__ = "0.19.0"
from starlette.background import BackgroundTasks

View File

@@ -119,7 +119,7 @@ class FastAPI(Starlette):
methods: List[str] = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> None:
self.router.add_api_route(
@@ -136,7 +136,7 @@ class FastAPI(Starlette):
methods=methods,
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
@@ -155,7 +155,7 @@ class FastAPI(Starlette):
methods: List[str] = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
def decorator(func: Callable) -> Callable:
@@ -173,7 +173,7 @@ class FastAPI(Starlette):
methods=methods,
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
return func
@@ -206,7 +206,7 @@ class FastAPI(Starlette):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.router.get(
@@ -221,7 +221,7 @@ class FastAPI(Starlette):
deprecated=deprecated,
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
@@ -239,7 +239,7 @@ class FastAPI(Starlette):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.router.put(
@@ -254,7 +254,7 @@ class FastAPI(Starlette):
deprecated=deprecated,
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
@@ -272,7 +272,7 @@ class FastAPI(Starlette):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.router.post(
@@ -287,7 +287,7 @@ class FastAPI(Starlette):
deprecated=deprecated,
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
@@ -305,7 +305,7 @@ class FastAPI(Starlette):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.router.delete(
@@ -320,7 +320,7 @@ class FastAPI(Starlette):
deprecated=deprecated,
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
@@ -338,7 +338,7 @@ class FastAPI(Starlette):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.router.options(
@@ -353,7 +353,7 @@ class FastAPI(Starlette):
deprecated=deprecated,
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
@@ -371,7 +371,7 @@ class FastAPI(Starlette):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.router.head(
@@ -386,7 +386,7 @@ class FastAPI(Starlette):
deprecated=deprecated,
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
@@ -404,7 +404,7 @@ class FastAPI(Starlette):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.router.patch(
@@ -419,7 +419,7 @@ class FastAPI(Starlette):
deprecated=deprecated,
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
@@ -437,7 +437,7 @@ class FastAPI(Starlette):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.router.trace(
@@ -452,6 +452,6 @@ class FastAPI(Starlette):
deprecated=deprecated,
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)

View File

@@ -22,8 +22,8 @@ from fastapi.dependencies.models import Dependant, SecurityRequirement
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 UnconstrainedConfig, get_path_param_names
from pydantic import Schema, create_model
from fastapi.utils import get_path_param_names
from pydantic import BaseConfig, Schema, create_model
from pydantic.error_wrappers import ErrorWrapper
from pydantic.errors import MissingError
from pydantic.fields import Field, Required, Shape
@@ -203,8 +203,8 @@ def add_param_to_fields(
default=None if required else default_value,
alias=alias,
required=required,
model_config=UnconstrainedConfig,
class_validators=[],
model_config=BaseConfig,
class_validators={},
schema=schema,
)
if schema.in_ == params.ParamTypes.path:
@@ -237,8 +237,8 @@ def add_param_to_body_fields(*, param: inspect.Parameter, dependant: Dependant)
default=None if required else default_value,
alias=schema.alias or param.name,
required=required,
model_config=UnconstrainedConfig,
class_validators=[],
model_config=BaseConfig,
class_validators={},
schema=schema,
)
dependant.body_params.append(field)
@@ -336,7 +336,7 @@ def request_params_to_args(
ErrorWrapper(
MissingError(),
loc=(schema.in_.value, field.alias),
config=UnconstrainedConfig,
config=BaseConfig,
)
)
else:
@@ -379,9 +379,7 @@ async def request_body_to_args(
if field.required:
errors.append(
ErrorWrapper(
MissingError(),
loc=("body", field.alias),
config=UnconstrainedConfig,
MissingError(), loc=("body", field.alias), config=BaseConfig
)
)
else:
@@ -456,8 +454,8 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[Field]:
type_=BodyModel,
default=None,
required=required,
model_config=UnconstrainedConfig,
class_validators=[],
model_config=BaseConfig,
class_validators={},
alias="body",
schema=BodySchema(None),
)

View File

@@ -10,7 +10,7 @@ def jsonable_encoder(
obj: Any,
include: Set[str] = None,
exclude: Set[str] = set(),
by_alias: bool = False,
by_alias: bool = True,
include_none: bool = True,
custom_encoder: dict = {},
sqlalchemy_safe: bool = True,

View File

@@ -197,7 +197,7 @@ def get_openapi_path(
] = response
status_code = str(route.status_code)
response_schema = {"type": "string"}
if lenient_issubclass(route.content_type, JSONResponse):
if lenient_issubclass(route.response_class, JSONResponse):
if route.response_field:
response_schema, _ = field_schema(
route.response_field,
@@ -211,7 +211,7 @@ def get_openapi_path(
] = route.response_description
operation.setdefault("responses", {}).setdefault(
status_code, {}
).setdefault("content", {}).setdefault(route.content_type.media_type, {})[
).setdefault("content", {}).setdefault(route.response_class.media_type, {})[
"schema"
] = response_schema
if all_route_params or route.body_field:

View File

@@ -7,8 +7,7 @@ from fastapi import params
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import get_body_field, get_dependant, solve_dependencies
from fastapi.encoders import jsonable_encoder
from fastapi.utils import UnconstrainedConfig
from pydantic import BaseModel, Schema
from pydantic import BaseConfig, BaseModel, Schema
from pydantic.error_wrappers import ErrorWrapper, ValidationError
from pydantic.fields import Field
from pydantic.utils import lenient_issubclass
@@ -41,7 +40,7 @@ def get_app(
dependant: Dependant,
body_field: Field = None,
status_code: int = 200,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
response_field: Field = None,
) -> Callable:
assert dependant.call is not None, "dependant.call must me a function"
@@ -59,7 +58,7 @@ def get_app(
if body_bytes:
body = await request.json()
except Exception as e:
logging.error("Error getting request body", e)
logging.error(f"Error getting request body: {e}")
raise HTTPException(
status_code=400, detail="There was an error parsing the body"
)
@@ -84,7 +83,7 @@ def get_app(
response_data = serialize_response(
field=response_field, response=raw_response
)
return content_type(
return response_class(
content=response_data,
status_code=status_code,
background=background_tasks,
@@ -111,7 +110,7 @@ class APIRoute(routing.Route):
methods: List[str] = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
) -> None:
assert path.startswith("/"), "Routed paths must always start with '/'"
self.path = path
@@ -120,16 +119,16 @@ class APIRoute(routing.Route):
self.response_model = response_model
if self.response_model:
assert lenient_issubclass(
content_type, JSONResponse
response_class, JSONResponse
), "To declare a type the response must be a JSON response"
response_name = "Response_" + self.name
self.response_field: Optional[Field] = Field(
name=response_name,
type_=self.response_model,
class_validators=[],
class_validators={},
default=None,
required=False,
model_config=UnconstrainedConfig,
model_config=BaseConfig,
schema=Schema(None),
)
else:
@@ -137,7 +136,7 @@ class APIRoute(routing.Route):
self.status_code = status_code
self.tags = tags or []
self.summary = summary
self.description = description or self.endpoint.__doc__
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
self.response_description = response_description
self.responses = responses or {}
response_fields = {}
@@ -155,7 +154,7 @@ class APIRoute(routing.Route):
class_validators=None,
default=None,
required=False,
model_config=UnconstrainedConfig,
model_config=BaseConfig,
schema=Schema(None),
)
response_fields[additional_status_code] = response_field
@@ -169,7 +168,7 @@ class APIRoute(routing.Route):
self.methods = methods
self.operation_id = operation_id
self.include_in_schema = include_in_schema
self.content_type = content_type
self.response_class = response_class
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
assert inspect.isfunction(endpoint) or inspect.ismethod(
@@ -182,7 +181,7 @@ class APIRoute(routing.Route):
dependant=self.dependant,
body_field=self.body_field,
status_code=self.status_code,
content_type=self.content_type,
response_class=self.response_class,
response_field=self.response_field,
)
)
@@ -205,7 +204,7 @@ class APIRouter(routing.Router):
methods: List[str] = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> None:
route = APIRoute(
@@ -222,7 +221,7 @@ class APIRouter(routing.Router):
methods=methods,
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
self.routes.append(route)
@@ -242,7 +241,7 @@ class APIRouter(routing.Router):
methods: List[str] = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
def decorator(func: Callable) -> Callable:
@@ -260,7 +259,7 @@ class APIRouter(routing.Router):
methods=methods,
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
return func
@@ -299,7 +298,7 @@ class APIRouter(routing.Router):
methods=route.methods,
operation_id=route.operation_id,
include_in_schema=route.include_in_schema,
content_type=route.content_type,
response_class=route.response_class,
name=route.name,
)
elif isinstance(route, routing.Route):
@@ -329,7 +328,7 @@ class APIRouter(routing.Router):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.api_route(
@@ -345,7 +344,7 @@ class APIRouter(routing.Router):
methods=["GET"],
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
@@ -363,7 +362,7 @@ class APIRouter(routing.Router):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.api_route(
@@ -379,7 +378,7 @@ class APIRouter(routing.Router):
methods=["PUT"],
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
@@ -397,7 +396,7 @@ class APIRouter(routing.Router):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.api_route(
@@ -413,7 +412,7 @@ class APIRouter(routing.Router):
methods=["POST"],
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
@@ -431,7 +430,7 @@ class APIRouter(routing.Router):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.api_route(
@@ -447,7 +446,7 @@ class APIRouter(routing.Router):
methods=["DELETE"],
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
@@ -465,7 +464,7 @@ class APIRouter(routing.Router):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.api_route(
@@ -481,7 +480,7 @@ class APIRouter(routing.Router):
methods=["OPTIONS"],
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
@@ -499,7 +498,7 @@ class APIRouter(routing.Router):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.api_route(
@@ -515,7 +514,7 @@ class APIRouter(routing.Router):
methods=["HEAD"],
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
@@ -533,7 +532,7 @@ class APIRouter(routing.Router):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.api_route(
@@ -549,7 +548,7 @@ class APIRouter(routing.Router):
methods=["PATCH"],
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)
@@ -567,7 +566,7 @@ class APIRouter(routing.Router):
deprecated: bool = None,
operation_id: str = None,
include_in_schema: bool = True,
content_type: Type[Response] = JSONResponse,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.api_route(
@@ -583,6 +582,6 @@ class APIRouter(routing.Router):
methods=["TRACE"],
operation_id=operation_id,
include_in_schema=include_in_schema,
content_type=content_type,
response_class=response_class,
name=name,
)

View File

@@ -2,6 +2,7 @@ import binascii
from base64 import b64decode
from typing import Optional
from fastapi.exceptions import HTTPException
from fastapi.openapi.models import (
HTTPBase as HTTPBaseModel,
HTTPBearer as HTTPBearerModel,
@@ -9,9 +10,8 @@ from fastapi.openapi.models import (
from fastapi.security.base import SecurityBase
from fastapi.security.utils import get_authorization_scheme_param
from pydantic import BaseModel
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.status import HTTP_403_FORBIDDEN
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
class HTTPBasicCredentials(BaseModel):
@@ -59,15 +59,21 @@ class HTTPBasic(HTTPBase):
async def __call__(self, request: Request) -> Optional[HTTPBasicCredentials]:
authorization: str = request.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(authorization)
# before implementing headers with 401 errors, wait for: https://github.com/encode/starlette/issues/295
# unauthorized_headers = {"WWW-Authenticate": "Basic"}
if self.realm:
unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
else:
unauthorized_headers = {"WWW-Authenticate": "Basic"}
invalid_user_credentials_exc = HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Invalid authentication credentials"
status_code=HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers=unauthorized_headers,
)
if not authorization or scheme.lower() != "basic":
if self.auto_error:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers=unauthorized_headers,
)
else:
return None
@@ -87,7 +93,7 @@ class HTTPBearer(HTTPBase):
*,
bearerFormat: str = None,
scheme_name: str = None,
auto_error: bool = True
auto_error: bool = True,
):
self.model = HTTPBearerModel(bearerFormat=bearerFormat)
self.scheme_name = scheme_name or self.__class__.__name__

View File

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

View File

@@ -44,8 +44,10 @@ nav:
- Path Operation Configuration: 'tutorial/path-operation-configuration.md'
- Path Operation Advanced Configuration: 'tutorial/path-operation-advanced-configuration.md'
- Additional Status Codes: 'tutorial/additional-status-codes.md'
- Custom Response: 'tutorial/custom-response.md'
- Additional Responses: 'tutorial/additional-responses.md'
- JSON compatible encoder: 'tutorial/encoder.md'
- Return a Response directly: 'tutorial/response-directly.md'
- Custom Response Class: 'tutorial/custom-response.md'
- Additional Responses in OpenAPI: 'tutorial/additional-responses.md'
- Dependencies:
- First Steps: 'tutorial/dependencies/first-steps.md'
- Classes as Dependencies: 'tutorial/dependencies/classes-as-dependencies.md'
@@ -58,6 +60,9 @@ nav:
- Simple OAuth2 with Password and Bearer: 'tutorial/security/simple-oauth2.md'
- OAuth2 with Password (and hashing), Bearer with JWT tokens: 'tutorial/security/oauth2-jwt.md'
- OAuth2 scopes: 'tutorial/security/oauth2-scopes.md'
- HTTP Basic Auth: 'tutorial/security/http-basic-auth.md'
- Middleware: 'tutorial/middleware.md'
- CORS (Cross-Origin Resource Sharing): 'tutorial/cors.md'
- Using the Request Directly: 'tutorial/using-request-directly.md'
- SQL (Relational) Databases: 'tutorial/sql-databases.md'
- Async SQL (Relational) Databases: 'tutorial/async-sql-databases.md'

View File

@@ -20,7 +20,7 @@ classifiers = [
]
requires = [
"starlette ==0.11.1",
"pydantic >=0.17,<=0.21.0"
"pydantic >=0.17,<=0.23.0"
]
description-file = "README.md"
requires-python = ">=3.6"

7
scripts/deploy.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -e
bash scripts/publish.sh
bash scripts/trigger-docker.sh

5
scripts/publish.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -e
flit publish

View File

@@ -3,7 +3,7 @@ from enum import Enum
import pytest
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from pydantic import BaseModel, Schema, ValidationError
class Person:
@@ -59,6 +59,10 @@ class ModelWithConfig(BaseModel):
use_enum_values = True
class ModelWithAlias(BaseModel):
foo: str = Schema(..., alias="Foo")
def test_encode_class():
person = Person(name="Foo")
pet = Pet(owner=person, name="Firulais")
@@ -85,3 +89,13 @@ def test_encode_custom_json_encoders_model():
def test_encode_model_with_config():
model = ModelWithConfig(role=RoleEnum.admin)
assert jsonable_encoder(model) == {"role": "admin"}
def test_encode_model_with_alias_raises():
with pytest.raises(ValidationError):
model = ModelWithAlias(foo="Bar")
def test_encode_model_with_alias():
model = ModelWithAlias(Foo="Bar")
assert jsonable_encoder(model) == {"Foo": "Bar"}

View File

@@ -67,7 +67,8 @@ def test_security_http_basic_invalid_credentials():
response = client.get(
"/users/me", headers={"Authorization": "Basic notabase64token"}
)
assert response.status_code == 403
assert response.status_code == 401
assert response.headers["WWW-Authenticate"] == "Basic"
assert response.json() == {"detail": "Invalid authentication credentials"}
@@ -75,5 +76,6 @@ def test_security_http_basic_non_basic_credentials():
payload = b64encode(b"johnsecret").decode("ascii")
auth_header = f"Basic {payload}"
response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 403
assert response.status_code == 401
assert response.headers["WWW-Authenticate"] == "Basic"
assert response.json() == {"detail": "Invalid authentication credentials"}

View File

@@ -7,7 +7,7 @@ from starlette.testclient import TestClient
app = FastAPI()
security = HTTPBasic()
security = HTTPBasic(realm="simple")
@app.get("/users/me")
@@ -56,15 +56,17 @@ def test_security_http_basic():
def test_security_http_basic_no_credentials():
response = client.get("/users/me")
assert response.status_code == 403
assert response.json() == {"detail": "Not authenticated"}
assert response.status_code == 401
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
def test_security_http_basic_invalid_credentials():
response = client.get(
"/users/me", headers={"Authorization": "Basic notabase64token"}
)
assert response.status_code == 403
assert response.status_code == 401
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
assert response.json() == {"detail": "Invalid authentication credentials"}
@@ -72,5 +74,6 @@ def test_security_http_basic_non_basic_credentials():
payload = b64encode(b"johnsecret").decode("ascii")
auth_header = f"Basic {payload}"
response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 403
assert response.status_code == 401
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
assert response.json() == {"detail": "Invalid authentication credentials"}

View File

@@ -31,7 +31,7 @@ openapi_schema = {
},
},
"summary": "Create an item",
"description": "\n Create an item with all the information:\n \n * name: each item must have a name\n * description: a long description\n * price: required\n * tax: if the item doesn't have tax, you can omit this\n * tags: a set of unique tag strings for this item\n ",
"description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item",
"operationId": "create_item_items__post",
"requestBody": {
"content": {

View File

@@ -0,0 +1,69 @@
from base64 import b64encode
from requests.auth import HTTPBasicAuth
from starlette.testclient import TestClient
from security.tutorial006 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/users/me": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Read Current User",
"operationId": "read_current_user_users_me_get",
"security": [{"HTTPBasic": []}],
}
}
},
"components": {
"securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_security_http_basic():
auth = HTTPBasicAuth(username="john", password="secret")
response = client.get("/users/me", auth=auth)
assert response.status_code == 200
assert response.json() == {"username": "john", "password": "secret"}
def test_security_http_basic_no_credentials():
response = client.get("/users/me")
assert response.json() == {"detail": "Not authenticated"}
assert response.status_code == 401
assert response.headers["WWW-Authenticate"] == "Basic"
def test_security_http_basic_invalid_credentials():
response = client.get(
"/users/me", headers={"Authorization": "Basic notabase64token"}
)
assert response.status_code == 401
assert response.headers["WWW-Authenticate"] == "Basic"
assert response.json() == {"detail": "Invalid authentication credentials"}
def test_security_http_basic_non_basic_credentials():
payload = b64encode(b"johnsecret").decode("ascii")
auth_header = f"Basic {payload}"
response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401
assert response.headers["WWW-Authenticate"] == "Basic"
assert response.json() == {"detail": "Invalid authentication credentials"}