mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-24 06:39:31 -05:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
894e131e03 | ||
|
|
8772e2f2ee | ||
|
|
7edbd9345b | ||
|
|
56819fdd89 | ||
|
|
febf8e7341 | ||
|
|
293ebd7cc2 | ||
|
|
54e3949f74 | ||
|
|
acbcbba94f | ||
|
|
f7b7a099c3 | ||
|
|
0ea0d0e82a | ||
|
|
890f1f7899 |
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.
|
||||
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,17 +1,35 @@
|
||||
## Next
|
||||
|
||||
## 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>
|
||||
* 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>
|
||||
* 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]}
|
||||
@@ -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, Security, HTTPException
|
||||
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,7 +1,8 @@
|
||||
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 = "sqlite:///./test.db"
|
||||
@@ -10,9 +11,7 @@ SQLALCHEMY_DATABASE_URI = "sqlite:///./test.db"
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URI, connect_args={"check_same_thread": False}
|
||||
)
|
||||
db_session = scoped_session(
|
||||
sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
)
|
||||
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
class CustomBase:
|
||||
@@ -34,12 +33,16 @@ class User(Base):
|
||||
|
||||
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):
|
||||
@@ -51,6 +54,14 @@ app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/users/{user_id}")
|
||||
def read_user(user_id: int):
|
||||
user = get_user(db_session, user_id=user_id)
|
||||
def read_user(request: Request, user_id: int):
|
||||
user = get_user(request._scope["db"], user_id=user_id)
|
||||
return user
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def close_db(request, call_next):
|
||||
request._scope["db"] = Session()
|
||||
response = await call_next(request)
|
||||
request._scope["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...
|
||||
57
docs/tutorial/handling-errors.md
Normal file
57
docs/tutorial/handling-errors.md
Normal file
@@ -0,0 +1,57 @@
|
||||
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!}
|
||||
```
|
||||
|
||||
### 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!}
|
||||
```
|
||||
@@ -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!}
|
||||
```
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ For now, don't pay attention to the rest, only the imports:
|
||||
|
||||
Define the database that SQLAlchemy should "connect" to:
|
||||
|
||||
```Python hl_lines="7"
|
||||
```Python hl_lines="8"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -55,7 +55,7 @@ SQLALCHEMY_DATABASE_URI = "postgresql://user:password@postgresserver/db"
|
||||
|
||||
## Create the SQLAlchemy `engine`
|
||||
|
||||
```Python hl_lines="10 11 12"
|
||||
```Python hl_lines="11 12 13"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -74,20 +74,43 @@ connect_args={"check_same_thread": False}
|
||||
That argument `check_same_thread` is there mainly to be able to run the tests that cover this example.
|
||||
|
||||
|
||||
## Create a `scoped_session`
|
||||
## Create a `Session` class
|
||||
|
||||
```Python hl_lines="13 14 15"
|
||||
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!}
|
||||
```
|
||||
|
||||
!!! note "Very Technical Details"
|
||||
Don't worry too much if you don't understand this. You can still use the code.
|
||||
## Create a middleware to handle sessions
|
||||
|
||||
This `scoped_session` is a feature of SQLAlchemy.
|
||||
Now let's temporarily jump to the end of the file, to use the `Session` class we created above.
|
||||
|
||||
The resulting object, the `db_session` can then be used anywhere as a normal SQLAlchemy session.
|
||||
|
||||
It can be used as a "global" variable 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.
|
||||
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._scope`
|
||||
|
||||
`request._scope` is a "private property" of each request. We normally shouldn't need to use a "private property" from a Python object.
|
||||
|
||||
But we need to attach the session to the request to be able to ensure a single session/database-connection is used through all the request, and then closed afterwards.
|
||||
|
||||
In the near future, Starlette <a href="https://github.com/encode/starlette/issues/379" target="_blank">will have a way to attach custom objects to each request</a>.
|
||||
|
||||
When that happens, this tutorial will be updated to use the new official way of doing it.
|
||||
|
||||
## Create a `CustomBase` model
|
||||
|
||||
@@ -99,13 +122,13 @@ That way you don't have to declare them explicitly in every model.
|
||||
|
||||
So, your models will behave very similarly to, for example, Flask-SQLAlchemy.
|
||||
|
||||
```Python hl_lines="18 19 20 21 22"
|
||||
```Python hl_lines="17 18 19 20 21"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Create the SQLAlchemy `Base` model
|
||||
|
||||
```Python hl_lines="25"
|
||||
```Python hl_lines="24"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -115,7 +138,7 @@ Now this is finally code specific to your app.
|
||||
|
||||
Here's a user model that will be a table in the database:
|
||||
|
||||
```Python hl_lines="28 29 30 31 32"
|
||||
```Python hl_lines="27 28 29 30 31"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -123,10 +146,17 @@ Here's a user model that will be a table in the database:
|
||||
|
||||
In a very simplistic way, initialize your database (create the tables, etc) and make sure you have a first user:
|
||||
|
||||
```Python hl_lines="35 37 38 39 40 41"
|
||||
```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>.
|
||||
@@ -144,7 +174,7 @@ Also, as all the functionality is self-contained in the same code, you can copy
|
||||
|
||||
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="45 46"
|
||||
```Python hl_lines="48 49"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -154,13 +184,17 @@ Now, finally, here's the standard **FastAPI** code.
|
||||
|
||||
Create your app and path operation function:
|
||||
|
||||
```Python hl_lines="50 53 54 55 56"
|
||||
```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
|
||||
|
||||
@@ -182,7 +216,7 @@ 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="54"
|
||||
```Python hl_lines="57"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -235,3 +269,11 @@ That's something that you can improve in this example application, here's the cu
|
||||
"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.3.0"
|
||||
__version__ = "0.5.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
|
||||
|
||||
@@ -13,7 +13,13 @@ from starlette.responses import JSONResponse, Response
|
||||
|
||||
|
||||
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):
|
||||
@@ -25,6 +31,7 @@ class FastAPI(Starlette):
|
||||
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],
|
||||
@@ -43,6 +50,7 @@ class FastAPI(Starlette):
|
||||
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 +74,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 +89,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 +98,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,
|
||||
)
|
||||
|
||||
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,11 +55,12 @@ 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'
|
||||
|
||||
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,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