mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-24 06:39:31 -05:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf53518141 | ||
|
|
0ed55eb7d3 | ||
|
|
12e087f0b5 | ||
|
|
ba10838c30 | ||
|
|
656e1c7ce9 | ||
|
|
88b31e6a4d | ||
|
|
2c3b826810 | ||
|
|
aa64eecda6 | ||
|
|
712b18a58a | ||
|
|
a809da5567 | ||
|
|
80b68cd97d | ||
|
|
894e131e03 | ||
|
|
8772e2f2ee | ||
|
|
7edbd9345b | ||
|
|
56819fdd89 | ||
|
|
febf8e7341 | ||
|
|
293ebd7cc2 | ||
|
|
54e3949f74 | ||
|
|
acbcbba94f | ||
|
|
f7b7a099c3 | ||
|
|
0ea0d0e82a | ||
|
|
890f1f7899 | ||
|
|
783816a7e3 | ||
|
|
7863490c8c | ||
|
|
955e9fcb31 |
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Create a file with '...'
|
||||
2. Add a path operation function with '....'
|
||||
3. Open the browser and call it with a payload of '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment:**
|
||||
- OS: [e.g. Linux / Windows / macOS]
|
||||
- FastAPI Version [e.g. 0.3.0], get it with:
|
||||
|
||||
```Python
|
||||
import fastapi
|
||||
print(fastapi.__version__)
|
||||
```
|
||||
|
||||
- Python version, get it with:
|
||||
|
||||
```bash
|
||||
python --version
|
||||
```
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE]"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I want to be able to [...] but I can't because [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
17
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask a question
|
||||
title: "[QUESTION]"
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
How can I [...]?
|
||||
|
||||
Is it possible to [...]?
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ site
|
||||
.coverage
|
||||
coverage.xml
|
||||
.netlify
|
||||
test.db
|
||||
|
||||
3
Pipfile
3
Pipfile
@@ -21,9 +21,10 @@ email-validator = "*"
|
||||
ujson = "*"
|
||||
flake8 = "*"
|
||||
python-multipart = "*"
|
||||
sqlalchemy = "*"
|
||||
|
||||
[packages]
|
||||
starlette = "==0.10.1"
|
||||
starlette = "==0.11.1"
|
||||
pydantic = "==0.18.2"
|
||||
|
||||
[requires]
|
||||
|
||||
173
Pipfile.lock
generated
173
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "20483e725e92e679c4c21ea3ff0043d759c74102b181f16b67908f979f854d5c"
|
||||
"sha256": "6b55a2dcce8b6bd5a1be8f170acb18478149218a01d1b026981a6297800cd3fa"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -34,10 +34,10 @@
|
||||
},
|
||||
"starlette": {
|
||||
"hashes": [
|
||||
"sha256:7cc05c33d00db3b2ddfd7516a737544ed0a34c9dd0ced94076f29b581ce4f532"
|
||||
"sha256:9d48b35d1fc7521d59ae53c421297ab3878d3c7cd4b75266d77f6c73cccb78bb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.10.1"
|
||||
"version": "==0.11.1"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
@@ -50,10 +50,10 @@
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
|
||||
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
|
||||
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
|
||||
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
|
||||
],
|
||||
"version": "==1.2.1"
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
@@ -199,19 +199,19 @@
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:09b9bb539920776da542e67a570a5df96ff933c9a08b62cfae920bcc789e4383",
|
||||
"sha256:e0f8cd519cfc0072c0ee31add5def09d2b3ef6040b34dc426445c3af9b02163c"
|
||||
"sha256:6d8c66a65635d46d54de59b027a1dda40abbe2275b3164b634835ac9c13fd048",
|
||||
"sha256:6eab21c6e34df2c05416faa40d0c59963008fff29b6f0ccfe8fa28152ab3e383"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.4"
|
||||
"version": "==3.7.6"
|
||||
},
|
||||
"flit": {
|
||||
"hashes": [
|
||||
"sha256:6aefa6ff89a993af7a7af40d3df3d0387d6663df99797981ec41b1431ec6d1e1",
|
||||
"sha256:9969db9708305b64fd8acf20043fcff144f910222397a221fd29871f02ed4a6f"
|
||||
"sha256:1d93f7a833ed8a6e120ddc40db5c4763bc39bccc75c05081ec8285ece718aefb",
|
||||
"sha256:6f6f0fb83c51ffa3a150fa41b5ac118df9ea4a87c2c06dff4ebf9adbe7b52b36"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.2.1"
|
||||
"version": "==1.3"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
@@ -229,11 +229,11 @@
|
||||
},
|
||||
"ipython": {
|
||||
"hashes": [
|
||||
"sha256:6a9496209b76463f1dec126ab928919aaf1f55b38beb9219af3fe202f6bbdd12",
|
||||
"sha256:f69932b1e806b38a7818d9a1e918e5821b685715040b48e59c657b3c7961b742"
|
||||
"sha256:06de667a9e406924f97781bda22d5d76bfb39762b678762d86a466e63f65dc39",
|
||||
"sha256:5d3e020a6b5f29df037555e5c45ab1088d6a7cf3bd84f47e0ba501eeb0c3ec82"
|
||||
],
|
||||
"markers": "python_version >= '3.3'",
|
||||
"version": "==7.2.0"
|
||||
"version": "==7.3.0"
|
||||
},
|
||||
"ipython-genutils": {
|
||||
"hashes": [
|
||||
@@ -387,27 +387,27 @@
|
||||
},
|
||||
"mkdocs-material": {
|
||||
"hashes": [
|
||||
"sha256:4b4af83c704d2bab41be3a5228e800a5e1157003368fbf548d95073ce19e0f61",
|
||||
"sha256:86c0042c803586985bf79c99962ebd4644c3f0ff095d5df541f09fa48f5b62cc"
|
||||
"sha256:63c49a7020e5d187d5adcd441b259e0b81ad418599b22e2c2574b419ed833851",
|
||||
"sha256:90a240f268f182a96098490d35bb75d5efc86b2f67d63a82b8750da20a72ef60"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.3.0"
|
||||
"version": "==4.0.1"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4",
|
||||
"sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc",
|
||||
"sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
|
||||
"sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40",
|
||||
"sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"
|
||||
],
|
||||
"version": "==5.0.0"
|
||||
"markers": "python_version > '2.7'",
|
||||
"version": "==6.0.0"
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:986a7f97808a865405c5fd98fae5ebfa963c31520a56c783df159e9a81e41b3e",
|
||||
"sha256:cc5df73cc11d35655a8c364f45d07b13c8db82c000def4bd7721be13356533b4"
|
||||
"sha256:308c274eb8482fbf16006f549137ddc0d69e5a589465e37b99c4564414363ca7",
|
||||
"sha256:e80fd6af34614a0e898a57f14296d0dacb584648f0339c2e000ddbf0f4cc2f8d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.660"
|
||||
"version": "==0.670"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
@@ -418,10 +418,10 @@
|
||||
},
|
||||
"nbconvert": {
|
||||
"hashes": [
|
||||
"sha256:08d21cf4203fabafd0d09bbd63f06131b411db8ebeede34b0fd4be4548351779",
|
||||
"sha256:a8a2749f972592aa9250db975304af6b7337f32337e523a2c995cc9e12c07807"
|
||||
"sha256:302554a2e219bc0fc84f3edd3e79953f3767b46ab67626fdec16e38ba3f7efe4",
|
||||
"sha256:5de8fb2284422272a1d45abc77c07b888127550a6d602ce619592a2b08a474ff"
|
||||
],
|
||||
"version": "==5.4.0"
|
||||
"version": "==5.4.1"
|
||||
},
|
||||
"nbformat": {
|
||||
"hashes": [
|
||||
@@ -445,10 +445,10 @@
|
||||
},
|
||||
"parso": {
|
||||
"hashes": [
|
||||
"sha256:4b8f9ed80c3a4a3191aa3261505d868aa552dd25649cb13a7d73b6b7315edf2d",
|
||||
"sha256:5a120be2e8863993b597f1c0437efca799e90e0793c98ae5d4e34ebd00140e31"
|
||||
"sha256:4580328ae3f548b358f4901e38c0578229186835f0fa0846e47369796dd5bcc9",
|
||||
"sha256:68406ebd7eafe17f8e40e15a84b56848eccbf27d7c1feb89e93d8fca395706db"
|
||||
],
|
||||
"version": "==0.3.2"
|
||||
"version": "==0.3.4"
|
||||
},
|
||||
"pexpect": {
|
||||
"hashes": [
|
||||
@@ -531,17 +531,17 @@
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
"sha256:5a3827d57ad3e46820e5ee4ed5b9e0ee7bc4686df6634a7368bc1863a5c48a77"
|
||||
"sha256:07f7ae71291af8b0dbad8c2ab630d8223e4a8c4e10fc37badda158c02e753acf"
|
||||
],
|
||||
"version": "==0.14.9"
|
||||
"version": "==0.14.10"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:65aeaa77ae87c7fc95de56285282546cfa9c886dc8e5dc78313db1c25e21bc07",
|
||||
"sha256:6ac6d467d9f053e95aaacd79f831dbecfe730f419c6c7022cb316b365cd9199d"
|
||||
"sha256:067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c",
|
||||
"sha256:9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.2.0"
|
||||
"version": "==4.3.0"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
@@ -553,10 +553,10 @@
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
"sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93",
|
||||
"sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02"
|
||||
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
|
||||
"sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
|
||||
],
|
||||
"version": "==2.7.5"
|
||||
"version": "==2.8.0"
|
||||
},
|
||||
"python-multipart": {
|
||||
"hashes": [
|
||||
@@ -583,33 +583,23 @@
|
||||
},
|
||||
"pyzmq": {
|
||||
"hashes": [
|
||||
"sha256:25a0715c8f69cf72f67cfe5a68a3f3ed391c67c063d2257bec0fe7fc2c7f08f8",
|
||||
"sha256:2bab63759632c6b9e0d5bf19cc63c3b01df267d660e0abcf230cf0afaa966349",
|
||||
"sha256:30ab49d99b24bf0908ebe1cdfa421720bfab6f93174e4883075b7ff38cc555ba",
|
||||
"sha256:32c7ca9fc547a91e3c26fc6080b6982e46e79819e706eb414dd78f635a65d946",
|
||||
"sha256:41219ae72b3cc86d97557fe5b1ef5d1adc1057292ec597b50050874a970a39cf",
|
||||
"sha256:4b8c48a9a13cea8f1f16622f9bd46127108af14cd26150461e3eab71e0de3e46",
|
||||
"sha256:55724997b4a929c0d01b43c95051318e26ddbae23565018e138ae2dc60187e59",
|
||||
"sha256:65f0a4afae59d4fc0aad54a917ab599162613a761b760ba167d66cc646ac3786",
|
||||
"sha256:6f88591a8b246f5c285ee6ce5c1bf4f6bd8464b7f090b1333a446b6240a68d40",
|
||||
"sha256:75022a4c60dcd8765bb9ca32f6de75a0ec83b0d96e0309dc479f4c7b21f26cb7",
|
||||
"sha256:76ea493bfab18dcb090d825f3662b5612e2def73dffc196d51a5194b0294a81d",
|
||||
"sha256:7b60c045b80709e4e3c085bab9b691e71761b44c2b42dbb047b8b498e7bc16b3",
|
||||
"sha256:8e6af2f736734aef8ed6f278f9f552ec7f37b1a6b98e59b887484a840757f67d",
|
||||
"sha256:9ac2298e486524331e26390eac14e4627effd3f8e001d4266ed9d8f1d2d31cce",
|
||||
"sha256:9ba650f493a9bc1f24feca1d90fce0e5dd41088a252ac9840131dfbdbf3815ca",
|
||||
"sha256:a02a4a385e394e46012dc83d2e8fd6523f039bb52997c1c34a2e0dd49ed839c1",
|
||||
"sha256:a3ceee84114d9f5711fa0f4db9c652af0e4636c89eabc9b7f03a3882569dd1ed",
|
||||
"sha256:a72b82ac1910f2cf61a49139f4974f994984475f771b0faa730839607eeedddf",
|
||||
"sha256:ab136ac51027e7c484c53138a0fab4a8a51e80d05162eb7b1585583bcfdbad27",
|
||||
"sha256:c095b224300bcac61e6c445e27f9046981b1ac20d891b2f1714da89d34c637c8",
|
||||
"sha256:c5cc52d16c06dc2521340d69adda78a8e1031705924e103c0eb8fc8af861d810",
|
||||
"sha256:d612e9833a89e8177f8c1dc68d7b4ff98d3186cd331acd616b01bbdab67d3a7b",
|
||||
"sha256:e828376a23c66c6fe90dcea24b4b72cd774f555a6ee94081670872918df87a19",
|
||||
"sha256:e9767c7ab2eb552796440168d5c6e23a99ecaade08dda16266d43ad461730192",
|
||||
"sha256:ebf8b800d42d217e4710d1582b0c8bff20cdcb4faad7c7213e52644034300924"
|
||||
"sha256:15f0bf7cd80020f165635595e197603aedb37fddf4164ad5ae226afc43242f7b",
|
||||
"sha256:1756dc72e192c670490e38c788c3a35f901adc74ee436e5131d5a3e85fdd7dc6",
|
||||
"sha256:1d1eb490da54679d724b08ef3ee04530849023670c4ba7e400ed2cdf906720c4",
|
||||
"sha256:228402625796821f08706f58cc42a3c51c9897d723550babaefe4feec2b6dacc",
|
||||
"sha256:3928534fa00a2aabfcfdb439c08ba37fbe99ab0cf57776c8db8d2b73a51693ba",
|
||||
"sha256:3d2a295b1086d450981f73d3561ac204a0cc9c8ded386a4a34327d918f3b1d0a",
|
||||
"sha256:4fd8621a309db6ec23ef1369f43cdf7a9b0dc217d8ff9ca4095a6e932b379bda",
|
||||
"sha256:54fe55a1694ffe608c8e4c5183e83cab7a91f3e5c84bd6f188868d6676c12aba",
|
||||
"sha256:618887be4ad754228c0cbba7631f6574608b4430fe93974e6322324f1304fdac",
|
||||
"sha256:69130efb6efa936de601cb135a8a4eec1caccd4ea2b784237145ff4075c2d3ae",
|
||||
"sha256:6e7f78eeac82140bde7e60e975c6e6b1b678a4dd377782ab63319c1c78bf3aa1",
|
||||
"sha256:6ee760cdb84e43574da6b3f2f1fc1251e8acf87253900d28a06451c5f5de39e9",
|
||||
"sha256:97cb1b7cd2c46e87b0a26651eccd2bbb8c758035efd1635ebb81ac36aa76a88c",
|
||||
"sha256:abfa774dbadacc849121ed92eae05189d226daab583388b499472e1bbb17ef69",
|
||||
"sha256:b30c339eb58355f51f4f54dd61d785f1ff58c86bca1c3a5916977631d121867b"
|
||||
],
|
||||
"version": "==17.1.2"
|
||||
"version": "==18.0.0"
|
||||
},
|
||||
"qtconsole": {
|
||||
"hashes": [
|
||||
@@ -640,6 +630,13 @@
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:7dede29f121071da9873e7b8c98091874617858e790dc364ffaab4b09d81216c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.0b3"
|
||||
},
|
||||
"terminado": {
|
||||
"hashes": [
|
||||
"sha256:55abf9ade563b8f9be1f34e4233c7b7bde726059947a593322e8a553cc4c067a",
|
||||
@@ -663,9 +660,9 @@
|
||||
},
|
||||
"tornado": {
|
||||
"hashes": [
|
||||
"sha256:00ebd485a52bd7eaa3f35bdf8ab43c109aaa2edc722849b6905c1ffd8c958e82"
|
||||
"sha256:d3b719a0cb7094e2b1ca94b31f4b601639fa7ad01a548a1a2ccdd6cbdfd56671"
|
||||
],
|
||||
"version": "==6.0a1"
|
||||
"version": "==6.0b1"
|
||||
},
|
||||
"traitlets": {
|
||||
"hashes": [
|
||||
@@ -676,29 +673,27 @@
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:023625bfa9359e29bd6e24cac2a4503495b49761d48a5f1e38333fc4ac4d93fe",
|
||||
"sha256:07591f7a5fdff50e2e566c4c1e9df545c75d21e27d98d18cb405727ed0ef329c",
|
||||
"sha256:153e526b0f4ffbfada72d0bb5ffe8574ba02803d2f3a9c605c8cf99dfedd72a2",
|
||||
"sha256:3ad2bdcd46a4a1518d7376e9f5016d17718a9ed3c6a3f09203d832f6c165de4a",
|
||||
"sha256:3ea98c84df53ada97ee1c5159bb3bc784bd734231235a1ede14c8ae0775049f7",
|
||||
"sha256:51a7141ccd076fa561af107cfb7a8b6d06a008d92451a1ac7e73149d18e9a827",
|
||||
"sha256:52c93cd10e6c24e7ac97e8615da9f224fd75c61770515cb323316c30830ddb33",
|
||||
"sha256:6344c84baeda3d7b33e157f0b292e4dd53d05ddb57a63f738178c01cac4635c9",
|
||||
"sha256:64699ca1b3bd5070bdeb043e6d43bc1d0cebe08008548f4a6bee782b0ecce032",
|
||||
"sha256:74903f2e56bbffe29282ef8a5487d207d10be0f8513b41aff787d954a4cf91c9",
|
||||
"sha256:7891710dba83c29ee2bd51ecaa82f60f6bede40271af781110c08be134207bf2",
|
||||
"sha256:91976c56224e26c256a0de0f76d2004ab885a29423737684b4f7ebdd2f46dde2",
|
||||
"sha256:9bad678a576ecc71f25eba9f1e3fd8d01c28c12a2834850b458428b3e855f062",
|
||||
"sha256:b4726339a4c180a8b6ad9d8b50d2b6dc247e1b79b38fe2290549c98e82e4fd15",
|
||||
"sha256:ba36f6aa3f8933edf94ea35826daf92cbb3ec248b89eccdc053d4a815d285357",
|
||||
"sha256:bbc96bde544fd19e9ef168e4dfa5c3dfe704bfa78128fa76f361d64d6b0f731a",
|
||||
"sha256:c0c927f1e44469056f7f2dada266c79b577da378bbde3f6d2ada726d131e4824",
|
||||
"sha256:c0f9a3708008aa59f560fa1bd22385e05b79b8e38e0721a15a8402b089243442",
|
||||
"sha256:f0bf6f36ff9c5643004171f11d2fdc745aa3953c5aacf2536a0685db9ceb3fb1",
|
||||
"sha256:f5be39a0146be663cbf210a4d95c3c58b2d7df7b043c9047c5448e358f0550a2",
|
||||
"sha256:fcd198bf19d9213e5cbf2cde2b9ef20a9856e716f76f9476157f90ae6de06cc6"
|
||||
"sha256:035a54ede6ce1380599b2ce57844c6554666522e376bd111eb940fbc7c3dad23",
|
||||
"sha256:037c35f2741ce3a9ac0d55abfcd119133cbd821fffa4461397718287092d9d15",
|
||||
"sha256:049feae7e9f180b64efacbdc36b3af64a00393a47be22fa9cb6794e68d4e73d3",
|
||||
"sha256:19228f7940beafc1ba21a6e8e070e0b0bfd1457902a3a81709762b8b9039b88d",
|
||||
"sha256:2ea681e91e3550a30c2265d2916f40a5f5d89b59469a20f3bad7d07adee0f7a6",
|
||||
"sha256:3a6b0a78af298d82323660df5497bcea0f0a4a25a0b003afd0ce5af049bd1f60",
|
||||
"sha256:5385da8f3b801014504df0852bf83524599df890387a3c2b17b7caa3d78b1773",
|
||||
"sha256:606d8afa07eef77280c2bf84335e24390055b478392e1975f96286d99d0cb424",
|
||||
"sha256:69245b5b23bbf7fb242c9f8f08493e9ecd7711f063259aefffaeb90595d62287",
|
||||
"sha256:6f6d839ab09830d59b7fa8fb6917023d8cb5498ee1f1dbd82d37db78eb76bc99",
|
||||
"sha256:730888475f5ac0e37c1de4bd05eeb799fdb742697867f524dc8a4cd74bcecc23",
|
||||
"sha256:9819b5162ffc121b9e334923c685b0d0826154e41dfe70b2ede2ce29034c71d8",
|
||||
"sha256:9e60ef9426efab601dd9aa120e4ff560f4461cf8442e9c0a2b92548d52800699",
|
||||
"sha256:af5fbdde0690c7da68e841d7fc2632345d570768ea7406a9434446d7b33b0ee1",
|
||||
"sha256:b64efdbdf3bbb1377562c179f167f3bf301251411eb5ac77dec6b7d32bcda463",
|
||||
"sha256:bac5f444c118aeb456fac1b0b5d14c6a71ea2a42069b09c176f75e9bd4c186f6",
|
||||
"sha256:bda9068aafb73859491e13b99b682bd299c1b5fd50644d697533775828a28ee0",
|
||||
"sha256:d659517ca116e6750101a1326107d3479028c5191f0ecee3c7203c50f5b915b0",
|
||||
"sha256:eddd3fb1f3e0f82e5915a899285a39ee34ce18fd25d89582bc89fc9fb16cd2c6"
|
||||
],
|
||||
"version": "==1.2.0"
|
||||
"version": "==1.3.1"
|
||||
},
|
||||
"ujson": {
|
||||
"hashes": [
|
||||
|
||||
123
docs/contributing.md
Normal file
123
docs/contributing.md
Normal file
@@ -0,0 +1,123 @@
|
||||
First, you might want to see the basic ways to <a href="https://fastapi.tiangolo.com/help-fastapi/" target="_blank">help FastAPI and get help</a>.
|
||||
|
||||
## Developing
|
||||
|
||||
If you already cloned the repository and you know that you need to deep dive in the code, here are some guidelines to set up your environment.
|
||||
|
||||
|
||||
### Pipenv
|
||||
|
||||
If you are using <a href="https://pipenv.readthedocs.io/en/latest/" target="_blank">Pipenv</a>, you can create a virtual environment and install the packages with:
|
||||
|
||||
```bash
|
||||
pipenv install --dev
|
||||
```
|
||||
|
||||
Then you can activate that virtual environment with:
|
||||
|
||||
```bash
|
||||
pipenv shell
|
||||
```
|
||||
|
||||
|
||||
### No Pipenv
|
||||
|
||||
If you are not using Pipenv, you can create a virtual environment with your preferred tool, and install the packages listed in the file `Pipfile`.
|
||||
|
||||
|
||||
### Flit
|
||||
|
||||
**FastAPI** uses <a href="https://flit.readthedocs.io/en/latest/index.html" target="_blank">Flit</a> to build, package and publish the project.
|
||||
|
||||
If you installed the development dependencies with one of the methods above, you already have the `flit` command.
|
||||
|
||||
To install your local version of FastAPI as a package in your local environment, run:
|
||||
|
||||
```bash
|
||||
flit install --symlink
|
||||
```
|
||||
|
||||
It will install your local FastAPI in your local environment.
|
||||
|
||||
|
||||
#### Using your local FastAPI
|
||||
|
||||
If you create a Python file that imports and uses FastAPI, and run it with the Python from your local environment, it will use your local FastAPI source code.
|
||||
|
||||
And if you update that local FastAPI source code, as it is installed with `--symlink`, when you run that Python file again, it will use the fresh version of FastAPI you just edited.
|
||||
|
||||
That way, you don't have to "install" your local version to be able to test every change.
|
||||
|
||||
|
||||
### Format
|
||||
|
||||
There is a script that you can run that will format and clean all your code:
|
||||
|
||||
```bash
|
||||
bash scripts/lint.sh
|
||||
```
|
||||
|
||||
It will also auto-sort all your imports.
|
||||
|
||||
For it to sort them correctly, you need to have FastAPI installed locally in your environment, with the command in the section above:
|
||||
|
||||
```bash
|
||||
flit install --symlink
|
||||
```
|
||||
|
||||
|
||||
### Docs
|
||||
|
||||
The documentation uses <a href="https://www.mkdocs.org/" target="_blank">MkDocs</a>.
|
||||
|
||||
All the documentation is in Markdown format in the directory `./docs`.
|
||||
|
||||
Many of the tutorials have blocks of code.
|
||||
|
||||
In most of the cases, these blocks of code are actual complete applicactions that can be run as is.
|
||||
|
||||
In fact, those blocks of code are not written inside the Markdown, they are Python files in the `./docs/src/` directory.
|
||||
|
||||
And those Python files are included/injected in the documentation when generating the site.
|
||||
|
||||
|
||||
#### Docs for tests
|
||||
|
||||
Most of the tests actually run against the example source files in the documentation.
|
||||
|
||||
This helps making sure that:
|
||||
|
||||
* The documentation is up to date.
|
||||
* The documentation examples can be run as is.
|
||||
* Most of the features are covered by the documentation, ensured by the coverage tests.
|
||||
|
||||
During local development, there is a script that builds the site and checks for any changes, live-reloading:
|
||||
|
||||
```bash
|
||||
bash scripts/docs-live.sh
|
||||
```
|
||||
|
||||
It will serve the documentation on `http://0.0.0.0:8008`.
|
||||
|
||||
That way, you can edit the documentation/source files and see the changes live.
|
||||
|
||||
#### Apps and docs at the same time
|
||||
|
||||
And if you run the examples with, e.g.:
|
||||
|
||||
```bash
|
||||
uvicorn tutorial001:app --debug
|
||||
```
|
||||
|
||||
as Uvicorn by default will use the port `8000`, the documentation on port `8008` won't clash.
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
There is a script that you can run locally to test all the code and generate coverage reports in HTML:
|
||||
|
||||
```bash
|
||||
bash scripts/test-cov-html.sh
|
||||
```
|
||||
|
||||
This command generates a directory `./htmlcov/`, if you open the file `./htmlcov/index.html` in your browser, you can explore interactively the regions of code that are covered by the tests, and notice if there is any region missing.
|
||||
100
docs/help-fastapi.md
Normal file
100
docs/help-fastapi.md
Normal file
@@ -0,0 +1,100 @@
|
||||
Are you liking **FastAPI**?
|
||||
|
||||
Would you like to help FastAPI, other users, and the author?
|
||||
|
||||
Or would you like to get help with **FastAPI**?
|
||||
|
||||
There are very simple ways to help (several involve just one or two clicks).
|
||||
|
||||
And there are several ways to get help too.
|
||||
|
||||
|
||||
## Star **FastAPI** in GitHub
|
||||
|
||||
You can "star" FastAPI in GitHub (clicking the star button at the top right): <a href="https://github.com/tiangolo/fastapi" target="_blank">https://github.com/tiangolo/fastapi</a>.
|
||||
|
||||
By adding a star, other users will be able to find it more easily and see that it has been already useful for others.
|
||||
|
||||
|
||||
## Watch the GitHub repository for releases
|
||||
|
||||
You can "watch" FastAPI in GitHub (clicking the "watch" button at the top right): <a href="https://github.com/tiangolo/fastapi" target="_blank">https://github.com/tiangolo/fastapi</a>.
|
||||
|
||||
There you can select "Releases only".
|
||||
|
||||
Doing it, you will receive notifications (in your email) whenever there's a new release (a new version) of **FastAPI** with bug fixes and new features.
|
||||
|
||||
|
||||
## Connect with the author
|
||||
|
||||
You can connect with me (Sebastián Ramírez / `tiangolo`), the author.
|
||||
|
||||
You can:
|
||||
|
||||
* <a href="https://github.com/tiangolo" target="_blank">Follow me on **GitHub**</a>.
|
||||
* See other Open Source projects I have created that could help you.
|
||||
* Follow me to see when I create a new Open Source project.
|
||||
* <a href="https://twitter.com/tiangolo" target="_blank">Follow me on **Twitter**</a>.
|
||||
* Tell me how you use FastAPI (I love to hear that).
|
||||
* Ask questions.
|
||||
* <a href="https://www.linkedin.com/in/tiangolo/" target="_blank">Connect with me on **Linkedin**</a>.
|
||||
* Talk to me.
|
||||
* Endorse me or recommend me :)
|
||||
* <a href="https://medium.com/@tiangolo" target="_blank">Read what I write (or follow me) on **Medium**</a>.
|
||||
* Read other ideas, articles and tools I have created.
|
||||
* Follow me to see when I publish something new.
|
||||
|
||||
|
||||
## Tweet about **FastAPI**
|
||||
|
||||
<a href="http://twitter.com/home/?status=I'm loving FastAPI because... https://github.com/tiangolo/fastapi cc @tiangolo" target="_blank">Tweet about **FastAPI**</a> and let me and others why you like it.
|
||||
|
||||
## Let me know how are you using **FastAPI**
|
||||
|
||||
I love to hear about how **FastAPI** is being used, what have you liked in it, in which project/company are you using it, etc.
|
||||
|
||||
You can let me know:
|
||||
|
||||
* <a href="http://twitter.com/home/?status=Hey @tiangolo, I'm using FastAPI at..." target="_blank">On **Twitter**</a>.
|
||||
* <a href="https://www.linkedin.com/in/tiangolo/" target="_blank">On **Linkedin**</a>.
|
||||
* <a href="https://medium.com/@tiangolo" target="_blank">On **Medium**</a>.
|
||||
|
||||
## 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>.
|
||||
|
||||
## Help others with issues in GitHub
|
||||
|
||||
You can see <a href="https://github.com/tiangolo/fastapi/issues" target="_blank">existing issues</a> and try and help others.
|
||||
|
||||
## Watch the GitHub repository
|
||||
|
||||
You can "watch" FastAPI in GitHub (clicking the "watch" button at the top right): <a href="https://github.com/tiangolo/fastapi" target="_blank">https://github.com/tiangolo/fastapi</a>.
|
||||
|
||||
If you select "Watching" instead of "Releases only", you will receive notifications when someone creates a new issue.
|
||||
|
||||
Then you can try and help them solving those issues.
|
||||
|
||||
## Create issues
|
||||
|
||||
You can <a href="https://github.com/tiangolo/fastapi/issues/new/choose" target="_blank">create a new issue</a> in the GitHub repository, for example to:
|
||||
|
||||
* Report a bug/issue.
|
||||
* Suggest a new feature.
|
||||
* Ask a question.
|
||||
|
||||
## Create a Pull Request
|
||||
|
||||
You can <a href="https://github.com/tiangolo/fastapi" target="_blank">create a Pull Request</a>, for example:
|
||||
|
||||
* To fix a typo you found on the documentation.
|
||||
* To propose new documentation sections.
|
||||
* To fix an existing issue/bug.
|
||||
* To add a new feature.
|
||||
|
||||
---
|
||||
|
||||
Thanks!
|
||||
BIN
docs/img/tutorial/sql-databases/image01.png
Normal file
BIN
docs/img/tutorial/sql-databases/image01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
BIN
docs/img/tutorial/sql-databases/image02.png
Normal file
BIN
docs/img/tutorial/sql-databases/image02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
BIN
docs/img/tutorial/sub-applications/image01.png
Normal file
BIN
docs/img/tutorial/sub-applications/image01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
docs/img/tutorial/sub-applications/image02.png
Normal file
BIN
docs/img/tutorial/sub-applications/image02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -1,13 +1,51 @@
|
||||
## Next
|
||||
|
||||
## 0.6.0
|
||||
|
||||
* Update SQL with SQLAlchemy tutorial at <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">https://fastapi.tiangolo.com/tutorial/sql-databases/</a> using the new official `request.state`. PR <a href="https://github.com/tiangolo/fastapi/pull/45" target="_blank">#45</a>.
|
||||
|
||||
* Upgrade Starlette to version `0.11.1` and add required compatibility changes. PR <a href="https://github.com/tiangolo/fastapi/pull/44" target="_blank">#44</a>.
|
||||
|
||||
## 0.5.1
|
||||
|
||||
* Add section about <a href="https://fastapi.tiangolo.com/help-fastapi/" target="_blank">helping and getting help with **FastAPI**</a>.
|
||||
|
||||
* Add note about <a href="https://fastapi.tiangolo.com/tutorial/path-params/#order-matters" target="_blank">path operations order in docs</a>.
|
||||
|
||||
* Update <a href="https://fastapi.tiangolo.com/tutorial/handling-errors/" target="_blank">section about error handling</a> with more information and make relation with Starlette error handling utilities more explicit. PR <a href="https://github.com/tiangolo/fastapi/pull/41" target="_blank">#41</a>.
|
||||
|
||||
* Add <a href="" target="_blank">Development - Contributing section to the docs</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/42" target="_blank">#42</a>.
|
||||
|
||||
## 0.5.0
|
||||
|
||||
* Add new `HTTPException` with support for custom headers. With new documentation for handling errors at: <a href="https://fastapi.tiangolo.com/tutorial/handling-errors/" target="_blank">https://fastapi.tiangolo.com/tutorial/handling-errors/</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/35" target="_blank">#35</a>.
|
||||
|
||||
* Add <a href="https://fastapi.tiangolo.com/tutorial/using-request-directly/" target="_blank">documentation to use Starlette `Request` object</a> directly. Check <a href="https://github.com/tiangolo/fastapi/pull/25" target="_blank">#25</a> by <a href="https://github.com/euri10" target="_blank">@euri10</a>.
|
||||
|
||||
* Add issue templates to simplify reporting bugs, getting help, etc: <a href="https://github.com/tiangolo/fastapi/pull/34" target="_blank">#34</a>.
|
||||
|
||||
* Update example for the SQLAlchemy tutorial at <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">https://fastapi.tiangolo.com/tutorial/sql-databases/</a> using middleware and database session attached to request.
|
||||
|
||||
## 0.4.0
|
||||
|
||||
* Add `openapi_prefix`, support for reverse proxy and mounting sub-applicaitons. See the docs at <a href="https://fastapi.tiangolo.com/tutorial/sub-applications-proxy/" target="_blank">https://fastapi.tiangolo.com/tutorial/sub-applications-proxy/</a>: <a href="https://github.com/tiangolo/fastapi/pull/26" target="_blank">#26</a> by <a href="https://github.com/kabirkhan" target="_blank">@kabirkhan</a>.
|
||||
|
||||
* Update <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">docs/tutorial for SQLAlchemy</a> including note about *DB Browser for SQLite*.
|
||||
|
||||
## 0.3.0
|
||||
|
||||
* Fix/add SQLAlchemy support, including ORM, and update <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">docs for SQLAlchemy</a>: <a href="https://github.com/tiangolo/fastapi/pull/30" target="_blank">#30</a>.
|
||||
|
||||
## 0.2.1
|
||||
|
||||
* Fix `jsonable_encoder` for Pydantic models with `Config` but without `json_encoders`: <a href="https://github.com/tiangolo/fastapi/pull/29" target="_blank">#29</a>.
|
||||
|
||||
## 0.2.0
|
||||
|
||||
* Fix typos in Security section: <a href="https://github.com/tiangolo/fastapi/pull/24" target="_blank">#24</a> by <a href="https://github.com/kkinder" target="_blank">@kkinder</a>
|
||||
* Fix typos in Security section: <a href="https://github.com/tiangolo/fastapi/pull/24" target="_blank">#24</a> by <a href="https://github.com/kkinder" target="_blank">@kkinder</a>.
|
||||
|
||||
* Add support for Pydantic custom JSON encoders: <a href="https://github.com/tiangolo/fastapi/pull/21" target="_blank">#21</a> by <a href="https://github.com/euri10" target="_blank">@euri10</a>
|
||||
* Add support for Pydantic custom JSON encoders: <a href="https://github.com/tiangolo/fastapi/pull/21" target="_blank">#21</a> by <a href="https://github.com/euri10" target="_blank">@euri10</a>.
|
||||
|
||||
## 0.1.19
|
||||
|
||||
* Upgrade Starlette version to the current latest `0.10.1`: <a href="https://github.com/tiangolo/fastapi/pull/17" target="_blank">#17</a> by <a href="https://github.com/euri10" target="_blank">@euri10</a>
|
||||
* Upgrade Starlette version to the current latest `0.10.1`: <a href="https://github.com/tiangolo/fastapi/pull/17" target="_blank">#17</a> by <a href="https://github.com/euri10" target="_blank">@euri10</a>.
|
||||
|
||||
12
docs/src/handling_errors/tutorial001.py
Normal file
12
docs/src/handling_errors/tutorial001.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
items = {"foo": "The Foo Wrestlers"}
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def create_item(item_id: str):
|
||||
if item_id not in items:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
return {"item": items[item_id]}
|
||||
16
docs/src/handling_errors/tutorial002.py
Normal file
16
docs/src/handling_errors/tutorial002.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
items = {"foo": "The Foo Wrestlers"}
|
||||
|
||||
|
||||
@app.get("/items-header/{item_id}")
|
||||
async def create_item_header(item_id: str):
|
||||
if item_id not in items:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Item not found",
|
||||
headers={"X-Error": "There goes my error"},
|
||||
)
|
||||
return {"item": items[item_id]}
|
||||
15
docs/src/handling_errors/tutorial003.py
Normal file
15
docs/src/handling_errors/tutorial003.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.responses import PlainTextResponse
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_exception(request, exc):
|
||||
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Hello World"}
|
||||
@@ -1,10 +1,13 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def read_item(item_id: UUID):
|
||||
return {"item_id": item_id}
|
||||
@app.get("/users/me")
|
||||
async def read_user_me():
|
||||
return {"user_id": "the current user"}
|
||||
|
||||
|
||||
@app.get("/users/{user_id}")
|
||||
async def read_user(user_id: str):
|
||||
return {"user_id": user_id}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi import Depends, FastAPI, HTTPException, Security
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from pydantic import BaseModel
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
fake_users_db = {
|
||||
"johndoe": {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import jwt
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi import Depends, FastAPI, HTTPException, Security
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from jwt import PyJWTError
|
||||
from passlib.context import CryptContext
|
||||
from pydantic import BaseModel
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
# to get a string like this run:
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
from sqlalchemy import Boolean, Column, Integer, String, create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base, declared_attr
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from starlette.requests import Request
|
||||
|
||||
# SQLAlchemy specific code, as with any other app
|
||||
SQLALCHEMY_DATABASE_URI = "postgresql://user:password@postgresserver/db"
|
||||
SQLALCHEMY_DATABASE_URI = "sqlite:///./test.db"
|
||||
# SQLALCHEMY_DATABASE_URI = "postgresql://user:password@postgresserver/db"
|
||||
|
||||
engine = create_engine(SQLALCHEMY_DATABASE_URI, convert_unicode=True)
|
||||
db_session = scoped_session(
|
||||
sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URI, connect_args={"check_same_thread": False}
|
||||
)
|
||||
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
class CustomBase:
|
||||
@@ -30,15 +31,37 @@ class User(Base):
|
||||
is_active = Column(Boolean(), default=True)
|
||||
|
||||
|
||||
def get_user(username, db_session):
|
||||
return db_session.query(User).filter(User.id == username).first()
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
db_session = Session()
|
||||
|
||||
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, user_id: int):
|
||||
return db_session.query(User).filter(User.id == user_id).first()
|
||||
|
||||
|
||||
# FastAPI specific code
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/users/{username}")
|
||||
def read_user(username: str):
|
||||
user = get_user(username, db_session)
|
||||
@app.get("/users/{user_id}")
|
||||
def read_user(request: Request, user_id: int):
|
||||
user = get_user(request.state.db, user_id=user_id)
|
||||
return user
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def close_db(request: Request, call_next):
|
||||
request.state.db = Session()
|
||||
response = await call_next(request)
|
||||
request.state.db.close()
|
||||
return response
|
||||
|
||||
19
docs/src/sub_applications/tutorial001.py
Normal file
19
docs/src/sub_applications/tutorial001.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/app")
|
||||
def read_main():
|
||||
return {"message": "Hello World from main app"}
|
||||
|
||||
|
||||
subapi = FastAPI(openapi_prefix="/subapi")
|
||||
|
||||
|
||||
@subapi.get("/sub")
|
||||
def read_sub():
|
||||
return {"message": "Hello World from sub API"}
|
||||
|
||||
|
||||
app.mount("/subapi", subapi)
|
||||
10
docs/src/using_request_directly/tutorial001.py
Normal file
10
docs/src/using_request_directly/tutorial001.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.requests import Request
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
def read_root(item_id: str, request: Request):
|
||||
client_host = request.client.host
|
||||
return {"client_host": client_host, "item_id": item_id}
|
||||
@@ -1 +0,0 @@
|
||||
Coming soon...
|
||||
99
docs/tutorial/handling-errors.md
Normal file
99
docs/tutorial/handling-errors.md
Normal file
@@ -0,0 +1,99 @@
|
||||
There are many situations in where you need to notify an error to the client that is using your API.
|
||||
|
||||
This client could be a browser with a frontend, the code from someone else, an IoT device, etc.
|
||||
|
||||
You could need to tell that client that:
|
||||
|
||||
* He doesn't have enough privileges for that operation.
|
||||
* He doesn't have access to that resource.
|
||||
* The item he was trying to access doesn't exist.
|
||||
* etc.
|
||||
|
||||
In these cases, you would normally return an **HTTP status code** in the range of **400** (from 400 to 499).
|
||||
|
||||
This is similar to the 200 HTTP status codes (from 200 to 299). Those "200" status codes mean that somehow there was a "success" in the request.
|
||||
|
||||
The status codes in the 400 range mean that there was an error from the client.
|
||||
|
||||
Remember all those **"404 Not Found"** errors (and jokes)?
|
||||
|
||||
## Use `HTTPException`
|
||||
|
||||
To return HTTP responses with errors to the client you use `HTTPException`.
|
||||
|
||||
### Import `HTTPException`
|
||||
|
||||
```Python hl_lines="1"
|
||||
{!./src/handling_errors/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Raise an `HTTPException` in your code
|
||||
|
||||
`HTTPException` is a normal Python exception with additional data relevant for APIs.
|
||||
|
||||
Because it's a Python exception, you don't `return` it, you `raise` it.
|
||||
|
||||
This also means that if you are inside a utility function that you are calling inside of your path operation function, and you raise the `HTTPException` from inside of that utility function, it won't run the rest of the code in the path operation function, it will terminate that request right away and send the HTTP error from the `HTTPException` to the client.
|
||||
|
||||
The benefit of raising an exception over `return`ing a value will be more evident in the section about Dependencies and Security.
|
||||
|
||||
In this example, when the client request an item by an ID that doesn't exist, raise an exception with a status code of `404`:
|
||||
|
||||
```Python hl_lines="11"
|
||||
{!./src/handling_errors/tutorial001.py!}
|
||||
```
|
||||
|
||||
### The resulting response
|
||||
|
||||
If the client requests `http://example.com/items/foo` (an `item_id` `"foo"`), he will receive an HTTP status code of 200, and a JSON response of:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"item": "The Foo Wrestlers"
|
||||
}
|
||||
```
|
||||
|
||||
But if the client requests `http://example.com/items/bar` (a non-existent `item_id` `"bar"`), he will receive an HTTP status code of 404 (the "not found" error), and a JSON response of:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"detail": "Item not found"
|
||||
}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
When raising an `HTTPException`, you can pass any value that can be converted to JSON as the parameter `detail`, not only `str`.
|
||||
|
||||
You could pass a `dict`, a `list`, etc.
|
||||
|
||||
They are handled automatically by **FastAPI** and converted to JSON.
|
||||
|
||||
### Adding 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.
|
||||
|
||||
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
|
||||
|
||||
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>.
|
||||
|
||||
For example, you could override the default exception handler with:
|
||||
|
||||
```Python hl_lines="2 3 8 9 10"
|
||||
{!./src/handling_errors/tutorial003.py!}
|
||||
```
|
||||
|
||||
...this would make it return "plain text" responses with the errors, instead of JSON responses.
|
||||
|
||||
!!! info
|
||||
Note that in this example we set the exception handler with Starlette's `HTTPException` instead of FastAPI's `HTTPException`.
|
||||
|
||||
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 catched by your exception handler.
|
||||
@@ -98,6 +98,24 @@ You can use the same type declarations with `str`, `float`, `bool` and many othe
|
||||
|
||||
These are explored in the next chapters of the tutorial.
|
||||
|
||||
|
||||
## Order matters
|
||||
|
||||
When creating *path operations*, you can find situations where you have a fixed path.
|
||||
|
||||
Like `/users/me`, let's say that it's to get data about the current user.
|
||||
|
||||
And then you can also have a path `/users/{user_id}` to get data about a specific user by some user ID.
|
||||
|
||||
Because path operations are evaluated in order, you need to make sure that the path for `/users/me` is declared before the one for `/users/{user_id}`:
|
||||
|
||||
```Python hl_lines="6 11"
|
||||
{!./src/path_params/tutorial003.py!}
|
||||
```
|
||||
|
||||
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"`.
|
||||
|
||||
|
||||
## Recap
|
||||
|
||||
With **FastAPI**, by using short, intuitive and standard Python type declarations, you get:
|
||||
|
||||
@@ -81,7 +81,7 @@ And another utility to verify if a received password matches the hash stored.
|
||||
|
||||
And another one to authenticate and return a user.
|
||||
|
||||
```Python hl_lines="7 51 58 59 62 63 72 73 74 75 76 77 78"
|
||||
```Python hl_lines="7 50 57 58 61 62 71 72 73 74 75 76 77"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
@@ -112,7 +112,7 @@ Define a Pydantic Model that will be used in the token endpoint for the response
|
||||
|
||||
Create a utility function to generate a new access token.
|
||||
|
||||
```Python hl_lines="3 6 14 15 16 17 31 32 33 81 82 83 84 85 86 87 88 89"
|
||||
```Python hl_lines="3 6 13 14 15 16 30 31 32 80 81 82 83 84 85 86 87 88"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
@@ -124,7 +124,7 @@ Decode the received token, verify it, and return the current user.
|
||||
|
||||
If the token is invalid, return an HTTP error right away.
|
||||
|
||||
```Python hl_lines="92 93 94 95 96 97 98 99 100 101"
|
||||
```Python hl_lines="91 92 93 94 95 96 97 98 99 100"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
@@ -134,7 +134,7 @@ Create a `timedelta` with the expiration time of the token.
|
||||
|
||||
Create a real JWT access token and return it.
|
||||
|
||||
```Python hl_lines="115 116 117 118 119"
|
||||
```Python hl_lines="114 115 116 117 118"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
|
||||
@@ -78,9 +78,9 @@ Now, get the user data from the (fake) database, using the `username` from the f
|
||||
|
||||
If there is no such user, we return an error saying "incorrect username or password".
|
||||
|
||||
For the error, we use the exception `HTTPException` provided by Starlette directly:
|
||||
For the error, we use the exception `HTTPException`:
|
||||
|
||||
```Python hl_lines="4 74 75 76"
|
||||
```Python hl_lines="1 73 74 75"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -108,7 +108,7 @@ If your database is stolen, the thief won't have your users' plaintext passwords
|
||||
|
||||
So, the thief won't be able to try to use that password in another system (as many users use the same password everywhere, this would be dangerous).
|
||||
|
||||
```Python hl_lines="77 78 79 80"
|
||||
```Python hl_lines="76 77 78 79"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -146,7 +146,7 @@ For this simple example, we are going to just be completely insecure and return
|
||||
|
||||
But for now, let's focus on the specific details we need.
|
||||
|
||||
```Python hl_lines="82"
|
||||
```Python hl_lines="81"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -162,7 +162,7 @@ Both of these dependencies will just return an HTTP error if the user doesn't ex
|
||||
|
||||
So, in our endpoint, we will only get a user if the user exists, was correctly authenticated, and is active:
|
||||
|
||||
```Python hl_lines="57 58 59 60 61 62 63 66 67 68 69 86"
|
||||
```Python hl_lines="56 57 58 59 60 61 62 65 66 67 68 85"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ You can easily adapt it to any database supported by SQLAlchemy, like:
|
||||
* Oracle
|
||||
* Microsoft SQL Server, etc.
|
||||
|
||||
In this example, we'll use **PostgreSQL**.
|
||||
In this example, we'll use **SQLite**, because it uses a single file and Python has integrated support. So, you can copy this example and run it as is.
|
||||
|
||||
Later, for your production application, you might want to use a database server like **PostgreSQL**.
|
||||
|
||||
!!! note
|
||||
Notice that most of the code is the standard `SQLAlchemy` code you would use with any framework.
|
||||
@@ -23,41 +25,89 @@ In this example, we'll use **PostgreSQL**.
|
||||
|
||||
For now, don't pay attention to the rest, only the imports:
|
||||
|
||||
```Python hl_lines="3 4 5"
|
||||
```Python hl_lines="2 3 4"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Define the database
|
||||
|
||||
Define the database that SQLAlchemy should connect to:
|
||||
Define the database that SQLAlchemy should "connect" to:
|
||||
|
||||
```Python hl_lines="8"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
This is the main line that you would have to modify if you wanted to use a different database than **PostgreSQL**.
|
||||
In this example, we are "connecting" to a SQLite database (opening a file with the SQLite database).
|
||||
|
||||
## Create the SQLAlchemy `engine`
|
||||
The file will be located at the same directory in the file `test.db`. That's why the last part is `./test.db`.
|
||||
|
||||
```Python hl_lines="10"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
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"
|
||||
```
|
||||
|
||||
## Create a `scoped_session`
|
||||
...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`
|
||||
|
||||
```Python hl_lines="11 12 13"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! note "Very Technical Details"
|
||||
Don't worry too much if you don't understand this. You can still use the code.
|
||||
### Note
|
||||
|
||||
This `scoped_session` is a feature of SQLAlchemy.
|
||||
The argument:
|
||||
|
||||
The resulting object, the `db_session` can then be used anywhere a a normal SQLAlchemy session.
|
||||
```Python
|
||||
connect_args={"check_same_thread": False}
|
||||
```
|
||||
|
||||
...is needed only for `SQLite`. It's not needed for other databases.
|
||||
|
||||
!!! info "Technical Details"
|
||||
|
||||
That argument `check_same_thread` is there mainly to be able to run the tests that cover this example.
|
||||
|
||||
It can be used as a global because it is implemented to work independently on each "<abbr title="A sequence of code being executed by the program, while at the same time, or at intervals, there can be others being executed too.">thread</abbr>", so the actions you perform with it in one path operation function won't affect the actions performed (possibly concurrently) by other path operation functions.
|
||||
|
||||
## Create a `Session` class
|
||||
|
||||
Each instance of the `Session` class will have a connection to the database.
|
||||
|
||||
This 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.
|
||||
|
||||
```Python hl_lines="14"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Create a middleware to handle sessions
|
||||
|
||||
Now let's temporarily jump to the end of the file, to use the `Session` class we created above.
|
||||
|
||||
We need to have an independent `Session` per request, use the same session through all the request and then close it after the request is finished.
|
||||
|
||||
And then a new session will be created for the next request.
|
||||
|
||||
For that, we will create a new middleware.
|
||||
|
||||
A "middleware" is a function that is always executed for each request, and have code before and after the request.
|
||||
|
||||
The middleware we will create (just a function) will create a new SQLAlchemy `Session` for each request, add it to the request and then close it once the request is finished.
|
||||
|
||||
```Python hl_lines="62 63 64 65 66 67"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
### 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).
|
||||
|
||||
|
||||
## Create a `CustomBase` model
|
||||
|
||||
@@ -65,17 +115,17 @@ This is more of a trick to facilitate your life than something required.
|
||||
|
||||
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.
|
||||
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="16 17 18 19 20"
|
||||
```Python hl_lines="17 18 19 20 21"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Create the SQLAlchemy `Base` model
|
||||
|
||||
```Python hl_lines="23"
|
||||
```Python hl_lines="24"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -85,15 +135,43 @@ 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="26 27 28 29 30"
|
||||
```Python hl_lines="27 28 29 30 31"
|
||||
{!./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="34 36 38 39 40 41 42 44"
|
||||
{!./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 `username` (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 test, written in code, that checks if another piece of code is working correctly.">unit tests</abbr> for it:
|
||||
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="33 34"
|
||||
```Python hl_lines="48 49"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -103,35 +181,39 @@ Now, finally, here's the standard **FastAPI** code.
|
||||
|
||||
Create your app and path operation function:
|
||||
|
||||
```Python hl_lines="38 41 42 43 44"
|
||||
```Python hl_lines="53 56 57 58 59"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
As we are using SQLAlchemy's `scoped_session`, we don't even have to create a dependency with `Depends`.
|
||||
We are creating the database session before each request, attaching it to the request, and then closing it afterwards.
|
||||
|
||||
We can just call `get_user` directly from inside of the path operation function and use the global `db_session`.
|
||||
All of this is done in the middleware explained above.
|
||||
|
||||
Because of that, we can use the `Request` to access the database session with `request._scope["db"]`.
|
||||
|
||||
Then we can just call `get_user` directly from inside of the path operation function and use that session.
|
||||
|
||||
## Create the path operation function
|
||||
|
||||
Here we are using SQLAlchemy code inside of the path operation function, and it in turn will go and communicate with an external database.
|
||||
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`, as would be with something like:
|
||||
But as SQLAlchemy doesn't have compatibility for using `await` directly, as would be with something like:
|
||||
|
||||
```Python
|
||||
user = await get_user(username, db_session)
|
||||
user = await get_user(db_session, user_id=user_id)
|
||||
```
|
||||
|
||||
...and instead we are using:
|
||||
|
||||
```Python
|
||||
user = get_user(username, db_session)
|
||||
user = get_user(db_session, user_id=user_id)
|
||||
```
|
||||
|
||||
Then we should declare the path operation without `async def`, just with a normal `def`:
|
||||
|
||||
```Python hl_lines="42"
|
||||
```Python hl_lines="57"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -140,3 +222,55 @@ 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.
|
||||
|
||||
## Check it
|
||||
|
||||
You can copy this code and use it as is.
|
||||
|
||||
!!! info
|
||||
|
||||
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`.
|
||||
|
||||
Then you can run it with Uvicorn:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --debug
|
||||
```
|
||||
|
||||
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>.
|
||||
|
||||
And you will be able to interact with your **FastAPI** application, reading data from a real database:
|
||||
|
||||
<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 direclty
|
||||
|
||||
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>.
|
||||
|
||||
It will look like this:
|
||||
|
||||
<img src="/img/tutorial/sql-databases/image02.png">
|
||||
|
||||
95
docs/tutorial/sub-applications-proxy.md
Normal file
95
docs/tutorial/sub-applications-proxy.md
Normal file
@@ -0,0 +1,95 @@
|
||||
There are at least two situations where you could need to create your **FastAPI** application using some specific paths.
|
||||
|
||||
But then you need to set them up to be served with a path prefix.
|
||||
|
||||
It could happen if you have a:
|
||||
|
||||
* **Proxy** server.
|
||||
* You are "**mounting**" a FastAPI application inside another FastAPI application (or inside another ASGI application, like Starlette).
|
||||
|
||||
## Proxy
|
||||
|
||||
Having a proxy in this case means that you could declare a path at `/app`, but then, you could need to add a layer on top (the Proxy) that would put your **FastAPI** application under a path like `/api/v1`.
|
||||
|
||||
In this case, the original path `/app` will actually be served at `/api/v1/app`.
|
||||
|
||||
Even though your application "thinks" it is serving at `/app`.
|
||||
|
||||
And the Proxy could be re-writing the path "on the fly" to keep your application convinced that it is serving at `/app`.
|
||||
|
||||
Up to here, everything would work as normally.
|
||||
|
||||
But then, when you open the integrated docs, they would expect to get the OpenAPI schema at `/openapi.json`, instead of `/api/v1/openapi.json`.
|
||||
|
||||
So, the frontend (that runs in the browser) would try to reach `/openapi.json` and wouldn't be able to get the OpenAPI schema.
|
||||
|
||||
So, it's needed that the frontend looks for the OpenAPI schema at `/api/v1/openapi.json`.
|
||||
|
||||
And it's also needed that the returned JSON OpenAPI schema has the defined path at `/api/v1/app` (behind the proxy) instead of `/app`.
|
||||
|
||||
---
|
||||
|
||||
For these cases, you can declare an `openapi_prefix` parameter in your `FastAPI` application.
|
||||
|
||||
See the section below, about "mounting", for an example.
|
||||
|
||||
|
||||
## Mounting a **FastAPI** application
|
||||
|
||||
"Mounting" means adding a complete "independent" application in a specific path, that then takes care of handling all the sub-paths.
|
||||
|
||||
You could want to do this if you have several "independent" applications that you want to separate, having their own independent OpenAPI schema and user interfaces.
|
||||
|
||||
### Top-level application
|
||||
|
||||
First, create the main, top-level, **FastAPI** application, and its path operations:
|
||||
|
||||
```Python hl_lines="3 6 7 8"
|
||||
{!./src/sub_applications/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Sub-application
|
||||
|
||||
Then, create your sub-application, and its path operations.
|
||||
|
||||
This sub-application is just another standard FastAPI application, but this is the one that will be "mounted".
|
||||
|
||||
When creating the sub-application, use the parameter `openapi_prefix`. In this case, with a prefix of `/subapi`:
|
||||
|
||||
```Python hl_lines="11 14 15 16"
|
||||
{!./src/sub_applications/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Mount the sub-application
|
||||
|
||||
In your top-level application, `app`, mount the sub-application, `subapi`.
|
||||
|
||||
Here you need to make sure you use the same path that you used for the `openapi_prefix`, in this case, `/subapi`:
|
||||
|
||||
```Python hl_lines="11 19"
|
||||
{!./src/sub_applications/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Check the automatic API docs
|
||||
|
||||
Now, run `uvicorn`, if your file is at `main.py`, it would be:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --debug
|
||||
```
|
||||
|
||||
And open the docs at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
|
||||
You will see the automatic API docs for the main app, including only its own paths:
|
||||
|
||||
<img src="/img/tutorial/sub-applications/image01.png">
|
||||
|
||||
|
||||
And then, open the docs for the sub-application, at <a href="http://127.0.0.1:8000/subapi/docs" target="_blank">http://127.0.0.1:8000/subapi/docs</a>.
|
||||
|
||||
You will see the automatic API docs for the sub-application, including only its own sub-paths, with their correct prefix:
|
||||
|
||||
<img src="/img/tutorial/sub-applications/image02.png">
|
||||
|
||||
|
||||
If you try interacting with any of the two user interfaces, they will work, because the browser will be able to talk to the correct path (or sub-path).
|
||||
55
docs/tutorial/using-request-directly.md
Normal file
55
docs/tutorial/using-request-directly.md
Normal file
@@ -0,0 +1,55 @@
|
||||
Up to now, you have been declaring the parts of the request that you need with their types.
|
||||
|
||||
Taking data from:
|
||||
|
||||
* The path as parameters.
|
||||
* Headers.
|
||||
* Cookies.
|
||||
* etc.
|
||||
|
||||
And by doing so, **FastAPI** is validating that data, converting it and generating documentation for your API automatically.
|
||||
|
||||
But there are situations where you might need to access the `Request` object directly.
|
||||
|
||||
## Details about the `Request` object
|
||||
|
||||
As **FastAPI** is actually **Starlette** underneath, with a layer of several tools on top, you can use Starlette's <a href="https://www.starlette.io/requests/" target="_blank">`Request`</a> object directly when you need to.
|
||||
|
||||
It would also mean that if you get data from the `Request` object directly (for example, read the body) it won't be validated, converted or annotated (with OpenAPI, for the automatic documentation) by FastAPI.
|
||||
|
||||
Although any other parameter declared normally (for example, the body with a Pydantic model) would still be validated, converted, annotated, etc.
|
||||
|
||||
But there are specific cases where it's useful to get the `Request` object.
|
||||
|
||||
## Use the `Request` object direclty
|
||||
|
||||
Let's imagine you want to get the client's IP address/host inside of your *path operation function*.
|
||||
|
||||
For that you need to access the request directly.
|
||||
|
||||
### Import the `Request`
|
||||
|
||||
First, import the `Request` class from Starlette:
|
||||
|
||||
```Python hl_lines="2"
|
||||
{!./src/using_request_directly/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Declare the `Request` parameter
|
||||
|
||||
Then declare a *path operation function* parameter with the type being the `Request` class:
|
||||
|
||||
```Python hl_lines="8"
|
||||
{!./src/using_request_directly/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Note that in this case, we are declaring a path parameter besides the request parameter.
|
||||
|
||||
So, the path parameter will be extracted, validated, converted to the specified type and annotated with OpenAPI.
|
||||
|
||||
The same way, you can declare any other parameter as normally, and additionally, get the `Request` too.
|
||||
|
||||
## `Request` documentation
|
||||
|
||||
You can read more details about the <a href="https://www.starlette.io/requests/" target="_blank">`Request` object in the official Starlette documentation site</a>.
|
||||
@@ -1,7 +1,8 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.2.1"
|
||||
__version__ = "0.6.0"
|
||||
|
||||
from .applications import FastAPI
|
||||
from .routing import APIRouter
|
||||
from .params import Body, Path, Query, Header, Cookie, Form, File, Security, Depends
|
||||
from .exceptions import HTTPException
|
||||
|
||||
@@ -7,42 +7,48 @@ from pydantic import BaseModel
|
||||
from starlette.applications import Starlette
|
||||
from starlette.exceptions import ExceptionMiddleware, HTTPException
|
||||
from starlette.middleware.errors import ServerErrorMiddleware
|
||||
from starlette.middleware.lifespan import LifespanMiddleware
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, Response
|
||||
from starlette.routing import BaseRoute
|
||||
|
||||
|
||||
async def http_exception(request: Request, exc: HTTPException) -> JSONResponse:
|
||||
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
|
||||
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,
|
||||
debug: bool = False,
|
||||
routes: List[BaseRoute] = None,
|
||||
template_directory: str = None,
|
||||
title: str = "Fast API",
|
||||
description: str = "",
|
||||
version: str = "0.1.0",
|
||||
openapi_url: Optional[str] = "/openapi.json",
|
||||
openapi_prefix: str = "",
|
||||
docs_url: Optional[str] = "/docs",
|
||||
redoc_url: Optional[str] = "/redoc",
|
||||
**extra: Dict[str, Any],
|
||||
) -> None:
|
||||
self._debug = debug
|
||||
self.router: routing.APIRouter = routing.APIRouter()
|
||||
self.router: routing.APIRouter = routing.APIRouter(routes)
|
||||
self.exception_middleware = ExceptionMiddleware(self.router, debug=debug)
|
||||
self.error_middleware = ServerErrorMiddleware(
|
||||
self.exception_middleware, debug=debug
|
||||
)
|
||||
self.lifespan_middleware = LifespanMiddleware(self.error_middleware)
|
||||
self.schema_generator = None
|
||||
self.template_env = self.load_template_env(template_directory)
|
||||
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.version = version
|
||||
self.openapi_url = openapi_url
|
||||
self.openapi_prefix = openapi_prefix.rstrip("/")
|
||||
self.docs_url = docs_url
|
||||
self.redoc_url = redoc_url
|
||||
self.extra = extra
|
||||
@@ -66,6 +72,7 @@ class FastAPI(Starlette):
|
||||
openapi_version=self.openapi_version,
|
||||
description=self.description,
|
||||
routes=self.routes,
|
||||
openapi_prefix=self.openapi_prefix,
|
||||
)
|
||||
return self.openapi_schema
|
||||
|
||||
@@ -80,7 +87,8 @@ class FastAPI(Starlette):
|
||||
self.add_route(
|
||||
self.docs_url,
|
||||
lambda r: get_swagger_ui_html(
|
||||
openapi_url=self.openapi_url, title=self.title + " - Swagger UI"
|
||||
openapi_url=self.openapi_prefix + self.openapi_url,
|
||||
title=self.title + " - Swagger UI",
|
||||
),
|
||||
include_in_schema=False,
|
||||
)
|
||||
@@ -88,7 +96,8 @@ class FastAPI(Starlette):
|
||||
self.add_route(
|
||||
self.redoc_url,
|
||||
lambda r: get_redoc_html(
|
||||
openapi_url=self.openapi_url, title=self.title + " - ReDoc"
|
||||
openapi_url=self.openapi_prefix + self.openapi_url,
|
||||
title=self.title + " - ReDoc",
|
||||
),
|
||||
include_in_schema=False,
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ def jsonable_encoder(
|
||||
by_alias: bool = False,
|
||||
include_none: bool = True,
|
||||
custom_encoder: dict = {},
|
||||
sqlalchemy_safe: bool = True,
|
||||
) -> Any:
|
||||
if isinstance(obj, BaseModel):
|
||||
encoder = getattr(obj.Config, "json_encoders", custom_encoder)
|
||||
@@ -20,39 +21,55 @@ def jsonable_encoder(
|
||||
obj.dict(include=include, exclude=exclude, by_alias=by_alias),
|
||||
include_none=include_none,
|
||||
custom_encoder=encoder,
|
||||
sqlalchemy_safe=sqlalchemy_safe,
|
||||
)
|
||||
if isinstance(obj, Enum):
|
||||
return obj.value
|
||||
if isinstance(obj, (str, int, float, type(None))):
|
||||
return obj
|
||||
if isinstance(obj, dict):
|
||||
return {
|
||||
jsonable_encoder(
|
||||
key,
|
||||
by_alias=by_alias,
|
||||
include_none=include_none,
|
||||
custom_encoder=custom_encoder,
|
||||
): jsonable_encoder(
|
||||
value,
|
||||
by_alias=by_alias,
|
||||
include_none=include_none,
|
||||
custom_encoder=custom_encoder,
|
||||
)
|
||||
for key, value in obj.items()
|
||||
if value is not None or include_none
|
||||
}
|
||||
encoded_dict = {}
|
||||
for key, value in obj.items():
|
||||
if (
|
||||
(
|
||||
not sqlalchemy_safe
|
||||
or (not isinstance(key, str))
|
||||
or (not key.startswith("_sa"))
|
||||
)
|
||||
and (value is not None or include_none)
|
||||
and ((include and key in include) or key not in exclude)
|
||||
):
|
||||
encoded_key = jsonable_encoder(
|
||||
key,
|
||||
by_alias=by_alias,
|
||||
include_none=include_none,
|
||||
custom_encoder=custom_encoder,
|
||||
sqlalchemy_safe=sqlalchemy_safe,
|
||||
)
|
||||
encoded_value = jsonable_encoder(
|
||||
value,
|
||||
by_alias=by_alias,
|
||||
include_none=include_none,
|
||||
custom_encoder=custom_encoder,
|
||||
sqlalchemy_safe=sqlalchemy_safe,
|
||||
)
|
||||
encoded_dict[encoded_key] = encoded_value
|
||||
return encoded_dict
|
||||
if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)):
|
||||
return [
|
||||
jsonable_encoder(
|
||||
item,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
include_none=include_none,
|
||||
custom_encoder=custom_encoder,
|
||||
encoded_list = []
|
||||
for item in obj:
|
||||
encoded_list.append(
|
||||
jsonable_encoder(
|
||||
item,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
include_none=include_none,
|
||||
custom_encoder=custom_encoder,
|
||||
sqlalchemy_safe=sqlalchemy_safe,
|
||||
)
|
||||
)
|
||||
for item in obj
|
||||
]
|
||||
return encoded_list
|
||||
errors = []
|
||||
try:
|
||||
if custom_encoder and type(obj) in custom_encoder:
|
||||
@@ -71,4 +88,10 @@ def jsonable_encoder(
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
raise ValueError(errors)
|
||||
return jsonable_encoder(data, by_alias=by_alias, include_none=include_none)
|
||||
return jsonable_encoder(
|
||||
data,
|
||||
by_alias=by_alias,
|
||||
include_none=include_none,
|
||||
custom_encoder=custom_encoder,
|
||||
sqlalchemy_safe=sqlalchemy_safe,
|
||||
)
|
||||
|
||||
9
fastapi/exceptions.py
Normal file
9
fastapi/exceptions.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
|
||||
|
||||
class HTTPException(StarletteHTTPException):
|
||||
def __init__(
|
||||
self, status_code: int, detail: str = None, headers: dict = None
|
||||
) -> None:
|
||||
super().__init__(status_code=status_code, detail=detail)
|
||||
self.headers = headers
|
||||
@@ -215,7 +215,8 @@ def get_openapi(
|
||||
version: str,
|
||||
openapi_version: str = "3.0.2",
|
||||
description: str = None,
|
||||
routes: Sequence[BaseRoute]
|
||||
routes: Sequence[BaseRoute],
|
||||
openapi_prefix: str = ""
|
||||
) -> Dict:
|
||||
info = {"title": title, "version": version}
|
||||
if description:
|
||||
@@ -234,7 +235,7 @@ def get_openapi(
|
||||
if result:
|
||||
path, security_schemes, path_definitions = result
|
||||
if path:
|
||||
paths.setdefault(route.path, {}).update(path)
|
||||
paths.setdefault(openapi_prefix + route.path, {}).update(path)
|
||||
if security_schemes:
|
||||
components.setdefault("securitySchemes", {}).update(
|
||||
security_schemes
|
||||
|
||||
@@ -40,6 +40,7 @@ nav:
|
||||
- Form Data: 'tutorial/request-forms.md'
|
||||
- Request Files: 'tutorial/request-files.md'
|
||||
- Request Forms and Files: 'tutorial/request-forms-and-files.md'
|
||||
- Handling Errors: 'tutorial/handling-errors.md'
|
||||
- Path Operation Configuration: 'tutorial/path-operation-configuration.md'
|
||||
- Path Operation Advanced Configuration: 'tutorial/path-operation-advanced-configuration.md'
|
||||
- Custom Response: 'tutorial/custom-response.md'
|
||||
@@ -54,16 +55,19 @@ nav:
|
||||
- Get Current User: 'tutorial/security/get-current-user.md'
|
||||
- Simple OAuth2 with Password and Bearer: 'tutorial/security/simple-oauth2.md'
|
||||
- OAuth2 with Password (and hashing), Bearer with JWT tokens: 'tutorial/security/oauth2-jwt.md'
|
||||
- Using the Request Directly: 'tutorial/using-request-directly.md'
|
||||
- SQL (Relational) Databases: 'tutorial/sql-databases.md'
|
||||
- NoSQL (Distributed / Big Data) Databases: 'tutorial/nosql-databases.md'
|
||||
- Bigger Applications - Multiple Files: 'tutorial/bigger-applications.md'
|
||||
- Sub Applications - Behind a Proxy: 'tutorial/sub-applications-proxy.md'
|
||||
- Application Configuration: 'tutorial/application-configuration.md'
|
||||
- Extra Starlette options: 'tutorial/extra-starlette.md'
|
||||
- Concurrency and async / await: 'async.md'
|
||||
- Deployment: 'deployment.md'
|
||||
- Project Generation - Template: 'project-generation.md'
|
||||
- Alternatives, Inspiration and Comparisons: 'alternatives.md'
|
||||
- Benchmarks: 'benchmarks.md'
|
||||
- Help FastAPI - Get Help: 'help-fastapi.md'
|
||||
- Development - Contributing: 'contributing.md'
|
||||
- Release Notes: release-notes.md
|
||||
|
||||
markdown_extensions:
|
||||
|
||||
@@ -19,7 +19,7 @@ classifiers = [
|
||||
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
||||
]
|
||||
requires = [
|
||||
"starlette >=0.9.11,<=0.10.1",
|
||||
"starlette ==0.11.1",
|
||||
"pydantic >=0.17,<=0.18.2"
|
||||
]
|
||||
description-file = "README.md"
|
||||
@@ -36,7 +36,8 @@ test = [
|
||||
"black",
|
||||
"isort",
|
||||
"requests",
|
||||
"email_validator"
|
||||
"email_validator",
|
||||
"sqlalchemy"
|
||||
]
|
||||
doc = [
|
||||
"mkdocs",
|
||||
|
||||
@@ -6,6 +6,11 @@ set -x
|
||||
export VERSION_SCRIPT="import sys; print('%s.%s' % sys.version_info[0:2])"
|
||||
export PYTHON_VERSION=`python -c "$VERSION_SCRIPT"`
|
||||
|
||||
# Remove temporary DB
|
||||
if [ -f ./test.db ]; then
|
||||
rm ./test.db
|
||||
fi
|
||||
|
||||
export PYTHONPATH=./docs/src
|
||||
pytest --cov=fastapi --cov=tests --cov=docs/src --cov-report=term-missing ${@}
|
||||
mypy fastapi --disallow-untyped-defs
|
||||
|
||||
156
tests/test_starlette_exception.py
Normal file
156
tests/test_starlette_exception.py
Normal file
@@ -0,0 +1,156 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
items = {"foo": "The Foo Wrestlers"}
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def create_item(item_id: str):
|
||||
if item_id not in items:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Item not found",
|
||||
headers={"X-Error": "Some custom header"},
|
||||
)
|
||||
return {"item": items[item_id]}
|
||||
|
||||
|
||||
@app.get("/starlette-items/{item_id}")
|
||||
async def create_item(item_id: str):
|
||||
if item_id not in items:
|
||||
raise StarletteHTTPException(status_code=404, detail="Item not found")
|
||||
return {"item": items[item_id]}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/{item_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Create Item Get",
|
||||
"operationId": "create_item_items__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
"/starlette-items/{item_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Create Item Get",
|
||||
"operationId": "create_item_starlette-items__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_get_item():
|
||||
response = client.get("/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item": "The Foo Wrestlers"}
|
||||
|
||||
|
||||
def test_get_item_not_found():
|
||||
response = client.get("/items/bar")
|
||||
assert response.status_code == 404
|
||||
assert response.headers.get("x-error") == "Some custom header"
|
||||
assert response.json() == {"detail": "Item not found"}
|
||||
|
||||
|
||||
def test_get_starlette_item():
|
||||
response = client.get("/starlette-items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item": "The Foo Wrestlers"}
|
||||
|
||||
|
||||
def test_get_starlette_item_not_found():
|
||||
response = client.get("/starlette-items/bar")
|
||||
assert response.status_code == 404
|
||||
assert response.headers.get("x-error") is None
|
||||
assert response.json() == {"detail": "Item not found"}
|
||||
90
tests/test_tutorial/test_handling_errors/test_tutorial001.py
Normal file
90
tests/test_tutorial/test_handling_errors/test_tutorial001.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from handling_errors.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/{item_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Create Item Get",
|
||||
"operationId": "create_item_items__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_get_item():
|
||||
response = client.get("/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item": "The Foo Wrestlers"}
|
||||
|
||||
|
||||
def test_get_item_not_found():
|
||||
response = client.get("/items/bar")
|
||||
assert response.status_code == 404
|
||||
assert response.headers.get("x-error") is None
|
||||
assert response.json() == {"detail": "Item not found"}
|
||||
90
tests/test_tutorial/test_handling_errors/test_tutorial002.py
Normal file
90
tests/test_tutorial/test_handling_errors/test_tutorial002.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from handling_errors.tutorial002 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items-header/{item_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Create Item Header Get",
|
||||
"operationId": "create_item_header_items-header__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_get_item_header():
|
||||
response = client.get("/items-header/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item": "The Foo Wrestlers"}
|
||||
|
||||
|
||||
def test_get_item_not_found_header():
|
||||
response = client.get("/items-header/bar")
|
||||
assert response.status_code == 404
|
||||
assert response.headers.get("x-error") == "There goes my error"
|
||||
assert response.json() == {"detail": "Item not found"}
|
||||
0
tests/test_tutorial/test_sql_databases/__init__.py
Normal file
0
tests/test_tutorial/test_sql_databases/__init__.py
Normal file
88
tests/test_tutorial/test_sql_databases/test_tutorial001.py
Normal file
88
tests/test_tutorial/test_sql_databases/test_tutorial001.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from sql_databases.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/{user_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read User Get",
|
||||
"operationId": "read_user_users__user_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "User_Id", "type": "integer"},
|
||||
"name": "user_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_first_user():
|
||||
response = client.get("/users/1")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"is_active": True,
|
||||
"hashed_password": "notreallyhashed",
|
||||
"email": "johndoe@example.com",
|
||||
"id": 1,
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from sub_applications.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema_main = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/app": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Main Get",
|
||||
"operationId": "read_main_app_get",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
openapi_schema_sub = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/subapi/sub": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Sub Get",
|
||||
"operationId": "read_sub_sub_get",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema_main():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema_main
|
||||
|
||||
|
||||
def test_main():
|
||||
response = client.get("/app")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Hello World from main app"}
|
||||
|
||||
|
||||
def test_openapi_schema_sub():
|
||||
response = client.get("/subapi/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema_sub
|
||||
|
||||
|
||||
def test_sub():
|
||||
response = client.get("/subapi/sub")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Hello World from sub API"}
|
||||
Reference in New Issue
Block a user