Compare commits

...

169 Commits

Author SHA1 Message Date
Sebastián Ramírez
53da56146e 🔖 Release version 0.37.0 2019-08-30 19:10:43 -05:00
Sebastián Ramírez
3799b9027e 📝 Update release notes 2019-08-30 19:09:12 -05:00
dmontagu
c70f3f1198 Add support for custom route class (#468) 2019-08-30 19:05:59 -05:00
Sebastián Ramírez
58dddc5e4f 📝 Update release notes 2019-08-30 19:02:29 -05:00
b1-luettje
c90c4fb6c1 Allow disabling Google fonts in ReDoc (#481) 2019-08-30 19:00:55 -05:00
Sebastián Ramírez
5b3df28f0c 📝 Update release notes 2019-08-30 18:59:08 -05:00
dmontagu
6c6bdb6233 🔒 Ensure skip_defaults doesn't cause extra fields to be serialized (#485) 2019-08-30 18:56:14 -05:00
Sebastián Ramírez
f156f45193 📝 Update release notes 2019-08-30 18:37:42 -05:00
Aliaksei Urbanski
f24d744a3b Enable tests for Python 3.8-dev (#465) 2019-08-30 18:34:49 -05:00
Sebastián Ramírez
937b462cdd 📝 Update release notes 2019-08-30 18:15:08 -05:00
dconathan
3025a368c6 Add support and tests for Pydantic dataclasses in response_model (#454) 2019-08-30 18:12:15 -05:00
Sebastián Ramírez
c218e0d560 📝 Update release notes 2019-08-30 17:40:14 -05:00
Pablo Marti
1ed5aa23e6 ✏️ Fix typo in oauth2-jwt.md (#447) 2019-08-30 17:35:52 -05:00
Sebastián Ramírez
106d2171d8 📝 Update release notes 2019-08-30 17:34:45 -05:00
Zoltan Papp
c5817912d2 🐛 use media_type from Body params for OpenAPI requestBody (Fixes: #431) (#439) 2019-08-30 17:32:39 -05:00
Sebastián Ramírez
a7a92bc637 📝 Update release notes 2019-08-30 17:02:40 -05:00
naxty
68d1fea961 📝 Add article: Deploying a scikit-learn model with ONNX and FastAPI (#438) 2019-08-30 17:00:00 -05:00
Sebastián Ramírez
8c6b2d5804 📝 Update release notes 2019-08-30 16:48:53 -05:00
Zoltan Papp
19c53b21c1 Allow using custom 422 validation error and use media type from response class in schema (#437)
* media_type of additional responses from the response_class

* Use HTTPValidationError only if a custom one is not defined (Fixes: #429)
2019-08-30 16:46:05 -05:00
Sebastián Ramírez
44d63cd555 📝 Update release notes 2019-08-30 16:36:18 -05:00
Sebastián Ramírez
55c4b5fb0b 🐛 Fix "default" extra response with extra status codes (#489)
* 🐛 Fix lowercase "default" extra response

* 🐛 Fix model for responses, to allow "default" plus status codes

*  Add test for "default" extra response
2019-08-30 16:34:47 -05:00
Sebastián Ramírez
c32e800c23 📝 Update release notes 2019-08-30 11:30:52 -05:00
Zoltan Papp
73dbbeab55 Allow additional responses to use status ranges and "default" (#435) 2019-08-30 11:17:42 -05:00
Sebastián Ramírez
417a3ab140 🔖 Release 0.36.0 2019-08-26 08:28:33 -05:00
Sebastián Ramírez
a3235ed8de 📝 Update release notes 2019-08-26 08:27:31 -05:00
dmontagu
38495fffa5 🐛 Fix skip_defaults implementation when returning a Pydantic model (#422) 2019-08-26 08:24:58 -05:00
Sebastián Ramírez
b77a43bcac 📝 Update release notes 2019-08-24 22:08:10 -05:00
dmontagu
483eb73b26 🐛 Use caching logic to determine OpenAPI spec for duplicate dependencies (#417) 2019-08-24 21:55:25 -05:00
Sebastián Ramírez
51a928d3f5 📝 Update release notes 2019-08-24 20:08:04 -05:00
Sebastián Ramírez
e71636e381 🐛 Fix mypy route errors after merging #415 (#462) 2019-08-24 20:05:44 -05:00
Vitaliy Kucheryaviy
f7f17fcfd6 Allow empty routed path (issue #414) (#415) 2019-08-24 19:39:48 -05:00
Sebastián Ramírez
033bc2a6c9 🔖 Release 0.35.0 2019-08-07 14:12:15 -05:00
Sebastián Ramírez
28d3b9f783 📝 Update release notes 2019-08-07 14:09:50 -05:00
Pablo Marti
0c55553328 ✏️ Fix typo in assert statement (#419) 2019-08-07 14:03:11 -05:00
Bronsen
b66056aa34 📝 Fix plural-s without apostrophe in docs (#411) 2019-08-07 14:01:31 -05:00
Sebastián Ramírez
4f10b8b98d 📝 Update release notes 2019-08-07 13:57:41 -05:00
Koudai Aono
06eb421934 Fix request body parsing with Union (#400) 2019-08-07 13:55:33 -05:00
Sebastián Ramírez
bf229ad5d8 🔖 Release 0.34.0 upgrading Starlette 2019-08-06 07:22:06 -05:00
Sebastián Ramírez
d0319001be 📝 Update Release Notes 2019-08-06 07:13:24 -05:00
David De Sousa
c4682af13d ⬆️ Upgrade Starlette max range to 0.12.7 (#367) 2019-08-06 07:10:29 -05:00
Sebastián Ramírez
6ca3ce80e4 📝 Update release notes 2019-07-12 19:15:21 -05:00
Sebastián Ramírez
25e85c8522 Add test from @dmontagu in #333 for duplicate models (#385) 2019-07-12 19:13:28 -05:00
Sebastián Ramírez
6bf3ab3b7a 🔖 Release 0.33.0, including Pydantic 0.30.0 2019-07-12 19:01:27 -05:00
Sebastián Ramírez
f5ea5eef2a 📝 Update release notes 2019-07-12 18:58:09 -05:00
James Kaplan
46a986cacf ⬆️ Upgrade Pydantic to 0.30 (#384)
* bump pydantic to 0.30

* 📌 Pin Pydantic to 0.30 as 0.31 hasn't been released
2019-07-12 18:56:25 -05:00
Sebastián Ramírez
e620aeb46d 🔖 Release 0.32.0, as PR ##347 might be a breaking change
in some specific cases
2019-07-12 18:32:30 -05:00
Sebastián Ramírez
d1e2e46b80 🔖 Release 0.31.1 2019-07-12 18:30:54 -05:00
Sebastián Ramírez
b1c4a8acd5 📝 Update release notes 2019-07-12 18:29:49 -05:00
Martino Mensio
362e2cdc79 📝 Fix small typo in docs for features (#380) 2019-07-12 18:28:07 -05:00
Sebastián Ramírez
93e6a08acd 📝 Update release notes 2019-07-12 18:25:04 -05:00
Ben Williams
3ec4342282 📝 Change limit default parameter to 10 in Query docs (#366)
Rest of docs reference 10 as the default.
2019-07-12 18:22:21 -05:00
Sebastián Ramírez
dc483478eb 📝 Update release notes 2019-07-12 18:20:02 -05:00
Chris Withers
bdd251a05b 📝 Tweak wording on OAuth2 scopes (#371) 2019-07-12 18:17:34 -05:00
Sebastián Ramírez
195559ccba 📝 Update release notes 2019-06-28 21:29:29 +02:00
Sebastián Ramírez
9a71672a95 📝 Update enum examples to use str, and improve Swagger UI in examples (#351) 2019-06-28 21:27:27 +02:00
Sebastián Ramírez
7e48be1561 📝 Update release notes 2019-06-28 20:57:14 +02:00
Sebastián Ramírez
508f9ce954 🐛 Fix regression, Swagger UI with deep linking (#350) 2019-06-28 20:56:48 +02:00
Sebastián Ramírez
afbdf2546f 📝 Update release notes 2019-06-28 20:16:53 +02:00
Sebastián Ramírez
62df417807 Add test for templates in include_router path (#349) 2019-06-28 20:15:17 +02:00
Sebastián Ramírez
09d2747a70 📝 Update release notes 2019-06-28 20:00:24 +02:00
Sebastián Ramírez
d3ea6f7514 📝 Add note to docs about including same router multiple times (#348) 2019-06-28 19:54:49 +02:00
Sebastián Ramírez
02187636ea 📝 Update release notes 2019-06-28 19:40:31 +02:00
Sebastián Ramírez
687065509b 🏗️ Fix same function names in different modules with composite bodies (#347)
* 🏗️ Implement unique IDs for dynamic models

like those used for composite bodies and responses. IDs based on path (not only on function name, as it can be duplicated in a different module).

*  Add tests for same function name and composite body

*  Update OpenAPI in tests with new dynamic model ID generation
2019-06-28 19:35:16 +02:00
Sebastián Ramírez
b30cca8e9e 🔖 Release 0.31.0, upgrading Pydantic to 0.29 2019-06-28 17:01:04 +02:00
Sebastián Ramírez
3906777065 📝 Update release notes 2019-06-28 12:36:52 +02:00
Sebastián Ramírez
d60a10fa59 ⬆️ Upgrade support for Pydantic to 0.29 (#344) 2019-06-28 12:36:08 +02:00
Sebastián Ramírez
54368e7b22 🔖 Release 0.30.1 2019-06-28 09:39:29 +02:00
Sebastián Ramírez
acc556e416 📝 Update release notes 2019-06-27 22:44:54 +02:00
Sebastián Ramírez
700585f99d 📝 Add section about external links to docs (#341) 2019-06-27 22:44:15 +02:00
Sebastián Ramírez
4c2993f353 📝 Update release notes 2019-06-27 21:53:59 +02:00
Sebastián Ramírez
ea9277aab4 🔥 Remove Pipfile.lock from the repository (each contributor can keep his/her locally)
* 🔥 Remove Pipfile.lock

Being a library, it should work independent of locking of dependency tree.

The Pipfile (and Pipfile.lock) is only used locally for development of FastAPI itself, it doesn't affect final users (that is controlled with pyproject.toml).

The Pipfile.lock adds unnecessary noise to PRs that update/upgrade development packages, and the locking is not the same in all environments (e.g. Linux, Mac, and Windows).

Each FastAPI contributor (developing FastAPI itself) can have his/her own Pipfile.lock, but it doesn't have to be in git.

* 🙈 Add Pipfile.lock to .gitignore
2019-06-27 21:51:38 +02:00
Sebastián Ramírez
8d86fca027 📝 Update release notes 2019-06-27 21:33:07 +02:00
Sebastián Ramírez
fc0716a7dd 📝 Update Docs: Help FastAPI (#339) 2019-06-27 21:32:27 +02:00
Sebastián Ramírez
1e593dc4d4 📝 Update release notes 2019-06-27 20:53:54 +02:00
Sebastián Ramírez
dcc1e1bcf8 ♻️ Refine internal type declarations and logic around them (#338) 2019-06-27 20:51:17 +02:00
Sebastián Ramírez
06eb775c63 📝 Update release notes 2019-06-27 13:27:53 +02:00
Camila Gutierrez
ab77c069d4 📝 Update, simplify, and clarify the SQL tutorial (#331) 2019-06-27 13:25:16 +02:00
Sebastián Ramírez
46fffc0e94 📝 Update release notes 2019-06-27 13:22:56 +02:00
cyril
1c2cdb97e9 📝 Add online SQLite browsers to docs (#330) 2019-06-27 13:12:38 +02:00
Sebastián Ramírez
76b6fd5c18 🔖 Release 0.30.0 2019-06-20 12:32:24 +02:00
Sebastián Ramírez
a2fb716035 📝 Update release notes 2019-06-20 12:30:54 +02:00
Sebastián Ramírez
aa84ac8e3e Implement support for Pydantic's ORM mode (#322)
*  Implement support for Pydantic's ORM mode

* 🏗️ Re-structure/augment SQL tutorial source using ORM mode

* 📝 Update SQL docs with SQLAlchemy, ORM mode, relationships

* 🔥 Remove unused util in tutorial

* 📝 Add tutorials for simple dict bodies and responses

* 🔥 Remove old SQL tutorial

*  Add/update tests for SQL tutorial

*  Add tests for simple dicts (body and response)

* 🐛 Fix cloning field from original field
2019-06-20 11:31:32 +02:00
Sebastián Ramírez
4ed2bd1fea 📝 Update release notes 2019-06-18 13:54:36 +02:00
dmontagu
87b7a63ff2 🔥 Remove unused regex in routing.py (#314) 2019-06-18 13:52:34 +02:00
Sebastián Ramírez
06d0918c3d 📝 Update release notes 2019-06-18 09:49:43 +02:00
Eric Du
5b3adfe449 Use default response status reasons in additional responses (#313)
* default the description of additional response to status reason phrase

* fix 404 description

* fix lint warning

* allow custom response status code
2019-06-18 09:46:57 +02:00
Sebastián Ramírez
bdd794a0e6 📝 Update release notes 2019-06-18 09:40:31 +02:00
James Kaplan
f0df79aa91 ⬆️ Upgrade Pydantic to 0.28 (#320) 2019-06-18 09:37:40 +02:00
Sebastián Ramírez
c26f1760d4 🔖 Release 0.29.1 2019-06-13 18:47:43 +02:00
Sebastián Ramírez
e5fa4b0af6 📝 Update release notes 2019-06-13 18:39:17 +02:00
Sebastián Ramírez
a33c299fd7 🔧 Add format-imports script 2019-06-13 18:38:49 +02:00
Sebastián Ramírez
6939621730 bug: Fix handling an empty-body request with a required body param (#311)
* 🐛 Fix solving a required body param from an empty body request

*  Add tests for receiving required body parameters with body not provided
2019-06-13 18:37:48 +02:00
dmontagu
120ab08360 📝 Update response-directly.md, fix link (#306) 2019-06-13 18:34:20 +02:00
Andrew Widdersheim
3f5521fdfb 📝 Fix default response model docs (#288)
Fix a discrepancy in the `tax` parameters default value between the docs
and the code example.
2019-06-13 18:31:48 +02:00
Sebastián Ramírez
7244e4b612 🔖 Release version 0.29.0 2019-06-06 14:31:50 +04:00
Sebastián Ramírez
d329745064 📝 Update release notes 2019-06-06 14:30:28 +04:00
Sebastián Ramírez
5f7fe926ab Add support for Response parameters to set headers, cookies, and status codes (#294)
*  Add support for declaring a Response parameter to set headers and cookies

*  Add source for docs and tests

* 📝 Add docs for setting headers, cookies and status code

* 📝 Add attribution to Hug for inspiring response parameters
2019-06-06 14:29:40 +04:00
Sebastián Ramírez
c8eea09664 📝 Update release notes 2019-06-05 21:20:12 +04:00
Sebastián Ramírez
5700d65188 🔖 Release 0.28.0 2019-06-05 21:13:32 +04:00
Sebastián Ramírez
46178a5347 📝 Update release notes 2019-06-05 21:09:11 +04:00
Sebastián Ramírez
bff5dbbf5d Implement dependency value cache per request (#292)
*  Add dependency cache, with support for disabling it

*  Add tests for dependency cache

* 📝 Add docs about dependency value caching
2019-06-05 21:00:54 +04:00
Sebastián Ramírez
09cd7c47a1 Implement dependency overrides for testing (#291)
*  Implement dependency overrides for testing

*  Add docs source tests and extra tests for dependency overrides

* 📝 Add docs for testing dependencies with overrides
2019-06-05 15:43:18 +04:00
Sebastián Ramírez
e2fadcbc90 🔖 Release version 0.27.2 2019-06-03 22:03:24 +04:00
Sebastián Ramírez
b3bb29afa8 📝 Update relase notes 2019-06-03 22:01:09 +04:00
Sebastián Ramírez
c7db2ff858 🐛 Fix path and query parameters receiving dict as valid (#287)
* 🐛 Fix path and query parameters accepting dict

*  Add several tests to ensure invalid types are not accepted

* 📝 Document (to include tested source) using query params with list

* 🐛 Fix OpenAPI schema in query with list tutorial
2019-06-03 21:59:40 +04:00
Sebastián Ramírez
2a7ef5504a 🔖 Release 0.27.1 2019-06-03 18:44:03 +04:00
Sebastián Ramírez
27964c5ffd 📝 Update release notes 2019-06-01 10:00:26 +04:00
Sebastián Ramírez
d262f6e929 🐛 Fix HTTP Bearer security auto-error (#282) 2019-06-01 09:57:45 +04:00
Sebastián Ramírez
d61f5e4b55 📝 Update release notes 2019-05-30 19:43:32 +04:00
Sebastián Ramírez
3ed112e8a9 🐛 Fix type declaration of HTTPException (#279) 2019-05-30 19:43:02 +04:00
Sebastián Ramírez
9da626eb2c 🔖 Release version 0.27.0 2019-05-30 17:48:52 +04:00
Sebastián Ramírez
6f74c7327b 📝 Update release notes 2019-05-30 17:45:38 +04:00
dmontagu
360a2797c1 🐛 Fix docs link in oauth2-scopes.md (#275)
#274
2019-05-30 17:43:18 +04:00
Sebastián Ramírez
0552977cd6 📝 Update release notes 2019-05-30 17:41:40 +04:00
Sebastián Ramírez
bd407cc4ed Refactor param extraction using Pydantic Field (#278)
*  Refactor parameter dependency using Pydantic Field

* ⬆️ Upgrade required Pydantic version with latest Shape values

*  Add tutorials and code for using Enum and Optional

*  Add tests for tutorials with new types and extra cases

* ♻️ Format, clean, and add annotations to dependencies.utils

* 📝 Update tutorial for query parameters with list defaults

*  Add tests for query param with list default
2019-05-30 17:40:43 +04:00
Sebastián Ramírez
83b1a117cc 🔖 Release version 0.26.0 2019-05-29 19:29:44 +04:00
Sebastián Ramírez
2a1ff213a0 📝 Update release notes 2019-05-29 16:33:19 +04:00
Sebastián Ramírez
62af6e0eeb Separate Pydantic's ValidationError handler and improve docs for error handling (#273)
*  Implement separated ValidationError handlers and custom exceptions

*  Add tutorial source examples and tests

* 📝 Add docs for custom exception handlers

* 📝 Update docs section titles
2019-05-29 16:27:55 +04:00
Sebastián Ramírez
15da01af5c 📝 Update release notes 2019-05-29 13:46:27 +04:00
William Hayes
d544bdf092 📝 Update docs for paths in path params (#256) 2019-05-29 13:43:41 +04:00
Sebastián Ramírez
703ade7967 🐛 Fix path in path parameters (#272) 2019-05-29 13:34:46 +04:00
Sebastián Ramírez
58f135ba2f 📝 Update link in release notes 2019-05-29 11:51:43 +04:00
Sebastián Ramírez
713d374484 📝 Update release notes 2019-05-29 11:47:46 +04:00
Sebastián Ramírez
24e9ea28d3 Update testing docs, examples for testing POST, headers (#271) 2019-05-29 11:47:21 +04:00
Sebastián Ramírez
cae53138b2 📝 Update release notes 2019-05-27 21:56:49 +04:00
Sebastián Ramírez
a49d45eaa9 🐛 Fix response_model type to allow List[Model] (#266) 2019-05-27 21:56:20 +04:00
Sebastián Ramírez
3986f79029 🔖 Release version 0.25.0 2019-05-27 20:21:03 +04:00
Sebastián Ramírez
7379fde5ee 📝 Update release notes 2019-05-27 16:28:24 +04:00
Sebastián Ramírez
7b63bc5551 Add include, exclude, and by_alias to path operation methods (#264)
*  Make jsonable_encoder's include and exclude receive sequences

*  Add include, exclude, and by_alias to app and router

*  Add and update tutorial code with new parameters

* 📝 Update docs for new parameters and add docs for updating data

*  Add tests for consistency in path operation methods

*  Add tests for new parameters and update tests
2019-05-27 16:08:13 +04:00
Sebastián Ramírez
747ae8210f 📝 Update release notes 2019-05-25 19:48:27 +04:00
William Hayes
c651416e05 📝 Create CONTRIBUTING.md (#255) 2019-05-25 19:47:49 +04:00
Sebastián Ramírez
814f95e2bf 📝 Update release notes 2019-05-25 19:40:04 +04:00
William Hayes
d8716f94ae Add skip_defaults support for path operations (for #242) (#248) 2019-05-25 19:35:57 +04:00
Sebastián Ramírez
67f8cb3b4f 🔖 Release 0.24.0 2019-05-24 22:48:27 +04:00
Sebastián Ramírez
5c2828bd13 📝 Update release notes 2019-05-24 20:46:36 +04:00
James Kaplan
b087246f26 Add support for WebSockets with dependencies, params, etc #166 (#178) 2019-05-24 20:41:41 +04:00
Sebastián Ramírez
219d299426 📝 Update release notes 2019-05-23 11:04:13 +04:00
euri10
31da760729 ⬆️ Upgrade Pydantic to 0.26 (#247) 2019-05-23 11:03:53 +04:00
Sebastián Ramírez
fc89eb8f81 🔖 Release version 0.23.0 2019-05-21 23:26:28 +04:00
Sebastián Ramírez
6fca1041e9 📝 Update release notes 2019-05-21 23:05:46 +04:00
Sebastián Ramírez
9db1f5641b ⬆️ Upgrade Starlette to 0.12.0 (#243) 2019-05-21 22:59:48 +04:00
Sebastián Ramírez
c3beb56e63 📝 Update release notes 2019-05-21 22:46:22 +04:00
Steinthor Palsson
325edd5f00 Add swagger UI OAuth2 redirect page for implicit/code auth flows in API docs (#198) 2019-05-21 22:39:58 +04:00
Sebastián Ramírez
08322ef359 📝 Update release notes 2019-05-20 18:37:39 +04:00
Trim21
01b43e6e25 Make Swagger UI, ReDoc and OpenAPI handlers be coroutines to improve performance (#241) 2019-05-20 18:34:33 +04:00
Sebastián Ramírez
3cf92a156c 📝 Update release notes 2019-05-20 11:27:33 +04:00
euri10
f54d8d57a4 Make Swagger UI and ReDoc parameterizable to host offline assets for docs (#112) 2019-05-20 11:26:54 +04:00
Sebastián Ramírez
56ab106bbb 🔖 Release version 0.22.0 2019-05-16 18:09:11 +04:00
Sebastián Ramírez
e92b43b5c8 Add parameter dependencies to path operation decorators and include_router (#235)
*  Implement dependencies in decorator and .include_router

* 📝 Add docs for parameter dependencies

*  Add tests for dependencies parameter

* 🔥 Remove debugging prints in tests

* 📝 Update release notes
2019-05-16 18:07:00 +04:00
Sebastián Ramírez
7c50025c47 📝 Update release notes 2019-05-16 17:58:22 +04:00
euri10
adfbd27100 🐛 Fix OpenAPI URL format for Starlette convertors (#234) 2019-05-16 17:55:14 +04:00
Sebastián Ramírez
eada8bf7db 📝 Update release notes 2019-05-15 23:01:19 +04:00
Sebastián Ramírez
440b7a8efa 📝 Update relase notes 2019-05-15 22:14:25 +04:00
Sebastián Ramírez
fcaff64646 🔧 Separate format and lint scripts (#232) 2019-05-15 22:13:06 +04:00
Sebastián Ramírez
d240421378 📝 Add docs about params as functions for mypy (#231) 2019-05-15 22:01:23 +04:00
Sebastián Ramírez
ca27317b65 Add param functions, to override types, to make mypy happy (#226) 2019-05-15 21:25:11 +04:00
Sebastián Ramírez
ce02d3cb83 📝 Update release notes 2019-05-15 18:33:43 +04:00
Sebastián Ramírez
95475aaa9c 🔥 Remove Python version extraction in tests, no longer used 2019-05-15 18:33:13 +04:00
zamiramir
7a8b054a12 🎨 Reenable Black --check for Python 3.7 (#229)
Reenabled Black --check for python 3.7, issue is fixed.
see https://github.com/ambv/black/issues/494
2019-05-15 18:29:36 +04:00
Sebastián Ramírez
7b2993682f 🔖 Release 0.21.0 2019-05-15 14:54:46 +04:00
Sebastián Ramírez
73fad03b46 📝 Update release notes 2019-05-15 14:53:43 +04:00
Ricardo Momm
b0b88f9d5b 🔊 Raise from previous exception (#195) 2019-05-15 14:50:58 +04:00
Sebastián Ramírez
49d33f9f70 📝 Update release notes 2019-05-15 14:45:04 +04:00
Sebastián Ramírez
1f27981045 📝 Update release notes 2019-05-15 14:44:00 +04:00
euri10
f541d2c200 Use a logger instead of the root logging (#222) 2019-05-15 14:43:31 +04:00
euri10
0e99b23ebc ⬆️ Upgrade Pydantic to version 0.25 (#225) 2019-05-15 14:19:51 +04:00
Sebastián Ramírez
de341abe66 📝 Update release notes 2019-05-14 22:06:55 +04:00
Derek J. Lambert
4a1648b04e ✏️ Minor spelling fix in routing (#221) 2019-05-14 22:04:18 +04:00
Sebastián Ramírez
5f13b53ea5 🔧 Enable FastAPI releases bot in main Gitter channel 2019-05-11 19:44:39 +04:00
183 changed files with 8445 additions and 1759 deletions

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ coverage.xml
.netlify
test.db
log.txt
Pipfile.lock

View File

@@ -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

1
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1 @@
Please read the [Development - Contributing](https://fastapi.tiangolo.com/contributing/) guidelines in the documentation site.

View File

@@ -25,8 +25,8 @@ sqlalchemy = "*"
uvicorn = "*"
[packages]
starlette = "==0.11.1"
pydantic = "==0.23.0"
starlette = "==0.12.7"
pydantic = "==0.30.0"
databases = {extras = ["sqlite"],version = "*"}
hypercorn = "*"

941
Pipfile.lock generated
View File

@@ -1,941 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "6cb8497fe3811f43ce664ab69859187b1f698b8b75b0bde627207497ed422cde"
},
"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:1205cd1213e8acee40a9ad7160b24de74484fd79ec3f09150b255896a3f506ab",
"sha256:58b71804e9a6b4e1ccf8b3dbbca8c0f9cf4b494e5bea219a96e2e2ecb5af688e"
],
"index": "pypi",
"version": "==0.23.0"
},
"pytoml": {
"hashes": [
"sha256:ca2d0cb127c938b8b76a9a0d0f855cf930c1d50cc3a0af6d3595b566519a1013"
],
"version": "==0.1.20"
},
"sqlalchemy": {
"hashes": [
"sha256:91c54ca8345008fceaec987e10924bf07dcab36c442925357e5a467b36a38319"
],
"version": "==1.3.3"
},
"starlette": {
"hashes": [
"sha256:9d48b35d1fc7521d59ae53c421297ab3878d3c7cd4b75266d77f6c73cccb78bb"
],
"index": "pypi",
"version": "==0.11.1"
},
"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:029c69deaeeeae1b15bc6c59f0ffa28aa8473721c614a23f2c2976dec245cd12",
"sha256:02abbbebc6e9d5abe13cd28b5e963dedb6ffb51c146c916d17b18f141acd9947",
"sha256:1bbfe5b82a3921d285e999c6d256c1e16b31c554c29da62d326f86c173d30337",
"sha256:210c02f923df33a8d0e461c86fdcbbb17228ff4f6d92609fc06370a98d283c2d",
"sha256:2d0807ba935f540d20b49d5bf1c0237b90ce81e133402feda906e540003f2f7a",
"sha256:35d7a013874a7c927ce997350d314144ffc5465faf787bb4e46e6c4f381ef562",
"sha256:3636f9d0dcb01aed4180ef2e57a4e34bb4cac3ecd203c2a23db8526d86ab2fb4",
"sha256:42f4be770af2455a75e4640f033a82c62f3fb0d7a074123266e143269d7010ef",
"sha256:48440b25ba6cda72d4c638f3a9efa827b5b87b489c96ab5f4ff597d976413156",
"sha256:4dac8dfd1acf6a3ac657475dfdc66c621f291b1b7422a939cc33c13ac5356473",
"sha256:4e8474771c69c2991d5eab65764289a7dd450bbea050bc0ebb42b678d8222b42",
"sha256:551f10ddfeff56a1325e5a34eff304c5892aa981fd810babb98bfee77ee2fb17",
"sha256:5b104982f1809c1577912519eb249f17d9d7e66304ad026666cb60a5ef73309c",
"sha256:5c62aef73dfc87bfcca32cee149a1a7a602bc74bac72223236b0023543511c88",
"sha256:633151f8d1ad9467b9f7e90854a7f46ed8f2919e8bc7d98d737833e8938fc081",
"sha256:772207b9e2d5bf3f9d283b88915723e4e92d9a62c83f44ec92b9bd0cd685541b",
"sha256:7d5e02f647cd727afc2659ec14d4d1cc0508c47e6cfb07aea33d7aa9ca94d288",
"sha256:a9798a4111abb0f94584000ba2a2c74841f2cfe5f9254709756367aabbae0541",
"sha256:b38ea741ab9e35bfa7015c93c93bbd6a1623428f97a67083fc8ebd366238b91f",
"sha256:b6a5478c904236543c0347db8a05fac6fc0bd574c870e7970faa88e1d9890044",
"sha256:c6248bfc1de36a3844685a2e10ba17c18119ba6252547f921062a323fb31bff1",
"sha256:c705ab445936457359b1424ef25ccc0098b0491b26064677c39f1d14a539f056",
"sha256:d95a363d663ceee647291131dbd213af258df24f41350246842481ec3709bd33",
"sha256:e27265eb80cdc5dab55a40ef6f890e04ecc618649ad3da5265f128b141f93f78",
"sha256:ebc276c9cb5d917bd2ae959f84ffc279acafa9c9b50b0fa436ebb70bbe2166ea",
"sha256:f4d229866d030863d0fe3bf297d6d11e6133ca15bbb41ed2534a8b9a3d6bd061",
"sha256:f95675bd88b51474d4fe5165f3266f419ce754ffadfb97f10323931fa9ac95e5",
"sha256:f95bc54fb6d61b9f9ff09c4ae8ff6a3f5edc937cda3ca36fc937302a7c152bf1",
"sha256:fd0f6be53de40683584e5331c341e65a679dbe5ec489a0697cec7c2ef1a48cda"
],
"version": "==5.0a4"
},
"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:ddc4b5b59fa699bb10127adcf7ad4de78fde4ec539a072b104b8bb16da666ae5"
],
"index": "pypi",
"version": "==1.0.3"
},
"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"
],
"markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'pypy'",
"version": "==0.0.13"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"ipykernel": {
"hashes": [
"sha256:0aeb7ec277ac42cc2b59ae3d08b10909b2ec161dc6908096210527162b53675d",
"sha256:0fc0bf97920d454102168ec2008620066878848fcfca06c22b669696212e292f"
],
"version": "==5.1.0"
},
"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:01cb7e1ca5e6c5b3f235f0385057f70558b70d2f00320208825fa62887292f43",
"sha256:268067462aed7eb2a1e237fcb287852f22077de3fb07964e87e00f829eea2d1a"
],
"index": "pypi",
"version": "==4.3.17"
},
"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:29cadfabcedd12eed792e0131991235b9d4764d4474bed75cf525f57109ec0a2",
"sha256:e632a6cd1d349155c1d7f13a65be873b38f43ef02961804a1bba8d817fa649a7"
],
"version": "==2.6.0"
},
"markdown": {
"hashes": [
"sha256:fc4a6f69a656b8d858d7503bda633f4dd63c2d70cf80abdc6eafa64c4ae8c250",
"sha256:fe463ff51e679377e3624984c829022e2cfb3be5518726b06f608a07a3aad680"
],
"version": "==3.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:8a572f4b3358b9c0e11af8ae319ba4f3747ebb61e2393734d875133b0d2f7891",
"sha256:91210776db541283dd4b7beb5339c190aa69de78ad661aa116a8aa97dd73c803"
],
"index": "pypi",
"version": "==4.1.2"
},
"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:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f",
"sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746"
],
"version": "==0.9.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:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a",
"sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"
],
"version": "==2.3.1"
},
"pymdown-extensions": {
"hashes": [
"sha256:25b0a7967fa697b5035e23340a48594e3e93acb10b06d74574218ace3347d1df",
"sha256:6cf0cf36b5a03b291ace22dc2f320f4789ce56fbdb6635a3be5fadbf5d7694dd"
],
"version": "==6.0"
},
"pyrsistent": {
"hashes": [
"sha256:5403d37f4d55ff4572b5b5676890589f367a9569529c6f728c11046c4ea4272b"
],
"version": "==0.15.1"
},
"pytest": {
"hashes": [
"sha256:3773f4c235918987d51daf1db66d51c99fac654c81d6f2f709a046ab446d5e5d",
"sha256:b7802283b70ca24d7119b32915efa7c409982f59913c1a6c0640aacf118b95f5"
],
"index": "pypi",
"version": "==4.4.1"
},
"pytest-cov": {
"hashes": [
"sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33",
"sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f"
],
"index": "pypi",
"version": "==2.6.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:1ac4a65e81a27b0838330a6d351c2f8435d4013d98a95373e8a41119b2968390",
"sha256:bc1ba15f50c29ed50f1268ad823bb6543be263c18dd093b80495e9df63b003ac"
],
"version": "==4.4.3"
},
"requests": {
"hashes": [
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
],
"index": "pypi",
"version": "==2.21.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: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.4"
},
"ujson": {
"hashes": [
"sha256:f66073e5506e91d204ab0c614a148d5aa938bdbf104751be66f8ad7a222f5f86"
],
"index": "pypi",
"version": "==1.35"
},
"urllib3": {
"hashes": [
"sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0",
"sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3"
],
"version": "==1.24.2"
},
"uvicorn": {
"hashes": [
"sha256:181d47abddedd0f6e23eaeed97976bdce9ea1dbff0ec12385309cf4835783f6a"
],
"index": "pypi",
"version": "==0.7.0"
},
"uvloop": {
"hashes": [
"sha256:0fcd894f6fc3226a962ee7ad895c4f52e3f5c3c55098e21efb17c071849a0573",
"sha256:2f31de1742c059c96cb76b91c5275b22b22b965c886ee1fced093fa27dde9e64",
"sha256:459e4649fcd5ff719523de33964aa284898e55df62761e7773d088823ccbd3e0",
"sha256:67867aafd6e0bc2c30a079603a85d83b94f23c5593b3cc08ec7e58ac18bf48e5",
"sha256:8c200457e6847f28d8bb91c5e5039d301716f5f2fce25646f5fb3fd65eda4a26",
"sha256:958906b9ca39eb158414fbb7d6b8ef1b7aee4db5c8e8e5d00fcbb69a1ce9dca7",
"sha256:ac1dca3d8f3ef52806059e81042ee397ac939e5a86c8a3cea55d6b087db66115",
"sha256:b284c22d8938866318e3b9d178142b8be316c52d16fcfe1560685a686718a021",
"sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f",
"sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe"
],
"markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'pypy'",
"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"
}
}
}

View File

@@ -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)

View File

@@ -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.

58
docs/external-links.md Normal file
View File

@@ -0,0 +1,58 @@
**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">FastAPIGoogle 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">FastAPIHow 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>.
### Japanese
* <a href="https://qiita.com/mtitg/items/47770e9a562dd150631d" target="_blank">FastAPIDB接続して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>.

View File

@@ -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

View File

@@ -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

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -1,4 +1,275 @@
## Next release
## Latest changes
## 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
* Separate error handling for validation errors.
* This will allow developers to customize the exception handlers.
* Document better how to handle exceptions and use error handlers.
* Include `RequestValidationError` and `WebSocketRequestValidationError` (this last one will be useful once [encode/starlette#527](https://github.com/encode/starlette/pull/527) or equivalent is merged).
* New documentation about exceptions handlers:
* [Install custom exception handlers](https://fastapi.tiangolo.com/tutorial/handling-errors/#install-custom-exception-handlers).
* [Override the default exception handlers](https://fastapi.tiangolo.com/tutorial/handling-errors/#override-the-default-exception-handlers).
* [Re-use **FastAPI's** exception handlers](https://fastapi.tiangolo.com/tutorial/handling-errors/#re-use-fastapis-exception-handlers).
* PR [#273](https://github.com/tiangolo/fastapi/pull/273).
* Fix support for *paths* in *path parameters* without needing explicit `Path(...)`.
* PR [#256](https://github.com/tiangolo/fastapi/pull/256).
* Documented in PR [#272](https://github.com/tiangolo/fastapi/pull/272) by [@wshayes](https://github.com/wshayes).
* New documentation at: [Path Parameters containing paths](https://fastapi.tiangolo.com/tutorial/path-params/#path-parameters-containing-paths).
* Update docs for testing FastAPI. Include using `POST`, sending JSON, testing headers, etc. New documentation: [Testing](https://fastapi.tiangolo.com/tutorial/testing/#testing-extended-example). PR [#271](https://github.com/tiangolo/fastapi/pull/271).
* Fix type declaration of `response_model` to allow generic Python types as `List[Model]`. Mainly to fix `mypy` for users. PR [#266](https://github.com/tiangolo/fastapi/pull/266).
## 0.25.0
* Add support for Pydantic's `include`, `exclude`, `by_alias`.
* Update documentation: [Response Model](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude).
* Add docs for: [Body - updates](https://fastapi.tiangolo.com/tutorial/body-updates/), using Pydantic's `skip_defaults`.
* Add method consistency tests.
* PR [#264](https://github.com/tiangolo/fastapi/pull/264).
* Add `CONTRIBUTING.md` file to GitHub, to help new contributors. PR [#255](https://github.com/tiangolo/fastapi/pull/255) by [@wshayes](https://github.com/wshayes).
* Add support for Pydantic's `skip_defaults`:
* There's a new *path operation decorator* parameter `response_model_skip_defaults`.
* The name of the parameter will most probably change in a future version to `response_skip_defaults`, `model_skip_defaults` or something similar.
* New [documentation section about using `response_model_skip_defaults`](https://fastapi.tiangolo.com/tutorial/response-model/#response-model-encoding-parameters).
* PR [#248](https://github.com/tiangolo/fastapi/pull/248) by [@wshayes](https://github.com/wshayes).
## 0.24.0
* Add support for WebSockets with dependencies and parameters.
* Support included for:
* `Depends`
* `Security`
* `Cookie`
* `Header`
* `Path`
* `Query`
* ...as these are compatible with the WebSockets protocol (e.g. `Body` is not).
* [Updated documentation for WebSockets](https://fastapi.tiangolo.com/tutorial/websockets/).
* PR [#178](https://github.com/tiangolo/fastapi/pull/178) by [@jekirl](https://github.com/jekirl).
* Upgrade the compatible version of Pydantic to `0.26.0`.
* This includes JSON Schema support for IP address and network objects, bug fixes, and other features.
* PR [#247](https://github.com/tiangolo/fastapi/pull/247) by [@euri10](https://github.com/euri10).
## 0.23.0
* Upgrade the compatible version of Starlette to `0.12.0`.
* This includes support for ASGI 3 (the latest version of the standard).
* It's now possible to use [Starlette's `StreamingResponse`](https://www.starlette.io/responses/#streamingresponse) with iterators, like [file-like](https://docs.python.org/3/glossary.html#term-file-like-object) objects (as those returned by `open()`).
* It's now possible to use the low level utility `iterate_in_threadpool` from `starlette.concurrency` (for advanced scenarios).
* PR [#243](https://github.com/tiangolo/fastapi/pull/243).
* Add OAuth2 redirect page for Swagger UI. This allows having delegated authentication in the Swagger UI docs. For this to work, you need to add `{your_origin}/docs/oauth2-redirect` to the allowed callbacks in your OAuth2 provider (in Auth0, Facebook, Google, etc).
* For example, during development, it could be `http://localhost:8000/docs/oauth2-redirect`.
* Have in mind that this callback URL is independent of whichever one is used by your frontend. You might also have another callback at `https://yourdomain.com/login/callback`.
* This is only to allow delegated authentication in the API docs with Swagger UI.
* PR [#198](https://github.com/tiangolo/fastapi/pull/198) by [@steinitzu](https://github.com/steinitzu).
* Make Swagger UI and ReDoc route handlers (*path operations*) be `async` functions instead of lambdas to improve performance. PR [#241](https://github.com/tiangolo/fastapi/pull/241) by [@Trim21](https://github.com/Trim21).
* Make Swagger UI and ReDoc URLs parameterizable, allowing to host and serve local versions of them and have offline docs. PR [#112](https://github.com/tiangolo/fastapi/pull/112) by [@euri10](https://github.com/euri10).
## 0.22.0
* Add support for `dependencies` parameter:
* A parameter in *path operation decorators*, for dependencies that should be executed but the return value is not important or not used in the *path operation function*.
* A parameter in the `.include_router()` method of FastAPI applications and routers, to include dependencies that should be executed in each *path operation* in a router.
* This is useful, for example, to require authentication or permissions in specific group of *path operations*.
* Different `dependencies` can be applied to different routers.
* These `dependencies` are run before the normal parameter dependencies. And normal dependencies are run too. They can be combined.
* Dependencies declared in a router are executed first, then the ones defined in *path operation decorators*, and then the ones declared in normal parameters. They are all combined and executed.
* All this also supports using `Security` with `scopes` in those `dependencies` parameters, for more advanced OAuth 2.0 security scenarios with scopes.
* New documentation about [dependencies in *path operation decorators*](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/).
* New documentation about [dependencies in the `include_router()` method](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-prefix-tags-responses-and-dependencies).
* PR [#235](https://github.com/tiangolo/fastapi/pull/235).
* Fix OpenAPI documentation of Starlette URL convertors. Specially useful when using `path` convertors, to take a whole path as a parameter, like `/some/url/{p:path}`. PR [#234](https://github.com/tiangolo/fastapi/pull/234) by [@euri10](https://github.com/euri10).
* Make default parameter utilities exported from `fastapi` be functions instead of classes (the new functions return instances of those classes). To be able to override the return types and fix `mypy` errors in FastAPI's users' code. Applies to `Path`, `Query`, `Header`, `Cookie`, `Body`, `Form`, `File`, `Depends`, and `Security`. PR [#226](https://github.com/tiangolo/fastapi/pull/226) and PR [#231](https://github.com/tiangolo/fastapi/pull/231).
* Separate development scripts `test.sh`, `lint.sh`, and `format.sh`. PR [#232](https://github.com/tiangolo/fastapi/pull/232).
* Re-enable `black` formatting checks for Python 3.7. PR [#229](https://github.com/tiangolo/fastapi/pull/229) by [@zamiramir](https://github.com/zamiramir).
## 0.21.0
* On body parsing errors, raise `from` previous exception, to allow better introspection in logging code. PR [#192](https://github.com/tiangolo/fastapi/pull/195) by [@ricardomomm](https://github.com/ricardomomm).
* Use Python logger named "`fastapi`" instead of root logger. PR [#222](https://github.com/tiangolo/fastapi/pull/222) by [@euri10](https://github.com/euri10).
* Upgrade Pydantic to version 0.25. PR [#225](https://github.com/tiangolo/fastapi/pull/225) by [@euri10](https://github.com/euri10).
* Fix typo in routing. PR [#221](https://github.com/tiangolo/fastapi/pull/221) by [@djlambert](https://github.com/djlambert).
## 0.20.1

View File

@@ -0,0 +1,36 @@
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
fake_secret_token = "coneofsilence"
fake_db = {
"foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
"bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}
app = FastAPI()
class Item(BaseModel):
id: str
title: str
description: str = None
@app.get("/items/{item_id}", response_model=Item)
async def read_main(item_id: str, x_token: str = Header(...)):
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="Invalid X-Token header")
if item_id not in fake_db:
raise HTTPException(status_code=404, detail="Item not found")
return fake_db[item_id]
@app.post("/items/", response_model=Item)
async def create_item(item: Item, x_token: str = Header(...)):
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="Invalid X-Token header")
if item.id in fake_db:
raise HTTPException(status_code=400, detail="Item already exists")
fake_db[item.id] = item
return item

View File

@@ -0,0 +1,65 @@
from starlette.testclient import TestClient
from .main_b import app
client = TestClient(app)
def test_read_item():
response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})
assert response.status_code == 200
assert response.json() == {
"id": "foo",
"title": "Foo",
"description": "There goes my hero",
}
def test_read_item_bad_token():
response = client.get("/items/foo", headers={"X-Token": "hailhydra"})
assert response.status_code == 400
assert response.json() == {"detail": "Invalid X-Token header"}
def test_read_inexistent_item():
response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
def test_create_item():
response = client.post(
"/items/",
headers={"X-Token": "coneofsilence"},
json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},
)
assert response.status_code == 200
assert response.json() == {
"id": "foobar",
"title": "Foo Bar",
"description": "The Foo Barters",
}
def test_create_item_bad_token():
response = client.post(
"/items/",
headers={"X-Token": "hailhydra"},
json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},
)
assert response.status_code == 400
assert response.json() == {"detail": "Invalid X-Token header"}
def test_create_existing_token():
response = client.post(
"/items/",
headers={"X-Token": "coneofsilence"},
json={
"id": "foo",
"title": "The Foo ID Stealers",
"description": "There goes my stealer",
},
)
assert response.status_code == 400
assert response.json() == {"detail": "Item already exists"}

View File

@@ -1,13 +1,20 @@
from fastapi import FastAPI
from fastapi import Depends, FastAPI, Header, HTTPException
from .routers import items, users
app = FastAPI()
async def get_token_header(x_token: str = Header(...)):
if x_token != "fake-super-secret-token":
raise HTTPException(status_code=400, detail="X-Token header invalid")
app.include_router(users.router)
app.include_router(
items.router,
prefix="/items",
tags=["items"],
dependencies=[Depends(get_token_header)],
responses={404: {"description": "Not found"}},
)

View 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

View File

@@ -0,0 +1,34 @@
from typing import List
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str = None
description: str = None
price: float = None
tax: float = 10.5
tags: List[str] = []
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}
@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: str):
return items[item_id]
@app.put("/items/{item_id}", response_model=Item)
async def update_item(item_id: str, item: Item):
update_item_encoded = jsonable_encoder(item)
items[item_id] = update_item_encoded
return update_item_encoded

View File

@@ -0,0 +1,37 @@
from typing import List
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str = None
description: str = None
price: float = None
tax: float = 10.5
tags: List[str] = []
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}
@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: str):
return items[item_id]
@app.patch("/items/{item_id}", response_model=Item)
async def update_item(item_id: str, item: Item):
stored_item_data = items[item_id]
stored_item_model = Item(**stored_item_data)
update_data = item.dict(skip_defaults=True)
updated_item = stored_item_model.copy(update=update_data)
items[item_id] = jsonable_encoder(updated_item)
return updated_item

View File

@@ -1,21 +1,19 @@
from fastapi import Depends, FastAPI
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
async def verify_token(x_token: str = Header(...)):
if x_token != "fake-super-secret-token":
raise HTTPException(status_code=400, detail="X-Token header invalid")
checker = FixedContentQueryChecker("bar")
async def verify_key(x_key: str = Header(...)):
if x_key != "fake-super-secret-key":
raise HTTPException(status_code=400, detail="X-Key header invalid")
return x_key
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: bool = Depends(checker)):
return {"fixed_content_in_query": fixed_content_included}
@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
return [{"item": "Foo"}, {"item": "Bar"}]

View File

@@ -0,0 +1,21 @@
from fastapi import Depends, FastAPI
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: bool = Depends(checker)):
return {"fixed_content_in_query": fixed_content_included}

View 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},
}

View 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}

View File

@@ -1,15 +1,26 @@
from fastapi import FastAPI
from starlette.exceptions import HTTPException
from starlette.responses import PlainTextResponse
from starlette.requests import Request
from starlette.responses import JSONResponse
class UnicornException(Exception):
def __init__(self, name: str):
self.name = name
app = FastAPI()
@app.exception_handler(HTTPException)
async def http_exception(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
return JSONResponse(
status_code=418,
content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
)
@app.get("/")
async def root():
return {"message": "Hello World"}
@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
if name == "yolo":
raise UnicornException(name=name)
return {"unicorn_name": name}

View File

@@ -0,0 +1,23 @@
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import PlainTextResponse
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return PlainTextResponse(str(exc), status_code=400)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}

View File

@@ -0,0 +1,28 @@
from fastapi import FastAPI, HTTPException
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def custom_http_exception_handler(request, exc):
print(f"OMG! An HTTP error!: {exc}")
return await http_exception_handler(request, exc)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
print(f"OMG! The client sent invalid data!: {exc}")
return await request_validation_exception_handler(request, exc)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}

View File

@@ -0,0 +1,8 @@
from fastapi import FastAPI
app = FastAPI()
@app.get("/files/{file_path:path}")
async def read_user_me(file_path: str):
return {"file_path": file_path}

View 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"}

View File

@@ -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]

View 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

View 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

View 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

View 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]

View 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"}

View 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"}

View File

@@ -1,4 +1,4 @@
from typing import Set
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
@@ -11,9 +11,9 @@ class Item(BaseModel):
description: str = None
price: float
tax: float = None
tags: Set[str] = []
tags: List[str] = []
@app.post("/items/", response_model=Item)
async def create_item(*, item: Item):
async def create_item(item: Item):
return item

View File

@@ -0,0 +1,26 @@
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = 10.5
tags: List[str] = []
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}
@app.get("/items/{item_id}", response_model=Item, response_model_skip_defaults=True)
async def read_item(item_id: str):
return items[item_id]

View File

@@ -0,0 +1,37 @@
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = 10.5
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
"baz": {
"name": "Baz",
"description": "There goes my baz",
"price": 50.2,
"tax": 10.5,
},
}
@app.get(
"/items/{item_id}/name",
response_model=Item,
response_model_include={"name", "description"},
)
async def read_item_name(item_id: str):
return items[item_id]
@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"})
async def read_item_public_data(item_id: str):
return items[item_id]

View File

@@ -0,0 +1,37 @@
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = 10.5
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
"baz": {
"name": "Baz",
"description": "There goes my baz",
"price": 50.2,
"tax": 10.5,
},
}
@app.get(
"/items/{item_id}/name",
response_model=Item,
response_model_include=["name", "description"],
)
async def read_item_name(item_id: str):
return items[item_id]
@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude=["tax"])
async def read_item_public_data(item_id: str):
return items[item_id]

View File

View File

View 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

View 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()

View 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

View 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")

View 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

View File

@@ -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

View File

View File

@@ -44,10 +44,9 @@ async def get():
return HTMLResponse(html)
@app.websocket_route("/ws")
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message text was: {data}")
await websocket.close()

View File

@@ -0,0 +1,78 @@
from fastapi import Cookie, Depends, FastAPI, Header
from starlette.responses import HTMLResponse
from starlette.status import WS_1008_POLICY_VIOLATION
from starlette.websockets import WebSocket
app = FastAPI()
html = """
<!DOCTYPE html>
<html>
<head>
<title>Chat</title>
</head>
<body>
<h1>WebSocket Chat</h1>
<form action="" onsubmit="sendMessage(event)">
<label>Item ID: <input type="text" id="itemId" autocomplete="off" value="foo"/></label>
<button onclick="connect(event)">Connect</button>
<br>
<label>Message: <input type="text" id="messageText" autocomplete="off"/></label>
<button>Send</button>
</form>
<ul id='messages'>
</ul>
<script>
var ws = null;
function connect(event) {
var input = document.getElementById("itemId")
ws = new WebSocket("ws://localhost:8000/items/" + input.value + "/ws");
ws.onmessage = function(event) {
var messages = document.getElementById('messages')
var message = document.createElement('li')
var content = document.createTextNode(event.data)
message.appendChild(content)
messages.appendChild(message)
};
}
function sendMessage(event) {
var input = document.getElementById("messageText")
ws.send(input.value)
input.value = ''
event.preventDefault()
}
</script>
</body>
</html>
"""
@app.get("/")
async def get():
return HTMLResponse(html)
async def get_cookie_or_client(
websocket: WebSocket, session: str = Cookie(None), x_client: str = Header(None)
):
if session is None and x_client is None:
await websocket.close(code=WS_1008_POLICY_VIOLATION)
return session or x_client
@app.websocket("/items/{item_id}/ws")
async def websocket_endpoint(
websocket: WebSocket,
item_id: int,
q: str = None,
cookie_or_client: str = Depends(get_cookie_or_client),
):
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(
f"Session Cookie or X-Client Header value is: {cookie_or_client}"
)
if q is not None:
await websocket.send_text(f"Query parameter q is: {q}")
await websocket.send_text(f"Message text was: {data}, for item ID: {item_id}")

View File

@@ -22,16 +22,15 @@ Let's say you have a file structure like this:
!!! tip
There are two `__init__.py` files: one in each directory or subdirectory.
This is what allows importing code from one file into another.
For example, in `app/main.py` you could have a line like:
```
from app.routers import items
```
* The `app` directory contains everything.
* This `app` directory has an empty file `app/__init__.py`.
* So, the `app` directory is a "Python package" (a collection of "Python modules").
@@ -107,7 +106,7 @@ And we don't want to have to explicitly type `/items/` and `tags=["items"]` in e
{!./src/bigger_applications/app/routers/items.py!}
```
### Add some custom `tags` and `responses`
### Add some custom `tags`, `responses`, and `dependencies`
We are not adding the prefix `/items/` nor the `tags=["items"]` to add them later.
@@ -175,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`.
@@ -197,12 +195,11 @@ So, to be able to use both of them in the same file, we import the submodules di
{!./src/bigger_applications/app/main.py!}
```
### Include an `APIRouter`
Now, let's include the `router` from the submodule `users`:
```Python hl_lines="7"
```Python hl_lines="13"
{!./src/bigger_applications/app/main.py!}
```
@@ -218,16 +215,14 @@ 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.
This will take microseconds and will only happen at startup.
So it won't affect performance.
### Include an `APIRouter` with a `prefix`, `tags`, and `responses`
### Include an `APIRouter` with a `prefix`, `tags`, `responses`, and `dependencies`
Now, let's include the router from the `items` submodule.
@@ -251,7 +246,9 @@ We can also add a list of `tags` that will be applied to all the *path operation
And we can add predefined `responses` that will be included in all the *path operations* too.
```Python hl_lines="8 9 10 11 12 13"
And we can add a list of `dependencies` that will be added to all the *path operations* in the router and will be executed/solved for each request made to them.
```Python hl_lines="8 9 10 14 15 16 17 18 19 20"
{!./src/bigger_applications/app/main.py!}
```
@@ -262,27 +259,28 @@ The end result is that the item paths are now:
...as we intended.
They will be marked with a list of tags that contain a single string `"items"`.
* They will be marked with a list of tags that contain a single string `"items"`.
* The *path operation* that declared a `"custom"` tag will have both tags, `items` and `custom`.
* These "tags" are especially useful for the automatic interactive documentation systems (using OpenAPI).
* All of them will include the predefined `responses`.
* The *path operation* that declared a custom `403` response will have both the predefined responses (`404`) and the `403` declared in it directly.
* All these *path operations* will have the list of `dependencies` evaluated/executed before them.
* If you also declare dependencies in a specific *path operation*, **they will be executed too**.
* The router dependencies are executed first, then the <a href="https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-decorator/" target="_blank">`dependencies` in the decorator</a>, and then the normal parameter dependencies.
* You can also add <a href="https://fastapi.tiangolo.com/tutorial/security/oauth2-scopes/" target="_blank">`Security` dependencies with `scopes`</a>.
The *path operation* that declared a `"custom"` tag will have both tags, `items` and `custom`.
These "tags" are especially useful for the automatic interactive documentation systems (using OpenAPI).
And all of them will include the the predefined `responses`.
The *path operation* that declared a custom `403` response will have both the predefined responses (`404`) and the `403` declared in it directly.
!!! tip
Having `dependencies` in a decorator can be used, for example, to require authentication for a whole group of *path operations*. Even if the dependencies are not added individually to each one of them.
!!! check
The `prefix`, `tags`, and `responses` parameters are (as in many other cases) just a feature from **FastAPI** to help you avoid code duplication.
The `prefix`, `tags`, `responses` and `dependencies` parameters are (as in many other cases) just a feature from **FastAPI** to help you avoid code duplication.
!!! tip
You could also add path operations directly, for example with: `@app.get(...)`.
Apart from `app.include_router()`, in the same **FastAPI** app.
It would still work the same.
Apart from `app.include_router()`, in the same **FastAPI** app.
It would still work the same.
!!! info "Very Technical Details"
**Note**: this is a very technical detail that you probably can **just skip**.
@@ -295,7 +293,6 @@ The *path operation* that declared a custom `403` response will have both the pr
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.

View File

@@ -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.

View File

@@ -22,12 +22,13 @@ You can then use `Schema` with model attributes:
`Schema` works the same way as `Query`, `Path` and `Body`, it has all the same parameters, etc.
!!! info
!!! note "Technical Details"
Actually, `Query`, `Path` and others you'll see next are subclasses of a common `Param` which is itself a subclass of Pydantic's `Schema`.
`Body` is also a subclass of `Schema` directly. And there are others you will see later that are subclasses of `Body`.
But remember that when you import `Query`, `Path` and others from `fastapi`, <a href="https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#recap" target="_blank">those are actually functions that return classes of the same name</a>.
!!! tip
Notice how each model's attribute with a type, default value and `Schema` has the same structure as a path operation function's parameter, with `Schema` instead of `Path`, `Query` and `Body`.

View File

@@ -0,0 +1,97 @@
## Update replacing with `PUT`
To update an item you can use the [HTTP `PUT`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT) operation.
You can use the `jsonable_encoder` to convert the input data to data that can be stored as JSON (e.g. with a NoSQL database). For example, converting `datetime` to `str`.
```Python hl_lines="30 31 32 33 34 35"
{!./src/body_updates/tutorial001.py!}
```
`PUT` is used to receive data that should replace the existing data.
### Warning about replacing
That means that if you want to update the item `bar` using `PUT` with a body containing:
```Python
{
"name": "Barz",
"price": 3,
"description": None,
}
```
because it doesn't include the already stored attribute `"tax": 20.2`, the input model would take the default value of `"tax": 10.5`.
And the data would be saved with that "new" `tax` of `10.5`.
## Partial updates with `PATCH`
You can also use the [HTTP `PATCH`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH) operation to *partially* update data.
This means that you can send only the data that you want to update, leaving the rest intact.
!!! Note
`PATCH` is less commonly used and known than `PUT`.
And many teams use only `PUT`, even for partial updates.
You are **free** to use them however you want, **FastAPI** doesn't impose any restrictions.
But this guide shows you, more or less, how they are intended to be used.
### Using Pydantic's `skip_defaults` parameter
If you want to receive partial updates, it's very useful to use the parameter `skip_defaults` in Pydantic's model's `.dict()`.
Like `item.dict(skip_defaults=True)`.
That would generate a `dict` with only the data that was set when creating the `item` model, excluding default values.
Then you can use this to generate a `dict` with only the data that was set, omitting default values:
```Python hl_lines="34"
{!./src/body_updates/tutorial002.py!}
```
### Using Pydantic's `update` parameter
Now, you can create a copy of the existing model using `.copy()`, and pass the `update` parameter with a `dict` containing the data to update.
Like `stored_item_model.copy(update=update_data)`:
```Python hl_lines="35"
{!./src/body_updates/tutorial002.py!}
```
### Partial updates recap
In summary, to apply partial updates you would:
* (Optionally) use `PATCH` instead of `PUT`.
* Retrieve the stored data.
* Put that data in a Pydantic model.
* Generate a `dict` without default values from the input model (using `skip_defaults`).
* This way you can update only the values actually set by the user, instead of overriding values already stored with default values in your model.
* Create a copy of the stored model, updating it's attributes with the received partial updates (using the `update` parameter).
* Convert the copied model to something that can be stored in your DB (for example, using the `jsonable_encoder`).
* This is comparable to using the model's `.dict()` method again, but it makes sure (and converts) the values to data types that can be converted to JSON, for example, `datetime` to `str`.
* Save the data to your DB.
* Return the updated model.
```Python hl_lines="30 31 32 33 34 35 36 37"
{!./src/body_updates/tutorial002.py!}
```
!!! tip
You can actually use this same technique with an HTTP `PUT` operation.
But the example here uses `PATCH` because it was created for these use cases.
!!! note
Notice that the input model is still validated.
So, if you want to receive partial updates that can omit all the attributes, you need to have a model with all the attributes marked as optional (with default values or `None`).
To distinguish from the models with all optional values for **updates** and models with required values for **creation**, you can use the ideas described in <a href="https://fastapi.tiangolo.com/tutorial/extra-models/" target="_blank">Extra Models</a>.

View File

@@ -18,9 +18,11 @@ The first value is the default value, you can pass all the extra validation or a
{!./src/cookie_params/tutorial001.py!}
```
!!! info
!!! note "Technical Details"
`Cookie` is a "sister" class of `Path` and `Query`. It also inherits from the same common `Param` class.
But remember that when you import `Query`, `Path`, `Cookie` and others from `fastapi`, <a href="https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#recap" target="_blank">those are actually functions that return classes of the same name</a>.
!!! info
To declare cookies, you need to use `Cookie`, because otherwise the parameters would be interpreted as query parameters.

View File

@@ -22,7 +22,7 @@ Not the class itself (which is already a callable), but an instance of that clas
To do that, we declare a method `__call__`:
```Python hl_lines="10"
{!./src/dependencies/tutorial006.py!}
{!./src/dependencies/tutorial007.py!}
```
In this case, this `__call__` is what **FastAPI** will use to check for additional parameters and sub-dependencies, and this is what will be called to pass a value to the parameter in your *path operation function* later.
@@ -32,7 +32,7 @@ In this case, this `__call__` is what **FastAPI** will use to check for addition
And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency:
```Python hl_lines="7"
{!./src/dependencies/tutorial006.py!}
{!./src/dependencies/tutorial007.py!}
```
In this case, **FastAPI** won't ever touch or care about `__init__`, we will use it directly in our code.
@@ -42,7 +42,7 @@ In this case, **FastAPI** won't ever touch or care about `__init__`, we will use
We could create an instance of this class with:
```Python hl_lines="16"
{!./src/dependencies/tutorial006.py!}
{!./src/dependencies/tutorial007.py!}
```
And that way we are able to "parameterize" our dependency, that now has `"bar"` inside of it, as the attribute `checker.fixed_content`.
@@ -60,7 +60,7 @@ checker(q="somequery")
...and pass whatever that returns as the value of the dependency in our path operation function as the parameter `fixed_content_included`:
```Python hl_lines="20"
{!./src/dependencies/tutorial006.py!}
{!./src/dependencies/tutorial007.py!}
```
!!! tip

View File

@@ -0,0 +1,60 @@
In some cases you don't really need the return value of a dependency inside your *path operation function*.
Or the dependency doesn't return a value.
But you still need it to be executed/solved.
For those cases, instead of declaring a *path operation function* parameter with `Depends`, you can add a `list` of `dependencies` to the *path operation decorator*.
## Add `dependencies` to the *path operation decorator*
The *path operation decorator* receives an optional argument `dependencies`.
It should be a `list` of `Depends()`:
```Python hl_lines="17"
{!./src/dependencies/tutorial006.py!}
```
These dependencies will be executed/solved the same way normal dependencies. But their value (if they return any) won't be passed to your *path operation function*.
!!! tip
Some editors check for unused function parameters, and show them as errors.
Using these `dependencies` in the *path operation decorator* you can make sure they are executed while avoiding editor/tooling errors.
It might also help avoiding confusion for new developers that see an un-used parameter in your code and could think it's unnecessary.
## Dependencies errors and return values
You can use the same dependency *functions* you use normally.
### Dependency requirements
They can declare request requirements (like headers) or other sub-dependencies:
```Python hl_lines="6 11"
{!./src/dependencies/tutorial006.py!}
```
### Raise exceptions
These dependencies can `raise` exceptions, the same as normal dependencies:
```Python hl_lines="8 13"
{!./src/dependencies/tutorial006.py!}
```
### Return values
And they can return values or not, the values won't be used.
So, you can re-use a normal dependency (that returns a value) you already use somewhere else, and even though the value won't be used, the dependency will be executed:
```Python hl_lines="9 14"
{!./src/dependencies/tutorial006.py!}
```
## Dependencies for a group of *path operations*
Later, when reading about how to <a href="https://fastapi.tiangolo.com/tutorial/bigger-applications/" target="_blank">structure bigger applications</a>, possibly with multiple files, you will learn how to declare a single `dependencies` parameter for a group of *path operations*.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -68,7 +68,7 @@ But if the client requests `http://example.com/items/bar` (a non-existent `item_
They are handled automatically by **FastAPI** and converted to JSON.
### Adding custom headers
## Add custom headers
There are some situations in where it's useful to be able to add custom headers to the HTTP error. For example, for some types of security.
@@ -76,24 +76,138 @@ You probably won't need to use it directly in your code.
But in case you needed it for an advanced scenario, you can add custom headers:
```Python hl_lines="14"
{!./src/handling_errors/tutorial002.py!}
```
### Installing custom handlers
## Install custom exception handlers
If you need to add other custom exception handlers, or override the default one (that sends the errors as JSON), you can use <a href="https://www.starlette.io/exceptions/" target="_blank">the same exception utilities from Starlette</a>.
You can add custom exception handlers with <a href="https://www.starlette.io/exceptions/" target="_blank">the same exception utilities from Starlette</a>.
For example, you could override the default exception handler with:
Let's say you have a custom exception `UnicornException` that you (or a library you use) might `raise`.
```Python hl_lines="2 3 8 9 10"
And you want to handle this exception globally with FastAPI.
You could add a custom exception handler with `@app.exception_handler()`:
```Python hl_lines="6 7 8 14 15 16 17 18 24"
{!./src/handling_errors/tutorial003.py!}
```
...this would make it return "plain text" responses with the errors, instead of JSON responses.
Here, if you request `/unicorns/yolo`, the *path operation* will `raise` a `UnicornException`.
!!! info
Note that in this example we set the exception handler with Starlette's `HTTPException` instead of FastAPI's `HTTPException`.
But it will be handled by the `unicorn_exception_handler`.
This would ensure that if you use a plug-in or any other third-party tool that raises Starlette's `HTTPException` directly, it will be caught by your exception handler.
So, you will receive a clean error, with an HTTP status code of `418` and a JSON content of:
```JSON
{"message": "Oops! yolo did something. There goes a rainbow..."}
```
## Override the default exception handlers
**FastAPI** has some default exception handlers.
These handlers are in charge or returning the default JSON responses when you `raise` an `HTTPException` and when the request has invalid data.
You can override these exception handlers with your own.
### Override request validation exceptions
When a request contains invalid data, **FastAPI** internally raises a `RequestValidationError`.
And it also includes a default exception handler for it.
To override it, import the `RequestValidationError` and use it with `@app.exception_handler(RequestValidationError)` to decorate the exception handler.
The exception handler will receive a `Request` and the exception.
```Python hl_lines="2 14 15 16"
{!./src/handling_errors/tutorial004.py!}
```
Now, if you go to `/items/foo`, instead of getting the default JSON error with:
```JSON
{
"detail": [
{
"loc": [
"path",
"item_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
```
you will get a text version, with:
```
1 validation error
path -> item_id
value is not a valid integer (type=type_error.integer)
```
#### `RequestValidationError` vs `ValidationError`
!!! warning
These are technical details that you might skip if it's not important for you now.
`RequestValidationError` is a sub-class of Pydantic's <a href="https://pydantic-docs.helpmanual.io/#error-handling" target="_blank">`ValidationError`</a>.
**FastAPI** uses it so that, if you use a Pydantic model in `response_model`, and your data has an error, you will see the error in your log.
But the client/user will not see it. Instead, the client will receive an "Internal Server Error" with a HTTP status code `500`.
It should be this way because if you have a Pydantic `ValidationError` in your *response* or anywhere in your code (not in the client's *request*), it's actually a bug in your code.
And while you fix it, your clients/users shouldn't have access to internal information about the error, as that could expose a security vulnerability.
### Override the `HTTPException` error handler
The same way, you can override the `HTTPException` handler.
For example, you could want to return a plain text response instead of JSON for these errors:
```Python hl_lines="1 3 9 10 11 22"
{!./src/handling_errors/tutorial004.py!}
```
#### FastAPI's `HTTPException` vs Starlette's `HTTPException`
**FastAPI** has its own `HTTPException`.
And **FastAPI**'s `HTTPException` error class inherits from Starlette's `HTTPException` error class.
The only difference, is that **FastAPI**'s `HTTPException` allows you to add headers to be included in the response.
This is needed/used internally for OAuth 2.0 and some security utilities.
So, you can keep raising **FastAPI**'s `HTTPException` as normally in your code.
But when you register an exception handler, you should register it for Starlette's `HTTPException`.
This way, if any part of Starlette's internal code, or a Starlette extension or plug-in, raises an `HTTPException`, your handler will be able to catch handle it.
In this example, to be able to have both `HTTPException`s in the same code, Starlette's exceptions is renamed to `StarletteHTTPException`:
```Python
from starlette.exceptions import HTTPException as StarletteHTTPException
```
### Re-use **FastAPI**'s exception handlers
You could also just want to use the exception somehow, but then use the same default exception handlers from **FastAPI**.
You can import and re-use the default exception handlers from `fastapi.exception_handlers`:
```Python hl_lines="2 3 4 5 15 21"
{!./src/handling_errors/tutorial005.py!}
```
In this example, you are just `print`ing the error with a very expressive notification.
But you get the idea, you can use the exception and then just re-use the default exception handlers.

View File

@@ -18,9 +18,11 @@ The first value is the default value, you can pass all the extra validation or a
{!./src/header_params/tutorial001.py!}
```
!!! info
!!! note "Technical Details"
`Header` is a "sister" class of `Path`, `Query` and `Cookie`. It also inherits from the same common `Param` class.
But remember that when you import `Query`, `Path`, `Header`, and others from `fastapi`, <a href="https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#recap" target="_blank">those are actually functions that return classes of the same name</a>.
!!! info
To declare headers, you need to use `Header`, because otherwise the parameters would be interpreted as query parameters.

View File

@@ -103,6 +103,17 @@ And you can also declare numeric validations:
* `le`: `l`ess than or `e`qual
!!! info
`Query`, `Path` and others you will see later are subclasses of a common `Param` class (that you don't need to use).
And all of them share the same all these same parameters of additional validation and metadata you have seen.
`Query`, `Path` and others you will see later subclasses of a common `Param` class (that you don't need to use).
And all of them share the same all these same parameters of additional validation and metadata you have seen.
!!! note "Technical Details"
When you import `Query`, `Path` and others from `fastapi`, they are actually functions.
That when called, return instances of classes of the same name.
So, you import `Query`, which is a function. And when you call it, it returns an instance of a class also named `Query`.
These functions are there (instead of just using the classes directly) so that your editor doesn't mark errors about their types.
That way you can use your normal editor and coding tools without having to add custom configurations to disregard those errors.

View File

@@ -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,111 @@ 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}`.
But you need `file_path` itself to contain a *path*, like `home/johndoe/myfile.txt`.
So, the URL for that file would be something like: `/files/home/johndoe/myfile.txt`.
### OpenAPI support
OpenAPI doesn't support a way to declare a *path parameter* to contain a *path* inside, as that could lead to scenarios that are difficult to test and define.
Nevertheless, you can still do it in **FastAPI**, using one of the internal tools from Starlette.
And the docs would still work, although not adding any documentation telling that the parameter should contain a path.
### Path convertor
Using an option directly from Starlette you can declare a *path parameter* containing a *path* using a URL like:
```
/files/{file_path:path}
```
In this case, the name of the parameter is `file_path`, and the last part, `:path`, tells it that the parameter should match any *path*.
So, you can use it with:
```Python hl_lines="6"
{!./src/path_params/tutorial004.py!}
```
!!! tip
You could need the parameter to contain `/home/johndoe/myfile.txt`, with a leading slash (`/`).
In that case, the URL would be: `/files//home/johndoe/myfile.txt`, with a double slash (`//`) between `files` and `home`.
## Recap
@@ -127,4 +230,4 @@ With **FastAPI**, by using short, intuitive and standard Python type declaration
And you only have to declare them once.
That's probably the main visible advantage of **FastAPI** compared to alternative frameworks (apart from the raw performance).
That's probably the main visible advantage of **FastAPI** compared to alternative frameworks (apart from the raw performance).

View File

@@ -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.

View File

@@ -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!}
```

View File

@@ -19,6 +19,8 @@ Create file parameters the same way you would for `Body` or `Form`:
!!! info
`File` is a class that inherits directly from `Form`.
But remember that when you import `Query`, `Path`, `File` and others from `fastapi`, <a href="https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#recap" target="_blank">those are actually functions that return classes of the same name</a>.
!!! info
To declare File bodies, you need to use `File`, because otherwise the parameters would be interpreted as query parameters or body (JSON) parameters.

View 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.

View File

@@ -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>.

View File

@@ -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.

View File

@@ -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>.

View File

@@ -13,12 +13,14 @@ You can declare the model used for the response with the parameter `response_mod
!!! note
Notice that `response_model` is a parameter of the "decorator" method (`get`, `post`, etc). Not of your path operation function, like all the parameters and body.
It receives a standard Pydantic model and will:
It receives the same type you would declare for a Pydantic model attribute, so, it can be a Pydantic model, but it can also be, e.g. a `list` of Pydantic models, like `List[Item]`.
* Convert the output data to the type declarations of the model
* Validate the data
* Add a JSON Schema for the response, in the OpenAPI path operation
* Will be used by the automatic documentation systems
FastAPI will use this `response_model` to:
* Convert the output data to its type declaration.
* Validate the data.
* Add a JSON Schema for the response, in the OpenAPI path operation.
* Will be used by the automatic documentation systems.
But most importantly:
@@ -45,7 +47,7 @@ Now, whenever a browser is creating a user with a password, the API will return
In this case, it might not be a problem, because the user himself is sending the password.
But if we use the same model for another path operation, we could be sending the passwords of our users to every client.
But if we use the same model for another path operation, we could be sending our user's passwords to every client.
!!! danger
Never send the plain password of a user in a response.
@@ -82,6 +84,114 @@ And both models will be used for the interactive API documentation:
<img src="/img/tutorial/response-model/image02.png">
## Response Model encoding parameters
Your response model could have default values, like:
```Python hl_lines="11 13 14"
{!./src/response_model/tutorial004.py!}
```
* `description: str = 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.
For example, if you have models with many optional attributes in a NoSQL database, but you don't want to send very long JSON responses full of default values.
### Use the `response_model_skip_defaults` parameter
You can set the *path operation decorator* parameter `response_model_skip_defaults=True`:
```Python hl_lines="24"
{!./src/response_model/tutorial004.py!}
```
and those default values won't be included in the response.
So, if you send a request to that *path operation* for the item with ID `foo`, the response (not including default values) will be:
```JSON
{
"name": "Foo",
"price": 50.2
}
```
!!! info
FastAPI uses Pydantic model's `.dict()` with <a href="https://pydantic-docs.helpmanual.io/#copying" target="_blank">its `skip_defaults` parameter</a> to achieve this.
#### Data with values for fields with defaults
But if your data has values for the model's fields with default values, like the item with ID `bar`:
```Python hl_lines="3 5"
{
"name": "Bar",
"description": "The bartenders",
"price": 62,
"tax": 20.2
}
```
they will be included in the response.
#### Data with the same values as the defaults
If the data has the same values as the default ones, like the item with ID `baz`:
```Python hl_lines="3 5 6"
{
"name": "Baz",
"description": None,
"price": 50.2,
"tax": 10.5,
"tags": []
}
```
FastAPI is smart enough (actually, Pydantic is smart enough) to realize that, even though `description`, `tax`, and `tags` have the same values as the defaults, they were set explicitly (instead of taken from the defaults).
So, they will be included in the JSON response.
!!! tip
Notice that the default values can be anything, not only `None`.
They can be a list (`[]`), a `float` of `10.5`, etc.
### `response_model_include` and `response_model_exclude`
You can also use the *path operation decorator* parameters `response_model_include` and `response_model_exclude`.
They take a `set` of `str` with the name of the attributes to include (omitting the rest) or to exclude (including the rest).
This can be used as a quick shortcut if you have only one Pydantic model and want to remove some data from the output.
!!! tip
But it is still recommended to use the ideas above, using multiple classes, instead of these parameters.
This is because the JSON Schema generated in your app's OpenAPI (and the docs) will still be the one for the complete model, even if you use `response_model_include` or `response_model_exclude` to omit some attributes.
```Python hl_lines="29 35"
{!./src/response_model/tutorial005.py!}
```
!!! tip
The syntax `{"name", "description"}` creates a `set` with those two values.
It is equivalent to `set(["name", "description"])`.
#### Using `list`s instead of `set`s
If you forget to use a `set` and use a `list` or `tuple` instead, FastAPI will still convert it to a `set` and it will work correctly:
```Python hl_lines="29 35"
{!./src/response_model/tutorial006.py!}
```
## Recap
Use the path operation decorator's parameter `response_model` to define response models and especially to ensure private data is filtered out.
Use `response_model_skip_defaults` to return only the values explicitly set.

View File

@@ -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.

View File

@@ -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`).

View File

@@ -108,6 +108,8 @@ In this case, it requires the scope `me` (it could require more than one scope).
But by using `Security` instead of `Depends`, **FastAPI** will know that it can declare security scopes, use them internally, and document the API with OpenAPI.
But when you import `Query`, `Path`, `Depends`, `Security` and others from `fastapi`, <a href="https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#recap" target="_blank">those are actually functions that return classes of the same name</a>.
## Use `SecurityScopes`
Now update the dependency `get_current_user`.
@@ -174,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:
@@ -242,3 +244,7 @@ The most secure is the code flow, but is more complex to implement as it require
But in the end, they are implementing the same OAuth2 standard.
**FastAPI** includes utilities for all these OAuth2 authentication flows in `fastapi.security.oauth2`.
## `Security` in decorator `dependencies`
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.

View File

@@ -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>.

View 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).

View File

@@ -22,12 +22,11 @@ Write simple `assert` statements with the standard Python expressions that you n
!!! tip
Notice that the testing functions are normal `def`, not `async def`.
And the calls to the client are also normal calls, not using `await`.
This allows you to use `pytest` directly without complications.
## Separating tests
In a real application, you probably would have your tests in a different file.
@@ -50,6 +49,51 @@ Then you could have a file `test_main.py` with your tests, and import your `app`
{!./src/app_testing/test_main.py!}
```
## Testing: extended example
Now let's extend this example and add more details to see how to test different parts.
### Extended **FastAPI** app file
Let's say you have a file `main_b.py` with your **FastAPI** app.
It has a `GET` operation that could return an error.
It has a `POST` operation that could return several errors.
Both *path operations* require an `X-Token` header.
```Python
{!./src/app_testing/main_b.py!}
```
### Extended testing file
You could then have a `test_main_b.py`, the same as before, with the extended tests:
```Python
{!./src/app_testing/test_main_b.py!}
```
Whenever you need the client to pass information in the request and you don't know how to, you can search (Google) how to do it in `requests`.
Then you just do the same in your tests.
E.g.:
* To pass a *path* or *query* parameter, add it to the URL itself.
* To pass a JSON body, pass a Python object (e.g. a `dict`) to the parameter `json`.
* If you need to send *Form Data* instead of JSON, use the `data` parameter instead.
* To pass *headers*, use a `dict` in the `headers` parameter.
* For *cookies*, a `dict` in the `cookies` parameter.
For more information about how to pass data to the backend (using `requests` or the `TestClient`) check the <a href="http://docs.python-requests.org" target="_blank">Requests documentation</a>.
!!! info
Note that the `TestClient` receives data that can be converted to JSON, not Pydantic models.
If you have a Pydantic model in your test and you want to send its data to the application during testing, you can use the <a href="https://fastapi.tiangolo.com/tutorial/encoder/" target="_blank">JSON compatible encoder: `jsonable_encoder`</a>.
## Testing WebSockets
You can use the same `TestClient` to test WebSockets.

View File

@@ -27,9 +27,9 @@ But it's the simplest way to focus on the server-side of WebSockets and have a w
{!./src/websockets/tutorial001.py!}
```
## Create a `websocket_route`
## Create a `websocket`
In your **FastAPI** application, create a `websocket_route`:
In your **FastAPI** application, create a `websocket`:
```Python hl_lines="3 47 48"
{!./src/websockets/tutorial001.py!}
@@ -38,15 +38,6 @@ In your **FastAPI** application, create a `websocket_route`:
!!! tip
In this example we are importing `WebSocket` from `starlette.websockets` to use it in the type declaration in the WebSocket route function.
That is not required, but it's recommended as it will provide you completion and checks inside the function.
!!! info
This `websocket_route` we are using comes directly from <a href="https://www.starlette.io/applications/" target="_blank">Starlette</a>.
That's why the naming convention is not the same as with other API path operations (`get`, `post`, etc).
## Await for messages and send messages
In your WebSocket route you can `await` for messages and send messages.
@@ -57,6 +48,32 @@ In your WebSocket route you can `await` for messages and send messages.
You can receive and send binary, text, and JSON data.
## Using `Depends` and others
In WebSocket endpoints you can import from `fastapi` and use:
* `Depends`
* `Security`
* `Cookie`
* `Header`
* `Path`
* `Query`
They work the same way as for other FastAPI endpoints/*path operations*:
```Python hl_lines="55 56 57 58 59 60 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78"
{!./src/websockets/tutorial002.py!}
```
!!! info
In a WebSocket it doesn't really make sense to raise an `HTTPException`. So it's better to close the WebSocket connection directly.
You can use a closing code from the <a href="https://tools.ietf.org/html/rfc6455#section-7.4.1" target="_blank">valid codes defined in the specification</a>.
In the future, there will be a `WebSocketException` that you will be able to `raise` from anywhere, and add exception handlers for it. It depends on the <a href="https://github.com/encode/starlette/pull/527" target="_blank">PR #527</a> in Starlette.
## More info
To learn more about the options, check Starlette's documentation for:
* <a href="https://www.starlette.io/applications/" target="_blank">Applications (`websocket_route`)</a>.

View File

@@ -1,11 +1,21 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.20.1"
__version__ = "0.37.0"
from starlette.background import BackgroundTasks
from .applications import FastAPI
from .datastructures import UploadFile
from .exceptions import HTTPException
from .params import Body, Cookie, Depends, File, Form, Header, Path, Query, Security
from .param_functions import (
Body,
Cookie,
Depends,
File,
Form,
Header,
Path,
Query,
Security,
)
from .routing import APIRouter

View File

@@ -1,27 +1,26 @@
from typing import Any, Callable, Dict, List, Optional, Type, Union
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Type, Union
from fastapi import routing
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
get_swagger_ui_oauth2_redirect_html,
)
from fastapi.openapi.utils import get_openapi
from pydantic import BaseModel
from fastapi.params import Depends
from starlette.applications import Starlette
from starlette.exceptions import ExceptionMiddleware, HTTPException
from starlette.middleware.errors import ServerErrorMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.responses import HTMLResponse, JSONResponse, Response
from starlette.routing import BaseRoute
async def http_exception(request: Request, exc: HTTPException) -> JSONResponse:
headers = getattr(exc, "headers", None)
if headers:
return JSONResponse(
{"detail": exc.detail}, status_code=exc.status_code, headers=headers
)
else:
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
class FastAPI(Starlette):
def __init__(
self,
@@ -35,10 +34,13 @@ class FastAPI(Starlette):
openapi_prefix: str = "",
docs_url: Optional[str] = "/docs",
redoc_url: Optional[str] = "/redoc",
swagger_ui_oauth2_redirect_url: Optional[str] = "/docs/oauth2-redirect",
**extra: Dict[str, Any],
) -> None:
self._debug = debug
self.router: routing.APIRouter = routing.APIRouter(routes)
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
@@ -51,7 +53,9 @@ class FastAPI(Starlette):
self.openapi_prefix = openapi_prefix.rstrip("/")
self.docs_url = docs_url
self.redoc_url = redoc_url
self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
self.extra = extra
self.dependency_overrides: Dict[Callable, Callable] = {}
self.openapi_version = "3.0.2"
@@ -78,39 +82,55 @@ class FastAPI(Starlette):
def setup(self) -> None:
if self.openapi_url:
self.add_route(
self.openapi_url,
lambda req: JSONResponse(self.openapi()),
include_in_schema=False,
)
async def openapi(req: Request) -> JSONResponse:
return JSONResponse(self.openapi())
self.add_route(self.openapi_url, openapi, include_in_schema=False)
openapi_url = self.openapi_prefix + self.openapi_url
if self.openapi_url and self.docs_url:
self.add_route(
self.docs_url,
lambda r: get_swagger_ui_html(
openapi_url=self.openapi_prefix + self.openapi_url,
async def swagger_ui_html(req: Request) -> HTMLResponse:
return get_swagger_ui_html(
openapi_url=openapi_url,
title=self.title + " - Swagger UI",
),
include_in_schema=False,
)
oauth2_redirect_url=self.swagger_ui_oauth2_redirect_url,
)
self.add_route(self.docs_url, swagger_ui_html, include_in_schema=False)
if self.swagger_ui_oauth2_redirect_url:
async def swagger_ui_redirect(req: Request) -> HTMLResponse:
return get_swagger_ui_oauth2_redirect_html()
self.add_route(
self.swagger_ui_oauth2_redirect_url,
swagger_ui_redirect,
include_in_schema=False,
)
if self.openapi_url and self.redoc_url:
self.add_route(
self.redoc_url,
lambda r: get_redoc_html(
openapi_url=self.openapi_prefix + self.openapi_url,
title=self.title + " - ReDoc",
),
include_in_schema=False,
)
self.add_exception_handler(HTTPException, http_exception)
async def redoc_html(req: Request) -> HTMLResponse:
return get_redoc_html(
openapi_url=openapi_url, title=self.title + " - ReDoc"
)
self.add_route(self.redoc_url, redoc_html, include_in_schema=False)
self.add_exception_handler(HTTPException, http_exception_handler)
self.add_exception_handler(
RequestValidationError, request_validation_exception_handler
)
def add_api_route(
self,
path: str,
endpoint: Callable,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: Sequence[Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@@ -118,6 +138,10 @@ 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -128,6 +152,7 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
@@ -135,6 +160,10 @@ class FastAPI(Starlette):
deprecated=deprecated,
methods=methods,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -144,9 +173,10 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: Sequence[Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@@ -154,6 +184,10 @@ 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -165,6 +199,7 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
@@ -172,6 +207,10 @@ class FastAPI(Starlette):
deprecated=deprecated,
methods=methods,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -180,31 +219,53 @@ class FastAPI(Starlette):
return decorator
def add_api_websocket_route(
self, path: str, endpoint: Callable, name: str = None
) -> None:
self.router.add_api_websocket_route(path, endpoint, name=name)
def websocket(self, path: str, name: str = None) -> Callable:
def decorator(func: Callable) -> Callable:
self.add_api_websocket_route(path, func, name=name)
return func
return decorator
def include_router(
self,
router: routing.APIRouter,
*,
prefix: str = "",
tags: List[str] = None,
dependencies: Sequence[Depends] = None,
responses: Dict[Union[int, str], Dict[str, Any]] = None,
) -> None:
self.router.include_router(
router, prefix=prefix, tags=tags, responses=responses or {}
router,
prefix=prefix,
tags=tags,
dependencies=dependencies,
responses=responses or {},
)
def get(
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -214,12 +275,17 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -229,15 +295,20 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -247,12 +318,17 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -262,15 +338,20 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -280,12 +361,17 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -295,15 +381,20 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -313,12 +404,17 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
responses=responses or {},
deprecated=deprecated,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@@ -328,15 +424,20 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -346,12 +447,17 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -361,15 +467,20 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -379,12 +490,17 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -394,15 +510,20 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -412,12 +533,17 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -427,15 +553,20 @@ class FastAPI(Starlette):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -445,12 +576,17 @@ class FastAPI(Starlette):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,

View File

@@ -26,9 +26,13 @@ class Dependant:
name: str = None,
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 []
@@ -38,8 +42,15 @@ class Dependant:
self.dependencies = dependencies or []
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 []))))

View File

@@ -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,7 +21,7 @@ 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
@@ -33,26 +31,27 @@ 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_sub_dependant(
def get_param_sub_dependant(
*, param: inspect.Parameter, path: str, security_scopes: List[str] = None
) -> Dependant:
depends: params.Depends = param.default
@@ -60,20 +59,48 @@ def get_sub_dependant(
dependency = depends.dependency
else:
dependency = param.annotation
return get_sub_dependant(
depends=depends,
dependency=dependency,
path=path,
name=param.name,
security_scopes=security_scopes,
)
def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> Dependant:
assert callable(
depends.dependency
), "A parameter-less dependency must have a callable dependency"
return get_sub_dependant(depends=depends, dependency=depends.dependency, path=path)
def get_sub_dependant(
*,
depends: params.Depends,
dependency: Callable,
path: str,
name: str = None,
security_scopes: List[str] = None,
) -> Dependant:
security_requirement = None
security_scopes = security_scopes or []
if isinstance(depends, params.Security):
dependency_scopes = depends.scopes
security_scopes.extend(dependency_scopes)
if isinstance(dependency, SecurityBase):
use_scopes = []
use_scopes: List[str] = []
if isinstance(dependency, (OAuth2, OpenIdConnect)):
use_scopes = security_scopes
security_requirement = SecurityRequirement(
security_scheme=dependency, scopes=use_scopes
)
sub_dependant = get_dependant(
path=path, call=dependency, name=param.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)
@@ -81,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(),
@@ -89,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)
@@ -101,91 +143,124 @@ 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_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)
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_sub_dependant(
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 param.default == param.empty or isinstance(
param.default, params.Path
), "Path params must have no defaults or use Path(...)"
assert is_scalar_field(
field=param_field
), f"Path params must be of one of the supported types"
add_param_to_fields(
param_field = get_param_field(
param=param,
dependant=dependant,
default_schema=params.Path,
force_type=params.ParamTypes.path,
)
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, 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:
) -> Field:
default_value = Required
had_schema = False
if not param.default == param.empty:
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
@@ -207,43 +282,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)
@@ -255,30 +313,80 @@ def is_coroutine_callable(call: Callable) -> bool:
async def solve_dependencies(
*,
request: Request,
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( # type: ignore
content=None, status_code=None, 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)
assert sub_dependant.name is not None, "Subdependants always have a name"
values[sub_dependant.name] = solved
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
)
@@ -298,21 +406,25 @@ async def solve_dependencies(
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
required_params=dependant.body_params, received_body=body
)
values.update(body_values)
errors.extend(body_errors)
if dependant.request_param_name:
if dependant.request_param_name and isinstance(request, Request):
values[dependant.request_param_name] = request
elif dependant.websocket_param_name and isinstance(request, WebSocket):
values[dependant.websocket_param_name] = request
if dependant.background_tasks_param_name:
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(
@@ -322,13 +434,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:
@@ -353,7 +465,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 = []
@@ -363,10 +476,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 == "")
@@ -442,6 +559,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):
@@ -449,6 +568,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,
@@ -457,6 +584,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

View File

@@ -11,14 +11,24 @@ def jsonable_encoder(
include: Set[str] = None,
exclude: Set[str] = set(),
by_alias: bool = True,
skip_defaults: bool = False,
include_none: bool = True,
custom_encoder: dict = {},
sqlalchemy_safe: bool = True,
) -> Any:
if include is not None and not isinstance(include, set):
include = set(include)
if exclude is not None and not isinstance(exclude, set):
exclude = set(exclude)
if isinstance(obj, BaseModel):
encoder = getattr(obj.Config, "json_encoders", custom_encoder)
return jsonable_encoder(
obj.dict(include=include, exclude=exclude, by_alias=by_alias),
obj.dict(
include=include,
exclude=exclude,
by_alias=by_alias,
skip_defaults=skip_defaults,
),
include_none=include_none,
custom_encoder=encoder,
sqlalchemy_safe=sqlalchemy_safe,
@@ -42,6 +52,7 @@ def jsonable_encoder(
encoded_key = jsonable_encoder(
key,
by_alias=by_alias,
skip_defaults=skip_defaults,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
@@ -49,6 +60,7 @@ def jsonable_encoder(
encoded_value = jsonable_encoder(
value,
by_alias=by_alias,
skip_defaults=skip_defaults,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
@@ -64,6 +76,7 @@ def jsonable_encoder(
include=include,
exclude=exclude,
by_alias=by_alias,
skip_defaults=skip_defaults,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
@@ -91,6 +104,7 @@ def jsonable_encoder(
return jsonable_encoder(
data,
by_alias=by_alias,
skip_defaults=skip_defaults,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,

View File

@@ -0,0 +1,23 @@
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
headers = getattr(exc, "headers", None)
if headers:
return JSONResponse(
{"detail": exc.detail}, status_code=exc.status_code, headers=headers
)
else:
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
async def request_validation_exception_handler(
request: Request, exc: RequestValidationError
) -> JSONResponse:
return JSONResponse(
status_code=HTTP_422_UNPROCESSABLE_ENTITY, content={"detail": exc.errors()}
)

View File

@@ -1,9 +1,20 @@
from typing import Any
from pydantic import ValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
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
class WebSocketRequestValidationError(ValidationError):
pass

View File

@@ -1,31 +1,40 @@
from typing import Optional
from starlette.responses import HTMLResponse
def get_swagger_ui_html(*, openapi_url: str, title: str) -> HTMLResponse:
return HTMLResponse(
"""
def get_swagger_ui_html(
*,
openapi_url: str,
title: str,
swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js",
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,
) -> HTMLResponse:
html = f"""
<! doctype html>
<html>
<head>
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css">
<link rel="shortcut icon" href="https://fastapi.tiangolo.com/img/favicon.png">
<title>
"""
+ title
+ """
</title>
<link type="text/css" rel="stylesheet" href="{swagger_css_url}">
<link rel="shortcut icon" href="{swagger_favicon_url}">
<title>{title}</title>
</head>
<body>
<div id="swagger-ui">
</div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
<script src="{swagger_js_url}"></script>
<!-- `SwaggerUIBundle` is now available on the page -->
<script>
const ui = SwaggerUIBundle({
url: '"""
+ openapi_url
+ """',
const ui = SwaggerUIBundle({{
url: '{openapi_url}',
"""
if oauth2_redirect_url:
html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',"
html += """
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
@@ -33,48 +42,124 @@ def get_swagger_ui_html(*, openapi_url: str, title: str) -> HTMLResponse:
],
layout: "BaseLayout",
deepLinking: true
})
</script>
</body>
</html>
"""
)
return HTMLResponse(html)
def get_redoc_html(*, openapi_url: str, title: str) -> HTMLResponse:
return HTMLResponse(
"""
def get_redoc_html(
*,
openapi_url: str,
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>
<html>
<head>
<title>
"""
+ title
+ """
</title>
<html>
<head>
<title>{title}</title>
<!-- 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">
<link rel="shortcut icon" href="https://fastapi.tiangolo.com/img/favicon.png">
"""
html += f"""
<link rel="shortcut icon" href="{redoc_favicon_url}">
<!--
ReDoc doesn't change outer page styles
-->
<style>
body {
body {{
margin: 0;
padding: 0;
}
}}
</style>
</head>
<body>
<redoc spec-url='"""
+ openapi_url
+ """'></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
</body>
</html>
</head>
<body>
<redoc spec-url="{openapi_url}"></redoc>
<script src="{redoc_js_url}"> </script>
</body>
</html>
"""
)
return HTMLResponse(html)
def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse:
html = """
<!doctype html>
<html lang="en-US">
<body onload="run()">
</body>
</html>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}
arr = qp.split("&")
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value)
}
) : {}
isValid = qp.state === sentState
if ((
oauth2.auth.schema.get("flow") === "accessCode"||
oauth2.auth.schema.get("flow") === "authorizationCode"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
</script>
"""
return HTMLResponse(content=html)

View File

@@ -5,13 +5,15 @@ from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel, Schema as PSchema
from pydantic.types import UrlStr
logger = logging.getLogger("fastapi")
try:
import email_validator
assert email_validator # make autoflake ignore the unused import
from pydantic.types import EmailStr # type: ignore
except ImportError: # pragma: no cover
logging.warning(
logger.warning(
"email-validator not installed, email fields will be treated as str.\n"
+ "To install, run: pip install email-validator"
)
@@ -120,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):
@@ -147,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
@@ -163,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
@@ -208,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
@@ -220,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

View File

@@ -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:
@@ -114,11 +121,8 @@ def get_openapi_operation_request_body(
def generate_operation_id(*, route: routing.APIRoute, method: str) -> str:
if route.operation_id:
return route.operation_id
path: str = route.path
operation_id = route.name + path
operation_id = operation_id.replace("{", "_").replace("}", "_").replace("/", "_")
operation_id = operation_id + "_" + method.lower()
return operation_id
path: str = route.path_format
return generate_operation_id_for_path(name=route.name, path=path, method=method)
def generate_operation_summary(*, route: routing.APIRoute, method: str) -> str:
@@ -151,7 +155,7 @@ def get_openapi_path(
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 +164,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 +174,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 +181,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_class.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,
@@ -214,8 +216,15 @@ def get_openapi_path(
).setdefault("content", {}).setdefault(route.response_class.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 +232,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
@@ -253,7 +269,9 @@ def get_openapi(
if result:
path, security_schemes, path_definitions = result
if path:
paths.setdefault(openapi_prefix + route.path, {}).update(path)
paths.setdefault(openapi_prefix + route.path_format, {}).update(
path
)
if security_schemes:
components.setdefault("securitySchemes", {}).update(
security_schemes

250
fastapi/param_functions.py Normal file
View File

@@ -0,0 +1,250 @@
from typing import Any, Callable, Sequence
from fastapi import params
def Path( # noqa: N802
default: Any,
*,
alias: str = None,
title: str = None,
description: str = None,
gt: float = None,
ge: float = None,
lt: float = None,
le: float = None,
min_length: int = None,
max_length: int = None,
regex: str = None,
deprecated: bool = None,
**extra: Any,
) -> Any:
return params.Path(
default=default,
alias=alias,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
regex=regex,
deprecated=deprecated,
**extra,
)
def Query( # noqa: N802
default: Any,
*,
alias: str = None,
title: str = None,
description: str = None,
gt: float = None,
ge: float = None,
lt: float = None,
le: float = None,
min_length: int = None,
max_length: int = None,
regex: str = None,
deprecated: bool = None,
**extra: Any,
) -> Any:
return params.Query(
default,
alias=alias,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
regex=regex,
deprecated=deprecated,
**extra,
)
def Header( # noqa: N802
default: Any,
*,
alias: str = None,
convert_underscores: bool = True,
title: str = None,
description: str = None,
gt: float = None,
ge: float = None,
lt: float = None,
le: float = None,
min_length: int = None,
max_length: int = None,
regex: str = None,
deprecated: bool = None,
**extra: Any,
) -> Any:
return params.Header(
default,
alias=alias,
convert_underscores=convert_underscores,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
regex=regex,
deprecated=deprecated,
**extra,
)
def Cookie( # noqa: N802
default: Any,
*,
alias: str = None,
title: str = None,
description: str = None,
gt: float = None,
ge: float = None,
lt: float = None,
le: float = None,
min_length: int = None,
max_length: int = None,
regex: str = None,
deprecated: bool = None,
**extra: Any,
) -> Any:
return params.Cookie(
default,
alias=alias,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
regex=regex,
deprecated=deprecated,
**extra,
)
def Body( # noqa: N802
default: Any,
*,
embed: bool = False,
media_type: str = "application/json",
alias: str = None,
title: str = None,
description: str = None,
gt: float = None,
ge: float = None,
lt: float = None,
le: float = None,
min_length: int = None,
max_length: int = None,
regex: str = None,
**extra: Any,
) -> Any:
return params.Body(
default,
embed=embed,
media_type=media_type,
alias=alias,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
regex=regex,
**extra,
)
def Form( # noqa: N802
default: Any,
*,
media_type: str = "application/x-www-form-urlencoded",
alias: str = None,
title: str = None,
description: str = None,
gt: float = None,
ge: float = None,
lt: float = None,
le: float = None,
min_length: int = None,
max_length: int = None,
regex: str = None,
**extra: Any,
) -> Any:
return params.Form(
default,
media_type=media_type,
alias=alias,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
regex=regex,
**extra,
)
def File( # noqa: N802
default: Any,
*,
media_type: str = "multipart/form-data",
alias: str = None,
title: str = None,
description: str = None,
gt: float = None,
ge: float = None,
lt: float = None,
le: float = None,
min_length: int = None,
max_length: int = None,
regex: str = None,
**extra: Any,
) -> Any:
return params.File(
default,
media_type=media_type,
alias=alias,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
regex=regex,
**extra,
)
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, use_cache: bool = True
) -> Any:
return params.Security(dependency=dependency, scopes=scopes, use_cache=use_cache)

View File

@@ -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)

View File

@@ -1,12 +1,19 @@
import asyncio
import inspect
import logging
from typing import Any, Callable, Dict, List, Optional, Type, Union
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Type, Union
from fastapi import params
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import get_body_field, get_dependant, solve_dependencies
from fastapi.dependencies.utils import (
get_body_field,
get_dependant,
get_parameterless_sub_dependant,
solve_dependencies,
)
from fastapi.encoders import 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
@@ -16,24 +23,46 @@ from starlette.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.routing import compile_path, get_name, request_response
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
from starlette.routing import (
compile_path,
get_name,
request_response,
websocket_session,
)
from starlette.status import WS_1008_POLICY_VIOLATION
from starlette.types import ASGIApp
from starlette.websockets import WebSocket
def serialize_response(*, field: Field = None, response: Response) -> Any:
encoded = jsonable_encoder(response)
def serialize_response(
*,
field: Field = None,
response: Response,
include: Set[str] = None,
exclude: Set[str] = set(),
by_alias: bool = True,
skip_defaults: bool = False,
) -> Any:
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)
return jsonable_encoder(value)
return jsonable_encoder(
value,
include=include,
exclude=exclude,
by_alias=by_alias,
skip_defaults=skip_defaults,
)
else:
return encoded
return jsonable_encoder(response)
def get_app(
@@ -42,8 +71,13 @@ def get_app(
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_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 me a function"
assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = asyncio.iscoroutinefunction(dependant.call)
is_body_form = body_field and isinstance(body_field.schema, params.Form)
@@ -61,17 +95,18 @@ def get_app(
logging.error(f"Error getting request body: {e}")
raise HTTPException(
status_code=400, detail="There was an error parsing the body"
)
values, errors, background_tasks = await solve_dependencies(
request=request, dependant=dependant, body=body
) from e
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:
errors_out = ValidationError(errors)
raise HTTPException(
status_code=HTTP_422_UNPROCESSABLE_ENTITY, detail=errors_out.errors()
)
raise RequestValidationError(errors)
else:
assert dependant.call is not None, "dependant.call must me a function"
assert dependant.call is not None, "dependant.call must be a function"
if is_coroutine:
raw_response = await dependant.call(**values)
else:
@@ -81,47 +116,109 @@ def get_app(
raw_response.background = background_tasks
return raw_response
response_data = serialize_response(
field=response_field, response=raw_response
field=response_field,
response=raw_response,
include=response_model_include,
exclude=response_model_exclude,
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, dependency_overrides_provider: Any = None
) -> Callable:
async def app(websocket: WebSocket) -> None:
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 be a function"
await dependant.call(**values)
return app
class APIWebSocketRoute(routing.WebSocketRoute):
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,
dependency_overrides_provider=dependency_overrides_provider,
)
)
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
class APIRoute(routing.Route):
def __init__(
self,
path: str,
endpoint: Callable,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
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,
@@ -131,10 +228,25 @@ 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 []
if dependencies:
self.dependencies = list(dependencies)
else:
self.dependencies = []
self.summary = summary
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
self.response_description = response_description
@@ -147,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,
@@ -163,56 +275,87 @@ 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
self.response_model_by_alias = response_model_by_alias
self.response_model_skip_defaults = response_model_skip_defaults
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"
self.dependant = get_dependant(path=path, call=self.endpoint)
self.body_field = get_body_field(dependant=self.dependant, name=self.name)
self.dependant = get_dependant(path=self.path_format, call=self.endpoint)
for depends in self.dependencies[::-1]:
self.dependant.dependencies.insert(
0,
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
)
self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id)
self.dependency_overrides_provider = dependency_overrides_provider
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_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,
endpoint: Callable,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> None:
route = APIRoute(
route = self.route_class(
path,
endpoint=endpoint,
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
@@ -220,9 +363,14 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=methods,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
dependency_overrides_provider=self.dependency_overrides_provider,
)
self.routes.append(route)
@@ -230,9 +378,10 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = None,
dependencies: Sequence[params.Depends] = None,
summary: str = None,
description: str = None,
response_description: str = "Successful Response",
@@ -240,6 +389,10 @@ 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -251,6 +404,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
@@ -258,6 +412,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=methods,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -266,12 +424,26 @@ class APIRouter(routing.Router):
return decorator
def add_api_websocket_route(
self, path: str, endpoint: Callable, name: str = None
) -> None:
route = APIWebSocketRoute(path, endpoint=endpoint, name=name)
self.routes.append(route)
def websocket(self, path: str, name: str = None) -> Callable:
def decorator(func: Callable) -> Callable:
self.add_api_websocket_route(path, func, name=name)
return func
return decorator
def include_router(
self,
router: "APIRouter",
*,
prefix: str = "",
tags: List[str] = None,
dependencies: Sequence[params.Depends] = None,
responses: Dict[Union[int, str], Dict[str, Any]] = None,
) -> None:
if prefix:
@@ -279,6 +451,14 @@ class APIRouter(routing.Router):
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:
@@ -290,6 +470,8 @@ class APIRouter(routing.Router):
response_model=route.response_model,
status_code=route.status_code,
tags=(route.tags or []) + (tags or []),
dependencies=list(dependencies or [])
+ list(route.dependencies or []),
summary=route.summary,
description=route.description,
response_description=route.response_description,
@@ -297,6 +479,10 @@ class APIRouter(routing.Router):
deprecated=route.deprecated,
methods=route.methods,
operation_id=route.operation_id,
response_model_include=route.response_model_include,
response_model_exclude=route.response_model_exclude,
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,
name=route.name,
@@ -305,10 +491,14 @@ class APIRouter(routing.Router):
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,
)
elif isinstance(route, APIWebSocketRoute):
self.add_api_websocket_route(
prefix + route.path, route.endpoint, name=route.name
)
elif isinstance(route, routing.WebSocketRoute):
self.add_websocket_route(
prefix + route.path, route.endpoint, name=route.name
@@ -318,24 +508,31 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.api_route(
path=path,
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
@@ -343,6 +540,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["GET"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -352,15 +553,20 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -370,6 +576,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
@@ -377,6 +584,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["PUT"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -386,15 +597,20 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -404,6 +620,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
@@ -411,6 +628,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["POST"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -420,15 +641,20 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -438,6 +664,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
@@ -445,6 +672,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["DELETE"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -454,15 +685,20 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -472,6 +708,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
@@ -479,6 +716,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["OPTIONS"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -488,15 +729,20 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -506,6 +752,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
@@ -513,6 +760,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["HEAD"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -522,15 +773,20 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -540,6 +796,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
@@ -547,6 +804,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["PATCH"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,
@@ -556,15 +817,20 @@ class APIRouter(routing.Router):
self,
path: str,
*,
response_model: Type[BaseModel] = None,
response_model: Type[Any] = None,
status_code: int = 200,
tags: List[str] = 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_by_alias: bool = True,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@@ -574,6 +840,7 @@ class APIRouter(routing.Router):
response_model=response_model,
status_code=status_code,
tags=tags or [],
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
@@ -581,6 +848,10 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["TRACE"],
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
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,
name=name,

View File

@@ -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(

View File

@@ -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)

View File

@@ -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

View File

@@ -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( # type: ignore
original_type.__name__,
__config__=original_type.__config__,
__validators__=original_type.__validators__,
)
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

View File

@@ -45,15 +45,18 @@ nav:
- Path Operation Advanced Configuration: 'tutorial/path-operation-advanced-configuration.md'
- Additional Status Codes: 'tutorial/additional-status-codes.md'
- JSON compatible encoder: 'tutorial/encoder.md'
- Body - updates: 'tutorial/body-updates.md'
- Return a Response directly: 'tutorial/response-directly.md'
- Custom Response Class: 'tutorial/custom-response.md'
- 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'
- Sub-dependencies: 'tutorial/dependencies/sub-dependencies.md'
- Dependencies in path operation decorators: 'tutorial/dependencies/dependencies-in-path-operation-decorators.md'
- Advanced Dependencies: 'tutorial/dependencies/advanced-dependencies.md'
- Security:
- Security Intro: 'tutorial/security/intro.md'
@@ -79,6 +82,7 @@ nav:
- WebSockets: 'tutorial/websockets.md'
- 'Events: startup - shutdown': 'tutorial/events.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'
@@ -86,6 +90,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'

View File

@@ -19,8 +19,8 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
]
requires = [
"starlette ==0.11.1",
"pydantic >=0.17,<=0.23.0"
"starlette >=0.11.1,<=0.12.7",
"pydantic >=0.30,<=0.30.0"
]
description-file = "README.md"
requires-python = ">=3.6"

6
scripts/format-imports.sh Executable file
View 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

6
scripts/format.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/sh -e
set -x
autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place docs/src/ fastapi tests --exclude=__init__.py
black fastapi tests docs/src
isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --thirdparty fastapi --apply fastapi tests docs/src

View File

@@ -4,7 +4,7 @@ import os
import requests
room_id = "5c9c9540d73408ce4fbc1403" # FastAPI
room_id = "5cc46398d73408ce4fbed233" # Gitter development
# room_id = "5cc46398d73408ce4fbed233" # Gitter development
gitter_token = os.getenv("GITTER_TOKEN")
assert gitter_token

Some files were not shown because too many files have changed in this diff Show More