Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
098e629344 | ||
|
|
bbe5f28b77 | ||
|
|
4a0922ebab | ||
|
|
8f16868c6a | ||
|
|
bc3e7f2bbc | ||
|
|
58848be2de | ||
|
|
cfb65d0e15 | ||
|
|
855daa2e53 | ||
|
|
de54e85152 | ||
|
|
b8d3070daf | ||
|
|
471c9cfc2d | ||
|
|
b79c13baed | ||
|
|
332ee4aee1 | ||
|
|
ad40f4a457 | ||
|
|
6b9931f882 | ||
|
|
4c51bb6714 | ||
|
|
57ff677027 | ||
|
|
613c3f3e95 | ||
|
|
bf6d923ca8 | ||
|
|
252188c686 | ||
|
|
510fec9bee | ||
|
|
a73709507c | ||
|
|
75407b9295 | ||
|
|
3180f35bdd | ||
|
|
d498b7feb3 | ||
|
|
3269e6a95c | ||
|
|
f1808de18e | ||
|
|
748dc375db | ||
|
|
b38fb937b0 | ||
|
|
23ef570bf6 | ||
|
|
c25a71e352 | ||
|
|
0c5e684ff9 |
1
Pipfile
@@ -21,6 +21,7 @@ mkdocs-material = "*"
|
||||
markdown-include = "*"
|
||||
autoflake = "*"
|
||||
email-validator = "*"
|
||||
ujson = "*"
|
||||
|
||||
[packages]
|
||||
starlette = "*"
|
||||
|
||||
13
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "64539bfa9f03f10715a5f83b1d62776513ae44518c0cff011b7540c17eada955"
|
||||
"sha256": "a0f966a95cb84845ca4aad02c44fc0e7c5e2047fc44dcf19a95a4abaa02d0197"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -624,9 +624,9 @@
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
"sha256:05910b7ff43cec0a853c15da0bfaf2867faa95f29b08e71f5846a195f1f38c75"
|
||||
"sha256:59880cc33ac293515892b2969aa8f4ed2cec592cbd0be4c4e20f2410468bbc62"
|
||||
],
|
||||
"version": "==0.14.7"
|
||||
"version": "==0.14.8"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
@@ -819,6 +819,13 @@
|
||||
"markers": "python_version < '3.7' and implementation_name == 'cpython'",
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"ujson": {
|
||||
"hashes": [
|
||||
"sha256:f66073e5506e91d204ab0c614a148d5aa938bdbf104751be66f8ad7a222f5f86"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.35"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
|
||||
|
||||
@@ -321,7 +321,6 @@ Try changing the line with:
|
||||
|
||||
...and see how your editor will auto-complete the attributes and know their types:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 65 KiB |
BIN
docs/img/tutorial/security/image01.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/img/tutorial/security/image02.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
docs/img/tutorial/security/image03.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
docs/img/tutorial/security/image04.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
docs/img/tutorial/security/image05.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
docs/img/tutorial/security/image06.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
docs/img/tutorial/security/image07.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
docs/img/tutorial/security/image08.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
docs/img/tutorial/security/image09.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
docs/img/tutorial/security/image10.png
Normal file
|
After Width: | Height: | Size: 155 KiB |
@@ -36,7 +36,7 @@ The key features are:
|
||||
* **Easy**: Designed to be easy to use and learn. Less time reading docs.
|
||||
* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Less bugs.
|
||||
* **Robust**: Get production-ready code. With automatic interactive documentation.
|
||||
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" target="_blank">OpenAPI</a> and <a href="http://json-schema.org/" target="_blank">JSON Schema</a>.
|
||||
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" target="_blank">OpenAPI</a> (previously known as Swagger) and <a href="http://json-schema.org/" target="_blank">JSON Schema</a>.
|
||||
|
||||
<small>* estimation based on tests on an internal development team, building production applications.</small>
|
||||
|
||||
@@ -321,7 +321,6 @@ Try changing the line with:
|
||||
|
||||
...and see how your editor will auto-complete the attributes and know their types:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
42
docs/project-generation.md
Normal file
@@ -0,0 +1,42 @@
|
||||
There is a project generator that you can use to get started, with a lot of the initial set up, security, database and first API endpoints already done for you.
|
||||
|
||||
## Full-Stack-FastAPI-Couchbase
|
||||
|
||||
GitHub: <a href="https://github.com/tiangolo/full-stack-fastapi-couchbase" target="_blank">https://github.com/tiangolo/full-stack-fastapi-couchbase</a>
|
||||
|
||||
### Features
|
||||
|
||||
* Full **Docker** integration (Docker based).
|
||||
* Docker Swarm Mode deployment.
|
||||
* **Docker Compose** integration and optimization for local development.
|
||||
* **Production ready** Python web server using Uvicorn and Gunicorn.
|
||||
* Python **FastAPI** backend with all its features.
|
||||
* **Celery** worker that can import and use code from the rest of the backend selectively (you don't have to install the complete app in each worker).
|
||||
* **NoSQL Couchbase** database that supports direct synchronization via Couchbase Sync Gateway for offline-first applications.
|
||||
* **Full Text Search** integrated, using Couchbase.
|
||||
* REST backend tests based on Pytest, integrated with Docker, so you can test the full API interaction, independent on the database. As it runs in Docker, it can build a new data store from scratch each time (so you can use ElasticSearch, MongoDB, or whatever you want, and just test that the API works).
|
||||
* Easy Python integration with **Jupyter** Kernels for remote or in-Docker development with extensions like Atom Hydrogen or Visual Studio Code Jupyter.
|
||||
* **Email notifications** for account creation and password recovery, compatible with:
|
||||
* Mailgun
|
||||
* SparkPost
|
||||
* SendGrid
|
||||
* ...any other provider that can generate standard SMTP credentials.
|
||||
* **Vue** frontend:
|
||||
* Generated with Vue CLI.
|
||||
* **JWT Authentication** handling.
|
||||
* Login view.
|
||||
* After login, main dashboard view.
|
||||
* Main dashboard with user creation and edition.
|
||||
* Self user edition.
|
||||
* **Vuex**.
|
||||
* **Vue-router**.
|
||||
* **Vuetify** for beautiful material design components.
|
||||
* **TypeScript**.
|
||||
* Docker server based on **Nginx** (configured to play nicely with Vue-router).
|
||||
* Docker multi-stage building, so you don't need to save or commit compiled code.
|
||||
* Frontend tests ran at build time (can be disabled too).
|
||||
* Made as modular as possible, so it works out of the box, but you can re-generate with Vue CLI or create it as you need, and re-use what you want.
|
||||
* Flower for Celery jobs monitoring.
|
||||
* Load balancing between frontend and backend with **Traefik**, so you can have both under the same domain, separated by path, but served by different containers.
|
||||
* Traefik integration, including Let's Encrypt **HTTPS** certificates automatic generation.
|
||||
* GitLab **CI** (continuous integration), including frontend and backend testing.
|
||||
@@ -1,5 +1,4 @@
|
||||
from fastapi import Depends, FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -7,18 +6,15 @@ app = FastAPI()
|
||||
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
|
||||
|
||||
|
||||
class CommonQueryParams(BaseModel):
|
||||
q: str = None
|
||||
skip: int = None
|
||||
limit: int = None
|
||||
|
||||
|
||||
async def common_parameters(q: str = None, skip: int = 0, limit: int = 100):
|
||||
return CommonQueryParams(q=q, skip=skip, limit=limit)
|
||||
class CommonQueryParams:
|
||||
def __init__(self, q: str = None, skip: int = 0, limit: int = 100):
|
||||
self.q = q
|
||||
self.skip = skip
|
||||
self.limit = limit
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items(commons: CommonQueryParams = Depends(common_parameters)):
|
||||
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
|
||||
response = {}
|
||||
if commons.q:
|
||||
response.update({"q": commons.q})
|
||||
|
||||
@@ -1,34 +1,23 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import Cookie, Depends, FastAPI
|
||||
from pydantic import BaseModel
|
||||
from fastapi import Depends, FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class InterestsTracker(BaseModel):
|
||||
track_code: str
|
||||
interests: List[str]
|
||||
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
|
||||
|
||||
|
||||
fake_tracked_users_db = {
|
||||
"Foo": {"track_code": "Foo", "interests": ["sports", "movies"]},
|
||||
"Bar": {"track_code": "Bar", "interests": ["food", "shows"]},
|
||||
"Baz": {"track_code": "Baz", "interests": ["gaming", "virtual reality"]},
|
||||
}
|
||||
class CommonQueryParams:
|
||||
def __init__(self, q: str = None, skip: int = 0, limit: int = 100):
|
||||
self.q = q
|
||||
self.skip = skip
|
||||
self.limit = limit
|
||||
|
||||
|
||||
async def get_tracked_interests(track_code: str = Cookie(None)):
|
||||
if track_code in fake_tracked_users_db:
|
||||
track_dict = fake_tracked_users_db[track_code]
|
||||
track = InterestsTracker(**track_dict)
|
||||
return track
|
||||
return None
|
||||
|
||||
|
||||
@app.get("/interests/")
|
||||
async def read_interests(
|
||||
tracked_interests: InterestsTracker = Depends(get_tracked_interests)
|
||||
):
|
||||
response = {"interests": tracked_interests.interests}
|
||||
@app.get("/items/")
|
||||
async def read_items(commons=Depends(CommonQueryParams)):
|
||||
response = {}
|
||||
if commons.q:
|
||||
response.update({"q": commons.q})
|
||||
items = fake_items_db[commons.skip : commons.limit]
|
||||
response.update({"items": items})
|
||||
return response
|
||||
|
||||
@@ -1,49 +1,23 @@
|
||||
from random import choice
|
||||
from typing import List
|
||||
|
||||
from fastapi import Cookie, Depends, FastAPI
|
||||
from pydantic import BaseModel
|
||||
from fastapi import Depends, FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class InterestsTracker(BaseModel):
|
||||
track_code: str
|
||||
interests: List[str]
|
||||
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
|
||||
|
||||
|
||||
fake_tracked_users_db = {
|
||||
"Foo": {"track_code": "Foo", "interests": ["sports", "movies"]},
|
||||
"Bar": {"track_code": "Bar", "interests": ["food", "shows"]},
|
||||
"Baz": {"track_code": "Baz", "interests": ["gaming", "virtual reality"]},
|
||||
}
|
||||
class CommonQueryParams:
|
||||
def __init__(self, q: str = None, skip: int = 0, limit: int = 100):
|
||||
self.q = q
|
||||
self.skip = skip
|
||||
self.limit = limit
|
||||
|
||||
|
||||
async def get_tracked_interests(track_code: str = Cookie(None)):
|
||||
if track_code in fake_tracked_users_db:
|
||||
track_dict = fake_tracked_users_db[track_code]
|
||||
track = InterestsTracker(**track_dict)
|
||||
return track
|
||||
return None
|
||||
|
||||
|
||||
class ComplexTracker:
|
||||
def __init__(self, tracker: InterestsTracker = Depends(get_tracked_interests)):
|
||||
self.tracker = tracker
|
||||
|
||||
def random_interest(self):
|
||||
"""
|
||||
Get a random interest from the tracked ones for the current user.
|
||||
If the user doesn't have tracked interests, return a random one from the ones available.
|
||||
"""
|
||||
if self.tracker.interests:
|
||||
return choice(self.tracker.interests)
|
||||
return choice(
|
||||
["sports", "movies", "food", "shows", "gaming", "virtual reality"]
|
||||
)
|
||||
|
||||
|
||||
@app.get("/suggested-category")
|
||||
async def read_suggested_category(tracker: ComplexTracker = Depends(None)):
|
||||
response = {"category": tracker.random_interest()}
|
||||
@app.get("/items/")
|
||||
async def read_items(commons: CommonQueryParams = Depends()):
|
||||
response = {}
|
||||
if commons.q:
|
||||
response.update({"q": commons.q})
|
||||
items = fake_items_db[commons.skip : commons.limit]
|
||||
response.update({"items": items})
|
||||
return response
|
||||
|
||||
20
docs/src/dependencies/tutorial005.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from fastapi import Cookie, Depends, FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
def query_extractor(q: str = None):
|
||||
return q
|
||||
|
||||
|
||||
def query_or_cookie_extractor(
|
||||
q: str = Depends(query_extractor), last_query: str = Cookie(None)
|
||||
):
|
||||
if not q:
|
||||
return last_query
|
||||
return q
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_query(query_or_default: str = Depends(query_or_cookie_extractor)):
|
||||
return {"q_or_cookie": query_or_default}
|
||||
21
docs/src/dependencies/tutorial006.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from fastapi import Depends, FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class FixedContentQueryChecker:
|
||||
def __init__(self, fixed_content: str):
|
||||
self.fixed_content = fixed_content
|
||||
|
||||
def __call__(self, q: str = ""):
|
||||
if q:
|
||||
return self.fixed_content in q
|
||||
return False
|
||||
|
||||
|
||||
checker = FixedContentQueryChecker("bar")
|
||||
|
||||
|
||||
@app.get("/query-checker/")
|
||||
async def read_query_check(fixed_content_included: bool = Depends(checker)):
|
||||
return {"fixed_content_in_query": fixed_content_included}
|
||||
27
docs/src/extra_data_types/tutorial001.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from datetime import datetime, time, timedelta
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Body, FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.put("/items/{item_id}")
|
||||
async def read_items(
|
||||
item_id: UUID,
|
||||
start_datetime: datetime = Body(None),
|
||||
end_datetime: datetime = Body(None),
|
||||
repeat_at: time = Body(None),
|
||||
process_after: timedelta = Body(None),
|
||||
):
|
||||
start_process = start_datetime + process_after
|
||||
duration = end_datetime - start_process
|
||||
return {
|
||||
"item_id": item_id,
|
||||
"start_datetime": start_datetime,
|
||||
"end_datetime": end_datetime,
|
||||
"repeat_at": repeat_at,
|
||||
"process_after": process_after,
|
||||
"start_process": start_process,
|
||||
"duration": duration,
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from pydantic import BaseModel
|
||||
@@ -10,7 +8,8 @@ fake_users_db = {
|
||||
"username": "johndoe",
|
||||
"full_name": "John Doe",
|
||||
"email": "johndoe@example.com",
|
||||
"password_hash": "fakehashedsecret",
|
||||
"hashed_password": "fakehashedsecret",
|
||||
"disabled": False,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +25,9 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: Optional[str] = None
|
||||
full_name: Optional[str] = None
|
||||
disabled: Optional[bool] = None
|
||||
email: str = None
|
||||
full_name: str = None
|
||||
disabled: bool = None
|
||||
|
||||
|
||||
class UserInDB(User):
|
||||
@@ -51,26 +50,27 @@ def fake_decode_token(token):
|
||||
async def get_current_user(token: str = Security(oauth2_scheme)):
|
||||
user = fake_decode_token(token)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Invalid authentication credentials"
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
||||
if not current_user.disabled:
|
||||
if current_user.disabled:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
||||
|
||||
@app.post("/token")
|
||||
async def login(form_data: OAuth2PasswordRequestForm):
|
||||
data = form_data.parse()
|
||||
user_dict = fake_users_db[data.username]
|
||||
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
user_dict = fake_users_db.get(form_data.username)
|
||||
if not user_dict:
|
||||
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
||||
user = UserInDB(**user_dict)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Incorrect email or password")
|
||||
hashed_password = fake_hash_password(data.password)
|
||||
if not hashed_password == user.hashed_password:
|
||||
raise HTTPException(status_code=400, detail="Incorrect email or password")
|
||||
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
||||
|
||||
return {"access_token": user.username, "token_type": "bearer"}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
import jwt
|
||||
from fastapi import Depends, FastAPI, Security
|
||||
@@ -23,7 +22,8 @@ fake_users_db = {
|
||||
"username": "johndoe",
|
||||
"full_name": "John Doe",
|
||||
"email": "johndoe@example.com",
|
||||
"password_hash": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
|
||||
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
|
||||
"disabled": False,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,9 @@ class TokenPayload(BaseModel):
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: Optional[str] = None
|
||||
full_name: Optional[str] = None
|
||||
disabled: Optional[bool] = None
|
||||
email: str = None
|
||||
full_name: str = None
|
||||
disabled: bool = None
|
||||
|
||||
|
||||
class UserInDB(User):
|
||||
@@ -102,24 +102,21 @@ async def get_current_user(token: str = Security(oauth2_scheme)):
|
||||
|
||||
|
||||
async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
||||
if not current_user.disabled:
|
||||
if current_user.disabled:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
||||
|
||||
@app.post("/token", response_model=Token)
|
||||
async def route_login_access_token(form_data: OAuth2PasswordRequestForm):
|
||||
data = form_data.parse()
|
||||
user = authenticate_user(fake_users_db, data.username, data.password)
|
||||
async def route_login_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Incorrect email or password")
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
return {
|
||||
"access_token": create_access_token(
|
||||
data={"username": data.username}, expires_delta=access_token_expires
|
||||
),
|
||||
"token_type": "bearer",
|
||||
}
|
||||
access_token = create_access_token(
|
||||
data={"username": form_data.username}, expires_delta=access_token_expires
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@app.get("/users/me", response_model=User)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
Coming soon...
|
||||
|
||||
```Python
|
||||
{!./src/application-configuration/tutorial001.py!}
|
||||
{!./src/application_configuration/tutorial001.py!}
|
||||
```
|
||||
|
||||
```Python
|
||||
{!./src/application-configuration/tutorial002.py!}
|
||||
{!./src/application_configuration/tutorial002.py!}
|
||||
```
|
||||
|
||||
```Python
|
||||
{!./src/application-configuration/tutorial003.py!}
|
||||
{!./src/application_configuration/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -116,7 +116,7 @@ Again, doing just that declaration, with **FastAPI** you get:
|
||||
|
||||
Apart from normal singular types like `str`, `int`, `float`, etc. You can use more complex singular types that inherit from `str`.
|
||||
|
||||
To see all the options you have, checkout the docs for <a href="https://pydantic-docs.helpmanual.io/#exotic-types" target="_blank">Pydantic's exotic types</a>.
|
||||
To see all the options you have, checkout the docs for <a href="https://pydantic-docs.helpmanual.io/#exotic-types" target="_blank">Pydantic's exotic types</a>. You will see some examples in the next chapter.
|
||||
|
||||
For example, as in the `Image` model we have a `url` field, we can declare it to be instead of a `str`, a Pydantic's `UrlStr`:
|
||||
|
||||
|
||||
71
docs/tutorial/dependencies/advanced-dependencies.md
Normal file
@@ -0,0 +1,71 @@
|
||||
!!! danger
|
||||
This is, more or less, an "advanced" chapter.
|
||||
|
||||
If you are just starting with **FastAPI** you might want to skip this chapter and come back to it later.
|
||||
|
||||
## Parameterized dependencies
|
||||
|
||||
All the dependencies we have seen are a fixed function or class.
|
||||
|
||||
But there could be cases where you want to be able to set parameters on the dependency, without having to declare many different functions or classes.
|
||||
|
||||
Let's imagine that we want to have a dependency that checks if the query parameter `q` contains some fixed content.
|
||||
|
||||
But we want to be able to parameterize that fixed content.
|
||||
|
||||
## A "callable" instance
|
||||
|
||||
In Python there's a way to make an instance of a class a "callable".
|
||||
|
||||
Not the class itself (which is already a callable), but an instance of that class.
|
||||
|
||||
To do that, we declare a method `__call__`:
|
||||
|
||||
```Python hl_lines="10"
|
||||
{!./src/dependencies/tutorial006.py!}
|
||||
```
|
||||
|
||||
## Parameterize the instance
|
||||
|
||||
And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency:
|
||||
|
||||
```Python hl_lines="7"
|
||||
{!./src/dependencies/tutorial006.py!}
|
||||
```
|
||||
|
||||
In this case, **FastAPI** won't ever touch or care about `__init__`, we will use it directly in our code.
|
||||
|
||||
## Create an instance
|
||||
|
||||
We could create an instance of this class with:
|
||||
|
||||
```Python hl_lines="16"
|
||||
{!./src/dependencies/tutorial006.py!}
|
||||
```
|
||||
|
||||
And that way we are able to "parameterize" our dependency, that now has `"bar"` inside of it, as the attribute `checker.fixed_content`.
|
||||
|
||||
## Use the instance as a dependency
|
||||
|
||||
Then, we could use this `checker` in a `Depends(checker)`, instead of `Depends(FixedContentQueryChecker)`, because the dependency is the instance, `checker`, not the class itself.
|
||||
|
||||
And when solving the dependency, **FastAPI** will call this `checker` like:
|
||||
|
||||
```Python
|
||||
checker(q="somequery")
|
||||
```
|
||||
|
||||
...and pass whatever that returns as the value of the dependency in our path operation function as the parameter `fixed_content_included`:
|
||||
|
||||
```Python hl_lines="20"
|
||||
{!./src/dependencies/tutorial006.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
All this might seem contrived. And it might not be very clear how is it useful yet.
|
||||
|
||||
These examples are intentionally simple, but show how it all works.
|
||||
|
||||
In the chapters about security, you will be using utility functions that are implemented in this same way.
|
||||
|
||||
If you understood all this, you already know how those utility tools for security work underneath.
|
||||
174
docs/tutorial/dependencies/classes-as-dependencies.md
Normal file
@@ -0,0 +1,174 @@
|
||||
Before diving deeper into the **Dependency Injection** system, let's upgrade the previous example.
|
||||
|
||||
## A `dict` from the previous example
|
||||
|
||||
In the previous example, we where returning a `dict` from our dependency ("dependable"):
|
||||
|
||||
```Python hl_lines="7"
|
||||
{!./src/dependencies/tutorial001.py!}
|
||||
```
|
||||
|
||||
But then we get a `dict` in the parameter `commons` of the path operation function.
|
||||
|
||||
And we know that `dict`s can't provide a lot of editor support because they can't know their keys and value types.
|
||||
|
||||
We can do better...
|
||||
|
||||
## What makes a dependency
|
||||
|
||||
Up to now you have seen dependencies declared as functions.
|
||||
|
||||
But that's not the only way to declare dependencies (although it would probably be the more common).
|
||||
|
||||
The key factor is that a dependency should be a "callable".
|
||||
|
||||
A "**callable**" in Python is anything that Python can "call" like a function.
|
||||
|
||||
So, if you have an object `something` (that might _not_ be a function) and you can do:
|
||||
|
||||
```Python
|
||||
something()
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```Python
|
||||
something(some_argument, some_keyword_argument="foo")
|
||||
```
|
||||
|
||||
then it is a "callable".
|
||||
|
||||
## Classes as dependencies
|
||||
|
||||
You might notice that to create an instance of a Python class, you use that same syntax.
|
||||
|
||||
So, a Python class is also a **callable**.
|
||||
|
||||
Then, in **FastAPI**, you could use a Python class as a dependency.
|
||||
|
||||
What FastAPI actually checks is that it is a "callable" (function, class or anything else) and the parameters defined.
|
||||
|
||||
If you pass a "callable" as a dependency in **FastAPI**, it will analyze the parameters for that "callable", and process them in the same way as the parameters for a path operation function. Including sub-dependencies.
|
||||
|
||||
That also applies to callables with no parameters at all. The same as would be for path operation functions with no parameteres.
|
||||
|
||||
Then, we can change the dependency "dependable" `common_parameters` from above to the class `CommonQueryParameters`:
|
||||
|
||||
```Python hl_lines="9 10 11 12 13"
|
||||
{!./src/dependencies/tutorial002.py!}
|
||||
```
|
||||
|
||||
Pay attention to the `__init__` method used to create the instance of the class:
|
||||
|
||||
```Python hl_lines="10"
|
||||
{!./src/dependencies/tutorial002.py!}
|
||||
```
|
||||
|
||||
...it has the same parameters as our previous `common_parameters`:
|
||||
|
||||
```Python hl_lines="6"
|
||||
{!./src/dependencies/tutorial001.py!}
|
||||
```
|
||||
|
||||
Those parameters are what **FastAPI** will use to "solve" the dependency.
|
||||
|
||||
In both cases, it will have:
|
||||
|
||||
* an optional `q` query parameter.
|
||||
* a `skip` query parameter, with a default of `0`.
|
||||
* a `limit` query parameter, with a default of `100`.
|
||||
|
||||
In both cases the data will be converted, validated, documented on the OpenAPI schema, etc.
|
||||
|
||||
## Use it
|
||||
|
||||
Now you can declare your dependency using this class.
|
||||
|
||||
And as when **FastAPI** calls that class the value that will be passed as `commons` to your function will be an "instance" of the class, you can declare that parameter `commons` to be of type of the class, `CommonQueryParams`.
|
||||
|
||||
```Python hl_lines="17"
|
||||
{!./src/dependencies/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Type annotation vs `Depends`
|
||||
|
||||
In the code above, you are declaring `commons` as:
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams = Depends(CommonQueryParams)
|
||||
```
|
||||
|
||||
The last `CommonQueryParams`, in:
|
||||
|
||||
```Python
|
||||
... = Depends(CommonQueryParams)
|
||||
```
|
||||
|
||||
...is what **FastAPI** will actually use to know what is the dependency.
|
||||
|
||||
From it is that FastAPI will extract the declared parameters and that is what FastAPI will actually call.
|
||||
|
||||
---
|
||||
|
||||
In this case, the first `CommonQueryParams`, in:
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams ...
|
||||
```
|
||||
|
||||
...doesn't have any special meaning for **FastAPI**. FastAPI won't use it for data conversion, validation, etc. (as it is using the `= Depends(CommonQueryParams)` for that).
|
||||
|
||||
You could actually write just:
|
||||
|
||||
```Python
|
||||
commons = Depends(CommonQueryParams)
|
||||
```
|
||||
|
||||
..as in:
|
||||
|
||||
```Python hl_lines="17"
|
||||
{!./src/dependencies/tutorial003.py!}
|
||||
```
|
||||
|
||||
But declaring the type is encouraged as that way your editor will know what will be passed as the parameter `commons`, and then it can help you with code completion, type checks, etc:
|
||||
|
||||
<img src="/img/tutorial/dependencies/image02.png">
|
||||
|
||||
## Shortcut
|
||||
|
||||
But you see that we are having some code repetition here, writing `CommonQueryParams` twice:
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams = Depends(CommonQueryParams)
|
||||
```
|
||||
|
||||
**FastAPI** provides a shortcut for these cases, in where the dependency is *specifically* a class that **FastAPI** will "call" to create an instance of the class itself.
|
||||
|
||||
For those specific cases, you can do the following:
|
||||
|
||||
Instead of writing:
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams = Depends(CommonQueryParams)
|
||||
```
|
||||
|
||||
...you write:
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams = Depends()
|
||||
```
|
||||
|
||||
So, you can declare the dependency as the type of the variable, and use `Depends()` as the "default" value, without any parameter, instead of having to write the full class *again* inside of `Depends(CommonQueryParams)`.
|
||||
|
||||
So, the same example would look like:
|
||||
|
||||
```Python hl_lines="17"
|
||||
{!./src/dependencies/tutorial004.py!}
|
||||
```
|
||||
|
||||
...and **FastAPI** will know what to do.
|
||||
|
||||
!!! tip
|
||||
If all that seems more confusing than helpful, disregard it, you don't *need* it.
|
||||
|
||||
It is just a shortcut. Because **FastAPI** cares about helping you minimize code repetition.
|
||||
@@ -22,7 +22,7 @@ That's it.
|
||||
|
||||
And it has the same shape and structure that all your path operation functions.
|
||||
|
||||
You can think of it as a path operation function without the "decorator" (the `@app.get("/some-path")`).
|
||||
You can think of it as a path operation function without the "decorator" (without the `@app.get("/some-path")`).
|
||||
|
||||
And it can return anything you want.
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
Before diving deeper into the **Dependency Injection** system, let's upgrade the previous example.
|
||||
|
||||
## A `dict` from the previous example
|
||||
|
||||
In the previous example, we where returning a `dict` from our dependency ("dependable"):
|
||||
|
||||
```Python hl_lines="7"
|
||||
{!./src/dependencies/tutorial001.py!}
|
||||
```
|
||||
|
||||
But then we get a `dict` in the parameter `commons` of the path operation function.
|
||||
|
||||
And we know that `dict`s can't provide a lot of editor support because they can't know their keys and value types.
|
||||
|
||||
## Create a Pydantic model
|
||||
|
||||
But we are already using Pydantic models in other places and we have already seen all the benefits.
|
||||
|
||||
Let's use them here too.
|
||||
|
||||
Create a model for the common parameters (and don't pay attention to the rest, for now):
|
||||
|
||||
```Python hl_lines="11 12 13 14"
|
||||
{!./src/dependencies/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Return a Pydantic model
|
||||
|
||||
Now we can return a Pydantic model from the dependency ("dependable") with the same data as the dict before:
|
||||
|
||||
```Python hl_lines="18"
|
||||
{!./src/dependencies/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Declare the Pydantic model
|
||||
|
||||
We can now come back to the path operation function and declare the type of the `commons` parameter to be that Pydantic model:
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams = Depends(common_parameters)
|
||||
```
|
||||
|
||||
It won't be interpreted as a JSON request `Body` because we are using `Depends`:
|
||||
|
||||
```Python hl_lines="22"
|
||||
{!./src/dependencies/tutorial002.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
In the case of dependencies with `Depends`, the type of the parameter is only to get editor support.
|
||||
|
||||
Your dependencies won't be enforced to return a specific type of data.
|
||||
|
||||
## Use the Pydantic model
|
||||
|
||||
And now we can use that model in our code, with all the lovable editor support:
|
||||
|
||||
```Python hl_lines="24 25 26"
|
||||
{!./src/dependencies/tutorial002.py!}
|
||||
```
|
||||
|
||||
<img src="/img/tutorial/dependencies/image02.png">
|
||||
|
||||
## Trees of hierarchical dependencies
|
||||
|
||||
With the **Dependency Injection** system you can build arbitrarily deep trees of hierarchical dependencies (also known as dependency graphs) by having dependencies that also have dependencies themselves.
|
||||
|
||||
You will see examples of these dependency trees in the next chapters about security.
|
||||
|
||||
## Recap
|
||||
|
||||
By using Pydantic models in your dependencies too you can keep all the editor support that **FastAPI** is designed to support.
|
||||
60
docs/tutorial/dependencies/sub-dependencies.md
Normal file
@@ -0,0 +1,60 @@
|
||||
You can create dependencies that have sub-dependencies.
|
||||
|
||||
They can be as "deep" as you need them to be.
|
||||
|
||||
**FastAPI** will take care of solving them.
|
||||
|
||||
### First dependency "dependable"
|
||||
|
||||
You could create a first dependency ("dependable") like:
|
||||
|
||||
```Python hl_lines="6 7"
|
||||
{!./src/dependencies/tutorial005.py!}
|
||||
```
|
||||
It declares an optional query parameter `q` as a `str`, and then it just returns it.
|
||||
|
||||
This is quite simple (not very useful), but will help us focus on how the sub-dependencies work.
|
||||
|
||||
### Second dependency, "dependable" and "dependant"
|
||||
|
||||
Then you can create another dependency function (a "dependable") that at the same time declares a dependency of its own (so it is a "dependant" too):
|
||||
|
||||
```Python hl_lines="11"
|
||||
{!./src/dependencies/tutorial005.py!}
|
||||
```
|
||||
|
||||
Let's focus on the parameters declared:
|
||||
|
||||
* Even though this function is a dependency ("dependable") itself, it also declares another dependency (it "depends" on something else).
|
||||
* It depends on the `query_extractor`, and assigns the value returned by it to the parameter `q`.
|
||||
* It also declares an optional `last_query` cookie, as a `str`.
|
||||
* Let's imagine that if the user didn't provide any query `q`, we just use the last query used, that we had saved to a cookie before.
|
||||
|
||||
### Use the dependency
|
||||
|
||||
Then we can use the dependency with:
|
||||
|
||||
```Python hl_lines="19"
|
||||
{!./src/dependencies/tutorial005.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
Notice that we are only declaring one dependency in the path operation function, the `query_or_cookie_extractor`.
|
||||
|
||||
But **FastAPI** will know that it has to solve `query_extractor` first, to pass the results of that to `query_or_cookie_extractor` while calling it.
|
||||
|
||||
|
||||
## Recap
|
||||
|
||||
Apart from all the fancy words used here, the **Dependency Injection** system is quite simple.
|
||||
|
||||
Just functions that look the same as the path operation functions.
|
||||
|
||||
But still, it is very powerful, and allows you to declare arbitrarily deeply nested dependency "graphs" (trees).
|
||||
|
||||
!!! tip
|
||||
All this might not seem as useful with these simple examples.
|
||||
|
||||
But you will see how useful it is in the chapters about **security**.
|
||||
|
||||
And you will also see the amounts of code it will save you.
|
||||
64
docs/tutorial/extra-data-types.md
Normal file
@@ -0,0 +1,64 @@
|
||||
Up to now, you have been using common data types, like:
|
||||
|
||||
* `int`
|
||||
* `float`
|
||||
* `str`
|
||||
* `bool`
|
||||
|
||||
But you can also use more complex data types.
|
||||
|
||||
And you will still have the same features as seen up to now:
|
||||
|
||||
* Great editor support.
|
||||
* Data conversion from incoming requests.
|
||||
* Data conversion for response data.
|
||||
* Data validation.
|
||||
* Automatic annotation and documentation.
|
||||
|
||||
## Other data types
|
||||
|
||||
Here are some of the additional data types you can use:
|
||||
|
||||
* `UUID`:
|
||||
* A standard "Universally Unique Identifier", common as an ID in many databases and systems.
|
||||
* In requests and responses will be represented as a `str`.
|
||||
* `datetime.datetime`:
|
||||
* A Python `datetime.datetime`.
|
||||
* In requests and responses will be represented as a `str` in ISO 8601 format, like: `2008-09-15T15:53:00+05:00`.
|
||||
* `datetime.date`:
|
||||
* Python `datetime.date`.
|
||||
* In requests and responses will be represented as a `str` in ISO 8601 format, like: `2008-09-15`.
|
||||
* `datetime.time`:
|
||||
* A Python `datetime.time`.
|
||||
* In requests and responses will be represented as a `str` in ISO 8601 format, like: `14:23:55.003`.
|
||||
* `datetime.timedelta`:
|
||||
* A Python `datetime.timedelta`.
|
||||
* In requests and responses will be represented as a `float` of total seconds.
|
||||
* Pydantic also allows representing it as a "ISO 8601 time diff encoding", <a href="https://pydantic-docs.helpmanual.io/#json-serialisation" target="_blank">see the docs for more info</a>.
|
||||
* `frozenset`:
|
||||
* In requests and responses, treated the same as a `set`:
|
||||
* In requests, a list will be read, eliminating duplicates and converting it to a `set`.
|
||||
* In responses, the `set` will be converted to a `list`.
|
||||
* The generated schema will specify that the `set` values are unique (using JSON Schema's `uniqueItems`).
|
||||
* `bytes`:
|
||||
* Standard Python `bytes`.
|
||||
* In requests and responses will be treated as `str`.
|
||||
* The generated schema will specify that it's a `str` with `binary` "format".
|
||||
* `Decimal`:
|
||||
* Standard Python `Decimal`.
|
||||
* In requests and responses, handled the same as a `float`.
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
Here's an example path operation with parameters using some of the above types.
|
||||
|
||||
```Python hl_lines="1 2 11 12 13 14 15"
|
||||
{!./src/extra_data_types/tutorial001.py!}
|
||||
```
|
||||
|
||||
Note that the parameters inside the function have their natural data type, and you can, for example, perform normal date manipulations, like:
|
||||
|
||||
```Python hl_lines="17 18"
|
||||
{!./src/extra_data_types/tutorial001.py!}
|
||||
```
|
||||
@@ -148,7 +148,7 @@ https://example.com/items/foo
|
||||
!!! info
|
||||
A "path" is also commonly called an "endpoint" or a "route".
|
||||
|
||||
Building an API, the "path" is the main way to separate "concerns" and functionalities.
|
||||
Building an API, the "path" is the main way to separate "concerns" and "resources".
|
||||
|
||||
#### Operation
|
||||
|
||||
@@ -172,7 +172,7 @@ In the HTTP protocol, you can communicate to each path using one (or more) of th
|
||||
|
||||
---
|
||||
|
||||
When building APIs, you normally use these specific HTTP methods to perform a specific operation.
|
||||
When building APIs, you normally use these specific HTTP methods to perform a specific action.
|
||||
|
||||
Normally you use:
|
||||
|
||||
@@ -183,7 +183,7 @@ Normally you use:
|
||||
|
||||
So, in OpenAPI, each of the HTTP methods is called an "operation".
|
||||
|
||||
We are going to call them "operations" too.
|
||||
We are going to call them "**operations**" too.
|
||||
|
||||
#### Define a path operation function
|
||||
|
||||
@@ -196,6 +196,17 @@ The `@app.get("/")` tells **FastAPI** that the function right below is in charge
|
||||
* the path `/`
|
||||
* using a <abbr title="an HTTP GET method"><code>get</code> operation</abbr>
|
||||
|
||||
!!! info "`@decorator` Info"
|
||||
That `@something` syntax in Python is called a "decorator".
|
||||
|
||||
You put it on top of a function. Like a pretty decorative hat (I guess that's where the term came from).
|
||||
|
||||
A "decorator" takes the function below and does something with it.
|
||||
|
||||
In our case, this decorator tells **FastAPI** that the function below corresponds to the **path** `/` with an **operation** `get`.
|
||||
|
||||
It is the "**path operation decorator**".
|
||||
|
||||
You can also use the other operations:
|
||||
|
||||
* `@app.post()`
|
||||
@@ -216,9 +227,15 @@ And the more exotic ones:
|
||||
|
||||
The information here is presented as a guideline, not a requirement.
|
||||
|
||||
For example, when using GraphQL you normally perform all the operations using only `post`.
|
||||
For example, when using GraphQL you normally perform all the actions using only `post`.
|
||||
|
||||
### Step 4: define the path operation function
|
||||
### Step 4: define the **path operation function**
|
||||
|
||||
This is our "**path operation function**":
|
||||
|
||||
* **path**: is `/`.
|
||||
* **operation**: is `get`.
|
||||
* **function**: is the function below the "decorator" (below `@app.get("/")`).
|
||||
|
||||
```Python hl_lines="7"
|
||||
{!./src/first_steps/tutorial001.py!}
|
||||
@@ -226,7 +243,7 @@ And the more exotic ones:
|
||||
|
||||
This is a Python function.
|
||||
|
||||
It will be called by FastAPI whenever it receives a request to the URL "`/`".
|
||||
It will be called by **FastAPI** whenever it receives a request to the URL "`/`" using `GET`.
|
||||
|
||||
In this case, it is an `async` function.
|
||||
|
||||
@@ -238,7 +255,8 @@ You could also define it as a normal function instead of `async def`:
|
||||
{!./src/first_steps/tutorial003.py!}
|
||||
```
|
||||
|
||||
To know the difference, read the section about [Concurrency and `async` / `await`](/async/).
|
||||
!!! note
|
||||
If you don't know the difference, check the _"In a hurry?"_ section about <a href="https://fastapi.tiangolo.com/async/#in-a-hurry" target="_blank">`async` and `await` in the docs</a>.
|
||||
|
||||
### Step 5: return the content
|
||||
|
||||
@@ -250,4 +268,13 @@ You can return a `dict`, `list`, singular values as `str`, `int`, etc.
|
||||
|
||||
You can also return Pydantic models (you'll see more about that later).
|
||||
|
||||
There are many other objects and models that will be automatically converted to JSON.
|
||||
There are many other objects and models that will be automatically converted to JSON (including ORMs, etc). Try using your favorite ones, it's highly probable that they are already supported.
|
||||
|
||||
|
||||
## Recap
|
||||
|
||||
* Import `FastAPI`.
|
||||
* Create an `app` instance.
|
||||
* Write a **path operation decorator** (like `@app.get("/")`).
|
||||
* Write a **path operation function** (like `def root(): ...` above).
|
||||
* Run the debugging server (like `uvicorn main:app --debug`).
|
||||
@@ -10,6 +10,9 @@ You can adapt it to any other NoSQL database like:
|
||||
* **ArangoDB**
|
||||
* **ElasticSearch**, etc.
|
||||
|
||||
!!! tip
|
||||
There is an official project generator with **FastAPI** and **Couchbase**, all based on **Docker**, including a frontend and more tools: <a href="https://github.com/tiangolo/full-stack-fastapi-couchbase" target="_blank">https://github.com/tiangolo/full-stack-fastapi-couchbase</a>
|
||||
|
||||
## Import Couchbase components
|
||||
|
||||
For now, don't pay attention to the rest, only the imports:
|
||||
@@ -49,7 +52,7 @@ This utility function will:
|
||||
* Set defaults for timeouts.
|
||||
* Return it.
|
||||
|
||||
```Python hl_lines="13 14 15 16 17 18 19 20"
|
||||
```Python hl_lines="13 14 15 16 17 18 19 20 21 22"
|
||||
{!./src/nosql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -61,7 +64,7 @@ As **Couchbase** "documents" are actually just "JSON objects", we can model them
|
||||
|
||||
First, let's create a `User` model:
|
||||
|
||||
```Python hl_lines="23 24 25 26 27"
|
||||
```Python hl_lines="25 26 27 28 29"
|
||||
{!./src/nosql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -75,7 +78,7 @@ This will have the data that is actually stored in the database.
|
||||
|
||||
We don't create it as a subclass of Pydantic's `BaseModel` but as a subclass of our own `User`, because it will have all the attributes in `User` plus a couple more:
|
||||
|
||||
```Python hl_lines="30 31 32"
|
||||
```Python hl_lines="32 33 34"
|
||||
{!./src/nosql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -96,7 +99,7 @@ Now create a function that will:
|
||||
|
||||
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:
|
||||
|
||||
```Python hl_lines="35 36 37 38 39 40 41"
|
||||
```Python hl_lines="37 38 39 40 41 42 43"
|
||||
{!./src/nosql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -131,7 +134,7 @@ UserInDB(username="johndoe", hashed_password="some_hash")
|
||||
|
||||
### Create the `FastAPI` app
|
||||
|
||||
```Python hl_lines="45"
|
||||
```Python hl_lines="47"
|
||||
{!./src/nosql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -141,7 +144,7 @@ As our code is calling Couchbase and we are not using the <a href="https://docs.
|
||||
|
||||
Also, Couchbase recommends not using a single `Bucket` object in multiple "<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>s", so, we can get just get the bucket directly and pass it to our utility functions:
|
||||
|
||||
```Python hl_lines="48 49 50 51 52"
|
||||
```Python hl_lines="50 51 52 53 54"
|
||||
{!./src/nosql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ In this case, `item_id` is declared to be an `int`.
|
||||
!!! check
|
||||
This will give you editor support inside of your function, with error checks, completion, etc.
|
||||
|
||||
## Data "parsing"
|
||||
## Data <abbr title="also known as: serialization, parsing, marshalling">conversion</abbr>
|
||||
|
||||
If you run this example and open your browser at <a href="http://127.0.0.1:8000/items/3" target="_blank">http://127.0.0.1:8000/items/3</a>, you will see a response of:
|
||||
|
||||
|
||||
@@ -1,5 +1,167 @@
|
||||
Coming soon...
|
||||
Let's imagine that you have your **backend** API in some domain.
|
||||
|
||||
And you have a **frontend** in another domain or in a different path of the same domain (or in a mobile application).
|
||||
|
||||
And you want to have a way for the frontend to authenticate with the backend, using a **username** and **password**.
|
||||
|
||||
We can use **OAuth2** to build that with **FastAPI**.
|
||||
|
||||
But let's save you the time of reading the full long specification just to find those little pieces of information you need.
|
||||
|
||||
Let's use the tools provided by **FastAPI** to handle security.
|
||||
|
||||
## How it looks
|
||||
|
||||
But let's first just use the code and see how it works, and then we'll come back to understand what's happening.
|
||||
|
||||
## Create `main.py`
|
||||
|
||||
Copy the example in a file `main.py`:
|
||||
|
||||
```Python
|
||||
{!./src/security/tutorial002.py!}
|
||||
{!./src/security/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Run it
|
||||
|
||||
Run the example with:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --debug
|
||||
```
|
||||
|
||||
## Check it
|
||||
|
||||
Go to the interactive docs at: <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
|
||||
You will see something like this:
|
||||
|
||||
<img src="/img/tutorial/security/image01.png">
|
||||
|
||||
!!! check "Authorize button!"
|
||||
You already have a shinny new "Authorize" button.
|
||||
|
||||
And your path operation has a little lock in the top-right corner that you can click.
|
||||
|
||||
|
||||
And if you click it, you have a little authorization form to type a `username` and `password` (and other optional fields):
|
||||
|
||||
<img src="/img/tutorial/security/image02.png">
|
||||
|
||||
!!! note
|
||||
It doesn't matter what you type in the form, it won't work yet. But we'll get there.
|
||||
|
||||
This is of course not the frontend for the final users, but it's a great automatic tool to document interactively all your API.
|
||||
|
||||
It can be used by the frontend team (that can also be yourself).
|
||||
|
||||
It can be used by third party applications and systems.
|
||||
|
||||
And it can also be used by yourself, to debug, check and test the same application.
|
||||
|
||||
|
||||
## The `password` flow
|
||||
|
||||
Now let's go back a bit and understand what is all that.
|
||||
|
||||
The `password` "flow" is one of the ways ("flows") defined in OAuth2, to handle security and authentication.
|
||||
|
||||
OAuth2 was designed so that the backend or API could be independent of the server that authenticates the user.
|
||||
|
||||
But in this case, the same **FastAPI** application will handle the API and the authentication.
|
||||
|
||||
So, let's review it from that simplified point of view:
|
||||
|
||||
* The user types his `username` and `password` in the frontend, and hits `Enter`.
|
||||
* The frontend (running in the user's browser) sends that `username` and `password` to a specific URL in our API.
|
||||
* The API checks that `username` and `password`, and responds with a "token".
|
||||
* A "token" is just a string with some content that we can use later to verify this user.
|
||||
* Normally, a token is set to expire after some time.
|
||||
* So, the user will have to login again at some point later.
|
||||
* And if the token is stolen, the risk is less. It is not like a permanent key that will work forever.
|
||||
* The frontend stores that token temporarily somewhere.
|
||||
* The user clicks in the frontend to go to another section of the frontend web app.
|
||||
* The frontend needs to fetch some more data from the API.
|
||||
* But it needs authentication for that specific endpoint.
|
||||
* So, to authenticate with our API, it sends a header `Authorization` with a value of `Bearer ` plus the token.
|
||||
* If the token contains `foobar`, the content of the `Authorization` header would be: `Bearer foobar`.
|
||||
* Note that although the header is case-insensitive (`Authorization` is the same as `authorization`), the value is not. So, `bearer foobar` would not be valid. It has to be `Bearer foobar`.
|
||||
|
||||
## **FastAPI**'s `Security`
|
||||
|
||||
### Import it
|
||||
|
||||
The same way **FastAPI** provides a `Depends`, there is a `Security` that you can import:
|
||||
|
||||
```Python hl_lines="1"
|
||||
{!./src/security/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Use it
|
||||
|
||||
It is actually a subclass of `Depends`, and it has just one extra parameter that we'll see later.
|
||||
|
||||
But by using `Security` instead of `Depends`, **FastAPI** will know that it can use this dependency to define "security schemes" in OpenAPI.
|
||||
|
||||
```Python hl_lines="10"
|
||||
{!./src/security/tutorial001.py!}
|
||||
```
|
||||
|
||||
In this case, we have a `Security` definition (which at the same time is a dependency definition) that will provide a `str` that is assigned to the parameter `token`.
|
||||
|
||||
## **FastAPI**'s `OAuth2PasswordBearer`
|
||||
|
||||
**FastAPI** provides several tools, at different levels of abstraction, to implement these security features.
|
||||
|
||||
In this example we are going to use **OAuth2**, with the **Password** flow, using a **Bearer** token.
|
||||
|
||||
|
||||
!!! info
|
||||
A "bearer" token is not the only option.
|
||||
|
||||
But it's the best one for our use case.
|
||||
|
||||
And it might be the best for most use cases, unless you are an OAuth2 expert and know exactly why there's another option that suits better your needs.
|
||||
|
||||
In that case, **FastAPI** also provides you with the tools to build it.
|
||||
|
||||
`OAuth2PasswordBearer` is a class that we create passing a parameter of the URL in where the client (the frontend running in the user's browser) can use to send the `username` and `password` and get a token.
|
||||
|
||||
```Python hl_lines="6"
|
||||
{!./src/security/tutorial001.py!}
|
||||
```
|
||||
|
||||
It doesn't create that endpoint / path operation, but declares that that URL is the one that the client should use to get the token. That information is used in OpenAPI, and then in the interactive API documentation systems.
|
||||
|
||||
!!! info
|
||||
If you are a very strict "Pythonista" you might dislike the style of the parameter name `tokenUrl` instead of `token_url`.
|
||||
|
||||
That's because it is using the same name as in the OpenAPI spec. So that if you need to investigate more about any of these security schemes you can just copy and paste it to find more information about it.
|
||||
|
||||
The `oauth2_scheme` variable is an instance of `OAuth2PasswordBearer`, but it is also a "callable".
|
||||
|
||||
It could be called as:
|
||||
|
||||
```Python
|
||||
oauth2_scheme(some, parameters)
|
||||
```
|
||||
|
||||
So, it can be used with `Security` (as it could be used with `Depends`).
|
||||
|
||||
## What it does
|
||||
|
||||
It will go and look in the request for that `Authorization` header, check if the value is `Bearer ` plus some token, and will return the token as a `str`.
|
||||
|
||||
If it doesn't see an `Authorization` header, or the value doesn't have a `Bearer ` token, it will respond with a 403 status code error (`FORBIDDEN`) directly.
|
||||
|
||||
You don't even have to check if the token exists to return an error. You can be sure that if your function is executed, it will have a `str` in that token.
|
||||
|
||||
You can try it already in the interactive docs:
|
||||
|
||||
<img src="/img/tutorial/security/image03.png">
|
||||
|
||||
We are not verifying the validity of the token yet, but that's a start already.
|
||||
|
||||
## Recap
|
||||
|
||||
So, in just 3 or 4 extra lines, you already have some primitive form of security.
|
||||
|
||||
120
docs/tutorial/security/get-current-user.md
Normal file
@@ -0,0 +1,120 @@
|
||||
In the previous chapter the security system (which is based on the dependency injection system) was giving the path operation function a `token` as a `str`:
|
||||
|
||||
```Python hl_lines="10"
|
||||
{!./src/security/tutorial001.py!}
|
||||
```
|
||||
|
||||
But that is still not that useful.
|
||||
|
||||
Let's make it give us the current user.
|
||||
|
||||
## Create a user model
|
||||
|
||||
First, let's create a Pydantic user model.
|
||||
|
||||
The same way we use Pydantic to declare bodies, we can use it anywhere else:
|
||||
|
||||
```Python hl_lines="5 12 13 14 15 16"
|
||||
{!./src/security/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Create a `get_current_user` dependency
|
||||
|
||||
Let's create a dependency `get_current_user`.
|
||||
|
||||
Remember that dependencies can have sub-dependencies?
|
||||
|
||||
And remember that `Security` is based on `Depends`?
|
||||
|
||||
So, we can have sub-dependencies using `Security` too.
|
||||
|
||||
`get_current_user` will have a `Security` dependency with the same `oauth2_scheme` we created before.
|
||||
|
||||
The same as we were doing before in the path operation direclty, our new dependency will receive a `token` as a `str` from the `Security` dependency:
|
||||
|
||||
```Python hl_lines="25"
|
||||
{!./src/security/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Get the user
|
||||
|
||||
`get_current_user` will use a (fake) utility function we created, that takes a token as a `str` and returns our Pydantic `User` model:
|
||||
|
||||
```Python hl_lines="19 20 21 22 26 27"
|
||||
{!./src/security/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Inject the current user
|
||||
|
||||
So now we can use the same `Depends` with our `get_current_user` in the path operation:
|
||||
|
||||
```Python hl_lines="31"
|
||||
{!./src/security/tutorial002.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
Here you could actually use `Security` instead of depends too.
|
||||
|
||||
But it is not required.
|
||||
|
||||
The key point where you should use `Security` is when passing an instance of `OAuth2PasswordBearer`.
|
||||
|
||||
Because **FastAPI** will use the fact that you are using `Security` and that you are passing an instance of that class `OAuth2PasswordBearer` (that inherits from `SecurityBase`) to create all the security definitions in OpenAPI.
|
||||
|
||||
Notice that we declare the type of `current_user` as the Pydantic model `User`.
|
||||
|
||||
This will help us inside of the function with all the completion and type checks.
|
||||
|
||||
!!! tip
|
||||
You might remember that request bodies are also declared with Pydantic models.
|
||||
|
||||
Here **FastAPI** won't get confused because you are using `Depends` or `Security`.
|
||||
|
||||
!!! check
|
||||
The way this dependency system is designed allows us to have different dependencies (different "dependables") that all return a `User` model.
|
||||
|
||||
We are not restricted to having only one dependency that can return that type of data.
|
||||
|
||||
|
||||
## Other models
|
||||
|
||||
You can now get the current user directly in the path operation functions and deal with the security mechanisms at the **Dependency Injection** level, using `Security`.
|
||||
|
||||
And you can use any model or data for the security requirements (in this case, a Pydantic model `User`).
|
||||
|
||||
But you are not restricted to using some specific data model, class or type.
|
||||
|
||||
Do you want to have an `id` and `email` and not have any `username` in your model? Sure. You can use these same tools.
|
||||
|
||||
Do you want to just have a `str`? Or just a `dict`? Or a database class model instance directly? It all works the same way.
|
||||
|
||||
|
||||
## Code size
|
||||
|
||||
This example might seem verbose. Have in mind that we are mixing security, data models utility functions and path operations in the same file.
|
||||
|
||||
But here's the key point.
|
||||
|
||||
The security and dependency injection stuff is written once.
|
||||
|
||||
And you can make it as complex as you want. And still, have it written only once, in a single place.
|
||||
|
||||
But you can have thousands of endpoints (path operations) using the same security system.
|
||||
|
||||
And all of them (or any portion of them that you want) can take the advantage of re-using these dependencies or any other dependencies you create.
|
||||
|
||||
And all these thousands of path operations can be as small as 3 lines:
|
||||
|
||||
```Python hl_lines="30 31 32"
|
||||
{!./src/security/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Recap
|
||||
|
||||
You can now get the current user directly in your path operation function.
|
||||
|
||||
We are already halfway there.
|
||||
|
||||
We just need to add a path operation for the user / client to actually send the `username` and `password`.
|
||||
|
||||
That comes next.
|
||||
@@ -1,5 +1,93 @@
|
||||
Coming soon...
|
||||
There are many ways to handle security, authentication and autorization.
|
||||
|
||||
```Python
|
||||
{!./src/security/tutorial001.py!}
|
||||
```
|
||||
And it normally is a complex and "difficult" topic.
|
||||
|
||||
In many frameworks and systems just handling security and authentication takes a big amount of effort and code (in many cases it can be 50% or more of all the code written).
|
||||
|
||||
**FastAPI** provides several tools to help you deal with **Security** easily, rapidly, in a standard way, without having to study and learn all the security specifications.
|
||||
|
||||
But first, let's check some small concepts.
|
||||
|
||||
## In a hurry?
|
||||
|
||||
If you don't care about any of these terms and you just need to add security with authentication based on username and password *right now*, skip to the next chapters.
|
||||
|
||||
## OAuth2
|
||||
|
||||
OAuth2 is a specification that defines several ways to handle authentication and autorization.
|
||||
|
||||
It is quite an extensive especification and covers several complex use cases.
|
||||
|
||||
It includes ways to authenticate using a "third party".
|
||||
|
||||
That's what all the system with "login with Facebook, Google, Twitter, GitHub" use underneath.
|
||||
|
||||
### OAuth 1
|
||||
|
||||
There was an OAuth 1, which is very different from OAuth2, and more complex, as it included directly specifications on how to encrypt the communication.
|
||||
|
||||
It is not very popular or used nowadays.
|
||||
|
||||
OAuth2 doesn't specify how to encrypt the communication, it expects you to have your application served with HTTPS.
|
||||
|
||||
!!! tip
|
||||
In the section about **deployment** you will see how to set up HTTPS for free, using Traefik and Let's Encrypt.
|
||||
|
||||
|
||||
## OpenID Connect
|
||||
|
||||
OpenID Connect is another specification, based on **OAuth2**.
|
||||
|
||||
It just extends OAuth2 specifying some things that are relatively ambiguous in OAuth2, to try to make it more interoperable.
|
||||
|
||||
For example, Google login used OpenID Connect (which underneath uses OAuth2).
|
||||
|
||||
But Facebook login doesn't support OpenID Connect. It has its own flavor of OAuth2.
|
||||
|
||||
### OpenID (not "OpenID Connect")
|
||||
|
||||
There was also an "OpenID" specification. That tried to solve the same thing as **OpenID Connect**, but was not based on OAuth2.
|
||||
|
||||
So, it was a complete additional system.
|
||||
|
||||
It is not very popular or used nowadays.
|
||||
|
||||
## OpenAPI
|
||||
|
||||
OpenAPI (previously known as Swagger) is the open specification for building APIs (now part of the Linux Foundation).
|
||||
|
||||
**FastAPI** is based on **OpenAPI**.
|
||||
|
||||
That's what makes it possible to have multiple automatic interactive documentation interfaces, code generation, etc.
|
||||
|
||||
OpenAPI has a way to define multiple security "schemes".
|
||||
|
||||
By using them, you can take advantage of all these standard-based tools, including these interactive documentation systems.
|
||||
|
||||
OpenAPI defines the following security schemes:
|
||||
|
||||
* `apiKey`: an application specific key that can come from:
|
||||
* A query parameter.
|
||||
* A header.
|
||||
* A cookie.
|
||||
* `http`: standard HTTP authentication systems, including:
|
||||
* `bearer`: a header `Authorization` with a value of `Bearer ` plus a token. This is inherited from OAuth2.
|
||||
* HTTP Basic authentication.
|
||||
* HTTP Digest, etc.
|
||||
* `oauth2`: all the OAuth2 ways to handle security (called "flows").
|
||||
* Several of these flows are appropriate for delegating the authentication to a third party (like Google, Facebook, Twitter, GitHub, etc):
|
||||
* `implicit`
|
||||
* `clientCredentials`
|
||||
* `authorizationCode`
|
||||
* But there is one specific "flow" that can be perfectly used for handling authentication in the same application directly:
|
||||
* `password`: some next chapters will cover examples of this.
|
||||
* `openIdConnect`: has a way to define how to discover OAuth2 authentication data automatically.
|
||||
* This automatic discovery is what is defined in the OpenID Connect specification.
|
||||
|
||||
## **FastAPI** utilities
|
||||
|
||||
FastAPI provides several tools for each of these security schemes in the `fastapi.security` module, to simplify using these security mechanisms.
|
||||
|
||||
In the next chapters you will see how to add security to your API in a very simple way, using the tools provided by **FastAPI**.
|
||||
|
||||
And you will also see how it gets automatically integrated into the interactive documentation system.
|
||||
|
||||
@@ -1,5 +1,207 @@
|
||||
Coming soon...
|
||||
Now that we have all the security flow, let's make the application actually secure, using JWT tokens and secure password hashing.
|
||||
|
||||
This code is something you can actually use in your application, save the password hashes in your database, etc.
|
||||
|
||||
We are going to start from where we left in the previous chapter and increment it.
|
||||
|
||||
## About JWT
|
||||
|
||||
JWT means "JSON Web Tokens".
|
||||
|
||||
It's a standard to codify a JSON object in a long string.
|
||||
|
||||
It is not encrypted, so, anyone could recover the information from the contents.
|
||||
|
||||
But it's signed. So, when you receive a token that you emitted, you can verify that you actually emitted it.
|
||||
|
||||
That way, you can create a token with an expiration of, let's say, 1 week, and then, after a week, when the user comes back with the token, you know he's still signed into your system.
|
||||
|
||||
And after a week, the token will be expired. And if the user (or a third party) tried to modify the token to change the expiration, you would be able to discover it, because the signature would not match.
|
||||
|
||||
If you want to play with JWT tokens and see how they work, check <a href="https://jwt.io/" target="_blank">https://jwt.io</a>.
|
||||
|
||||
## Install `PyJWT`
|
||||
|
||||
We need to install `PyJWT` to generate and verity the JWT tokens in Python:
|
||||
|
||||
```bash
|
||||
pip install pyjwt
|
||||
```
|
||||
|
||||
## Password hashing
|
||||
|
||||
"Hashing" means converting some content (a password in this case) into a sequence of bytes (just a string) that look like gibberish.
|
||||
|
||||
Whenever you pass exactly the same content (exactly the same password) you get exactly the same gibberish.
|
||||
|
||||
But you cannot convert from the gibberish back to the password.
|
||||
|
||||
### What for?
|
||||
|
||||
If your database is stolen, the thief won't have your users' plaintext passwords, only the hashes.
|
||||
|
||||
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).
|
||||
|
||||
## Install `passlib`
|
||||
|
||||
PassLib is a great Python package to handle password hashes.
|
||||
|
||||
It supports many secure hashing algorithms, and utilities to work with them.
|
||||
|
||||
The recommended algorithm is "Bcrypt".
|
||||
|
||||
So, install PassLib with Bcrypt:
|
||||
|
||||
```Python
|
||||
pip install passlib[bcrypt]
|
||||
```
|
||||
|
||||
!!! tip
|
||||
With `passlib`, you could even configure it to be able to read passwords created by **Django** (among many others).
|
||||
|
||||
So, you would be able to, for example, share the same data from a Django application in a database with a FastAPI application. Or gradually migrate a Django application using the same database.
|
||||
|
||||
|
||||
## Hash and verify the passwords
|
||||
|
||||
Import the tools we need from `passlib`.
|
||||
|
||||
Create a PassLib "context". This is what will be used to hash and verify passwords.
|
||||
|
||||
!!! tip
|
||||
The PassLib context also has functionality to use different hashing algorithms, deprecate old ones, but allow verifying them, etc.
|
||||
|
||||
For example, you could use it to read and verify passwords generated by another system (like Django) but hash any new passwords with a different algorithm like Bcrypt.
|
||||
|
||||
And be compatible with all of them at the same time.
|
||||
|
||||
Create a utility function to hash a password coming from the user.
|
||||
|
||||
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"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
!!! note
|
||||
If you check the new (fake) database `fake_users_db`, you will see how the hashed password looks like now: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`.
|
||||
|
||||
## Handle JWT tokens
|
||||
|
||||
Import the modules installed.
|
||||
|
||||
Create a random secret key that will be used to sign the JWT tokens.
|
||||
|
||||
To generate a secure random secret, key use the command:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
And copy the output to the variable `SECRET_KEY` (don't use the one in the example).
|
||||
|
||||
Create a variable `ALGORITHM` with the algorithm used to sign the JWT token and set it to `"HS256"`.
|
||||
|
||||
And another one for the `TOKEN_SUBJECT`, and set it to, for example, `"access"`.
|
||||
|
||||
Create a variable for the expiration of the token.
|
||||
|
||||
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"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
## Update the dependencies
|
||||
|
||||
Update `get_current_user` to receive the same token as before, but this time, using JWT tokens.
|
||||
|
||||
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"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
## Update the `/token` path operation
|
||||
|
||||
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"
|
||||
{!./src/security/tutorial004.py!}
|
||||
```
|
||||
|
||||
## Check it
|
||||
|
||||
Run the server and go to the docs: <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
|
||||
You'll see the user interface like:
|
||||
|
||||
<img src="/img/tutorial/security/image07.png">
|
||||
|
||||
Authorize the application the same way as before.
|
||||
|
||||
Using the credentials:
|
||||
|
||||
Username: `johndoe`
|
||||
Password: `secret`
|
||||
|
||||
!!! check
|
||||
Notice that nowhere in the code is the plaintext password "`secret`", we only have the hashed version.
|
||||
|
||||
<img src="/img/tutorial/security/image08.png">
|
||||
|
||||
Call the endpoint `/users/me`, you will get the response as:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"username": "johndoe",
|
||||
"email": "johndoe@example.com",
|
||||
"full_name": "John Doe",
|
||||
"disabled": false
|
||||
}
|
||||
```
|
||||
|
||||
<img src="/img/tutorial/security/image09.png">
|
||||
|
||||
If you open the developer tools, you could see how the data sent and received is just the token, the password is only sent in the first request to authenticate the user:
|
||||
|
||||
<img src="/img/tutorial/security/image10.png">
|
||||
|
||||
!!! note
|
||||
Notice the header `Authorization`, with a value that starts with `Bearer `.
|
||||
|
||||
## Advanced usage with `scopes`
|
||||
|
||||
We didn't use it in this example, but `Security` can receive a parameter `scopes`, as a list of strings.
|
||||
|
||||
It would describe the scopes required for a specific path operation, as different path operations might require different security scopes, even while using the same `OAuth2PasswordBearer` (or any of the other tools).
|
||||
|
||||
This only applies to OAuth2, and it might be, more or less, an advanced feature, but it is there, if you need to use it.
|
||||
|
||||
## Recap
|
||||
|
||||
This concludes our tour for the security features of **FastAPI**.
|
||||
|
||||
In almost any framework handling the security becomes a rather complex subject quite quickly.
|
||||
|
||||
Many packages that simplify it a lot have to make many compromises with the data model, database, and available features. And some of these packages that simplify things too much actually have security flaws underneath.
|
||||
|
||||
---
|
||||
|
||||
**FastAPI** doesn't make any compromise with any database, data model or tool.
|
||||
|
||||
It gives you all the flexibility to chose the ones that fit your project the best.
|
||||
|
||||
And you can use directly many well maintained and widely used packages like `passlib` and `pyjwt`, because **FastAPI** doesn't require any complex mechanisms to integrate external packages.
|
||||
|
||||
But it provides you the tools to simplify the process as much as possible without compromising flexibility, robustness or security.
|
||||
|
||||
And you can use secure, standard protocols like OAuth2 in a relatively simple way.
|
||||
|
||||
@@ -1,5 +1,205 @@
|
||||
Coming soon...
|
||||
Now let's build from the previous chapter and add the missing parts to have a complete security flow.
|
||||
|
||||
```Python
|
||||
## Get the `username` and `password`
|
||||
|
||||
We are going to use **FastAPI** security utilities to get the `username` and `password`.
|
||||
|
||||
OAuth2 specifies that when using the "password flow" (that we are using) the client / user must send a `username` and `password` fields as form data.
|
||||
|
||||
And the spec says that the fields have to be named like that. So `user-name` or `email` wouldn't work.
|
||||
|
||||
But don't worry, you can show it as you wish to your final users in the frontend.
|
||||
|
||||
And your database models can use any other names you want.
|
||||
|
||||
But for the login path operation, we need to use these names to be compatible with the spec (and be able to, for example, use the integrated API documentation system).
|
||||
|
||||
The spec also states that the `username` and `password` must be sent as form data (so, no JSON here).
|
||||
|
||||
### `scope`
|
||||
|
||||
The spec also says that the client can send another form field "`scope`".
|
||||
|
||||
The form field name is `scope` (in singular), but it is actually a long string with "scopes" separated by spaces.
|
||||
|
||||
Each "scope" is just a string (without spaces).
|
||||
|
||||
They are normally used to declare specific security permissions, for exampe:
|
||||
|
||||
* `"users:read"` or `"users:write"` are common examples.
|
||||
* `instagram_basic` is used by Facebook / Instagram.
|
||||
* `https://www.googleapis.com/auth/drive` is used by Google.
|
||||
|
||||
!!! info
|
||||
In OAuth2 a "scope" is just a string that declares a specific permision required.
|
||||
|
||||
It doesn't matter if it has other characters like `:`, or if it is a URL.
|
||||
|
||||
Those details are implementation specific.
|
||||
|
||||
For OAuth2 they are just strings.
|
||||
|
||||
|
||||
## Code to get the `username` and `password`
|
||||
|
||||
Now let's use the utilities provided by **FastAPI** to handle this.
|
||||
|
||||
### `OAuth2PasswordRequestForm`
|
||||
|
||||
First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depends` for the path `/token`:
|
||||
|
||||
```Python hl_lines="2 66"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
`OAuth2PasswordRequestForm` is a class dependency that declares a form body with:
|
||||
|
||||
* The `username`.
|
||||
* The `password`.
|
||||
* An optional `scope` field as a big string, composed of strings separated by spaces.
|
||||
* An optional `grant_type`.
|
||||
|
||||
!!! tip
|
||||
The OAuth2 spec actually *requires* a field `grant_type` with a fixed value of `password`, but `OAuth2PasswordRequestForm` doesn't enforce it.
|
||||
|
||||
If you need to enforce it, use `OAuth2PasswordRequestFormStrict` instead of `OAuth2PasswordRequestForm`.
|
||||
|
||||
* An optional `client_id` (we don't need it for our example).
|
||||
* An optional `client_secret` (we don't need it for our example).
|
||||
|
||||
### Use the form data
|
||||
|
||||
!!! tip
|
||||
The instance of the dependency class `OAuth2PasswordRequestForm` won't have an attribute `scope` with the long string separated by spaces, instead, it will have a `scopes` attribute with the actual list of strings for each scope sent.
|
||||
|
||||
We are not using `scopes` in this example, but the functionality is there if you need it.
|
||||
|
||||
Now, get the user data from the (fake) database, using the `username` from the form field.
|
||||
|
||||
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:
|
||||
|
||||
```Python hl_lines="4 67 68 69"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
### Check the password
|
||||
|
||||
At this point we have a the user data from our database, but we haven't checked the password.
|
||||
|
||||
Let's put that data in the Pydantic `UserInDB` model first.
|
||||
|
||||
You should never save plaintext passwords, so, we'll use the (fake) password hashing system.
|
||||
|
||||
If the passwords don't match, we return the same error.
|
||||
|
||||
```Python hl_lines="70 71 72 73"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
#### About `**user_dict`
|
||||
|
||||
`UserInDB(**user_dict)` means:
|
||||
|
||||
Pass the keys and values of the `user_dict` directly as key-value arguments, equivalent to:
|
||||
|
||||
```Python
|
||||
UserInDB(
|
||||
username = user_dict["username"],
|
||||
email = user_dict["email"],
|
||||
full_name = user_dict["full_name"],
|
||||
disabled = user_dict["disabled"],
|
||||
hashed_password = user_dict["hashed_password"],
|
||||
)
|
||||
```
|
||||
|
||||
## Return the token
|
||||
|
||||
The response of the `token` endpoint must be a JSON object.
|
||||
|
||||
It should have a `token_type`. In our case, as we are using "Bearer" tokens, the token type should be "`bearer`".
|
||||
|
||||
And it should have an `access_token`, with a string containing our access token.
|
||||
|
||||
For this simple example, we are going to just be completely insecure and return the same `username` as the token.
|
||||
|
||||
!!! tip
|
||||
In the next chapter, you will see a real secure implementation, with password hashing and JWT tokens.
|
||||
|
||||
But for now, let's focus on the specific details we need.
|
||||
|
||||
```Python hl_lines="75"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
## Update the dependencies
|
||||
|
||||
Now we are going to update our dependencies.
|
||||
|
||||
We want to get the `current_user` *only* if this user is active.
|
||||
|
||||
So, we create an additional dependency `get_current_active_user` that in turn uses `get_current_user` as a dependency.
|
||||
|
||||
Both of these dependencies will just return an HTTP error if the user doesn't exists, or if is inactive.
|
||||
|
||||
So, in our endpoint, we will only get a user if the user exists, was correctly authenticated, and is active:
|
||||
|
||||
```Python hl_lines="50 51 52 53 54 55 56 59 60 61 62 79"
|
||||
{!./src/security/tutorial003.py!}
|
||||
```
|
||||
|
||||
## See it in action
|
||||
|
||||
Open the interactive docs: <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
|
||||
### Authenticate
|
||||
|
||||
Click the "Authorize" button.
|
||||
|
||||
Use the credentials:
|
||||
|
||||
User: `johndoe`
|
||||
Password: `secret`
|
||||
|
||||
<img src="/img/tutorial/security/image04.png">
|
||||
|
||||
After authenticating in the system, you will see it like:
|
||||
|
||||
<img src="/img/tutorial/security/image05.png">
|
||||
|
||||
### Get your own user data
|
||||
|
||||
Now use the operation `GET` with the path `/users/me`.
|
||||
|
||||
You will get your user's data, like:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"username": "johndoe",
|
||||
"email": "johndoe@example.com",
|
||||
"full_name": "John Doe",
|
||||
"disabled": false,
|
||||
"hashed_password": "fakehashedsecret"
|
||||
}
|
||||
```
|
||||
|
||||
<img src="/img/tutorial/security/image06.png">
|
||||
|
||||
If you click the lock icon and logout, and then try the same operation again, you will get an HTTP 403 error of:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"detail": "Not authenticated"
|
||||
}
|
||||
```
|
||||
|
||||
## Recap
|
||||
|
||||
You now have the tools to implement a complete security system based on `username` and `password` for your API.
|
||||
|
||||
Using these tools, you can make the security system compatible with any database and with any user or data model.
|
||||
|
||||
The only detail missing is that it is not actually "secure" yet.
|
||||
|
||||
In the next chapter you'll see how to use a secure password hashing library and JWT tokens.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
**FastAPI** doesn't require you to use a SQL (relational) database.
|
||||
|
||||
But you can use relational database that you want.
|
||||
But you can use any relational database that you want.
|
||||
|
||||
Here we'll see an example using <a href="https://www.sqlalchemy.org/" target="_blank">SQLAlchemy</a>.
|
||||
|
||||
@@ -69,13 +69,13 @@ That way you don't have to declare them explicitly.
|
||||
|
||||
So, your models will behave very similarly to, for example, Flask-SQLAlchemy.
|
||||
|
||||
```Python hl_lines="15 16 17 18 19"
|
||||
```Python hl_lines="16 17 18 19 20"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Create the SQLAlchemy `Base` model
|
||||
|
||||
```Python hl_lines="22"
|
||||
```Python hl_lines="23"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -85,7 +85,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="25 26 27 28 29"
|
||||
```Python hl_lines="26 27 28 29 30"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -93,7 +93,7 @@ Here's a user model that will be a table in the database:
|
||||
|
||||
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:
|
||||
|
||||
```Python hl_lines="32 33"
|
||||
```Python hl_lines="33 34"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -103,7 +103,7 @@ Now, finally, here's the standard **FastAPI** code.
|
||||
|
||||
Create your app and path operation function:
|
||||
|
||||
```Python hl_lines="37 40 41 42 43"
|
||||
```Python hl_lines="38 41 42 43 44"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
@@ -131,7 +131,7 @@ user = get_user(username, db_session)
|
||||
|
||||
Then we should declare the path operation without `async def`, just with a normal `def`:
|
||||
|
||||
```Python hl_lines="41"
|
||||
```Python hl_lines="42"
|
||||
{!./src/sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.1.12"
|
||||
__version__ = "0.1.14"
|
||||
|
||||
from .applications import FastAPI
|
||||
from .routing import APIRouter
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
from copy import deepcopy
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import Any, Callable, Dict, List, Mapping, Sequence, Tuple, Type
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import params
|
||||
from fastapi.dependencies.models import Dependant, SecurityRequirement
|
||||
@@ -16,7 +19,18 @@ from pydantic.utils import lenient_issubclass
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.requests import Request
|
||||
|
||||
param_supported_types = (str, int, float, bool)
|
||||
param_supported_types = (
|
||||
str,
|
||||
int,
|
||||
float,
|
||||
bool,
|
||||
UUID,
|
||||
date,
|
||||
datetime,
|
||||
time,
|
||||
timedelta,
|
||||
Decimal,
|
||||
)
|
||||
|
||||
|
||||
def get_sub_dependant(*, param: inspect.Parameter, path: str) -> Dependant:
|
||||
@@ -74,7 +88,7 @@ def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant:
|
||||
assert (
|
||||
lenient_issubclass(param.annotation, param_supported_types)
|
||||
or param.annotation == param.empty
|
||||
), f"Path params must be of type str, int, float or boot: {param}"
|
||||
), f"Path params must be of one of the supported types"
|
||||
param = signature_params[param_name]
|
||||
add_param_to_fields(
|
||||
param=param,
|
||||
|
||||
@@ -3,7 +3,7 @@ from types import GeneratorType
|
||||
from typing import Any, Set
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic.json import pydantic_encoder
|
||||
from pydantic.json import ENCODERS_BY_TYPE
|
||||
|
||||
|
||||
def jsonable_encoder(
|
||||
@@ -41,4 +41,19 @@ def jsonable_encoder(
|
||||
)
|
||||
for item in obj
|
||||
]
|
||||
return pydantic_encoder(obj)
|
||||
errors = []
|
||||
try:
|
||||
encoder = ENCODERS_BY_TYPE[type(obj)]
|
||||
return encoder(obj)
|
||||
except KeyError as e:
|
||||
errors.append(e)
|
||||
try:
|
||||
data = dict(obj)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
try:
|
||||
data = vars(obj)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
raise ValueError(errors)
|
||||
return jsonable_encoder(data, by_alias=by_alias, include_none=include_none)
|
||||
|
||||
@@ -241,7 +241,6 @@ class Form(Body):
|
||||
self,
|
||||
default: Any,
|
||||
*,
|
||||
sub_key: bool = False,
|
||||
media_type: str = "application/x-www-form-urlencoded",
|
||||
alias: str = None,
|
||||
title: str = None,
|
||||
@@ -257,7 +256,7 @@ class Form(Body):
|
||||
):
|
||||
super().__init__(
|
||||
default,
|
||||
embed=sub_key,
|
||||
embed=True,
|
||||
media_type=media_type,
|
||||
alias=alias,
|
||||
title=title,
|
||||
@@ -278,7 +277,6 @@ class File(Form):
|
||||
self,
|
||||
default: Any,
|
||||
*,
|
||||
sub_key: bool = False,
|
||||
media_type: str = "multipart/form-data",
|
||||
alias: str = None,
|
||||
title: str = None,
|
||||
@@ -294,7 +292,6 @@ class File(Form):
|
||||
):
|
||||
super().__init__(
|
||||
default,
|
||||
embed=sub_key,
|
||||
media_type=media_type,
|
||||
alias=alias,
|
||||
title=title,
|
||||
|
||||
@@ -22,9 +22,10 @@ from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
|
||||
def serialize_response(*, field: Field = None, response: Response) -> Any:
|
||||
encoded = jsonable_encoder(response)
|
||||
if field:
|
||||
errors = []
|
||||
value, errors_ = field.validate(response, {}, loc=("response",))
|
||||
value, errors_ = field.validate(encoded, {}, loc=("response",))
|
||||
if isinstance(errors_, ErrorWrapper):
|
||||
errors.append(errors_)
|
||||
elif isinstance(errors_, list):
|
||||
@@ -33,7 +34,7 @@ def serialize_response(*, field: Field = None, response: Response) -> Any:
|
||||
raise ValidationError(errors)
|
||||
return jsonable_encoder(value)
|
||||
else:
|
||||
return jsonable_encoder(response)
|
||||
return encoded
|
||||
|
||||
|
||||
def get_app(
|
||||
@@ -51,17 +52,20 @@ def get_app(
|
||||
try:
|
||||
body = None
|
||||
if body_field:
|
||||
body_bytes = await request.body()
|
||||
if body_bytes and is_body_form:
|
||||
if is_body_form:
|
||||
raw_body = await request.form()
|
||||
body = {}
|
||||
form_fields = {}
|
||||
for field, value in raw_body.items():
|
||||
if isinstance(value, UploadFile):
|
||||
body[field] = await value.read()
|
||||
form_fields[field] = await value.read()
|
||||
else:
|
||||
body[field] = value
|
||||
elif body_bytes:
|
||||
body = await request.json()
|
||||
form_fields[field] = value
|
||||
if form_fields:
|
||||
body = form_fields
|
||||
else:
|
||||
body_bytes = await request.body()
|
||||
if body_bytes:
|
||||
body = await request.json()
|
||||
except Exception as e:
|
||||
logging.error("Error getting request body", e)
|
||||
raise HTTPException(
|
||||
@@ -83,40 +87,10 @@ def get_app(
|
||||
raw_response = await run_in_threadpool(dependant.call, **values)
|
||||
if isinstance(raw_response, Response):
|
||||
return raw_response
|
||||
if isinstance(raw_response, BaseModel):
|
||||
return content_type(
|
||||
content=serialize_response(
|
||||
field=response_field, response=raw_response
|
||||
),
|
||||
status_code=status_code,
|
||||
)
|
||||
errors = []
|
||||
try:
|
||||
return content_type(
|
||||
content=serialize_response(
|
||||
field=response_field, response=raw_response
|
||||
),
|
||||
status_code=status_code,
|
||||
)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
try:
|
||||
response = dict(raw_response)
|
||||
return content_type(
|
||||
content=serialize_response(field=response_field, response=response),
|
||||
status_code=status_code,
|
||||
)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
try:
|
||||
response = vars(raw_response)
|
||||
return content_type(
|
||||
content=serialize_response(field=response_field, response=response),
|
||||
status_code=status_code,
|
||||
)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
raise ValueError(errors)
|
||||
response_data = serialize_response(
|
||||
field=response_field, response=raw_response
|
||||
)
|
||||
return content_type(content=response_data, status_code=status_code)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.openapi.models import OAuth2 as OAuth2Model, OAuthFlows as OAuthFlowsModel
|
||||
from fastapi.params import Form
|
||||
from fastapi.security.base import SecurityBase
|
||||
from pydantic import BaseModel, Schema
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
class OAuth2PasswordRequestData(BaseModel):
|
||||
grant_type: str = "password"
|
||||
username: str
|
||||
password: str
|
||||
scope: Optional[List[str]] = None
|
||||
# Client ID and secret might come from headers
|
||||
client_id: Optional[str] = None
|
||||
client_secret: Optional[str] = None
|
||||
|
||||
|
||||
class OAuth2PasswordRequestForm(BaseModel):
|
||||
class OAuth2PasswordRequestForm:
|
||||
"""
|
||||
This is not a "Security" model. Use it as request Body. As in:
|
||||
This is a dependency class, use it like:
|
||||
|
||||
@app.post("/login")
|
||||
def login(form_data: Oauth2PasswordRequestForm):
|
||||
def login(form_data: Oauth2PasswordRequestForm = Depends()):
|
||||
data = form_data.parse()
|
||||
print(data.username)
|
||||
print(data.password)
|
||||
for scope in data.scope:
|
||||
for scope in data.scopes:
|
||||
print(scope)
|
||||
if data.client_id:
|
||||
print(data.client_id)
|
||||
@@ -39,8 +29,8 @@ class OAuth2PasswordRequestForm(BaseModel):
|
||||
It creates the following Form request parameters in your endpoint:
|
||||
|
||||
grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password".
|
||||
Nevertheless, this model is permissive and allows not passing it. If you want to enforce it,
|
||||
use instead the OAuth2PasswordRequestFormStrict model.
|
||||
Nevertheless, this dependency class is permissive and allows not passing it. If you want to enforce it,
|
||||
use instead the OAuth2PasswordRequestFormStrict dependency.
|
||||
username: username string. The OAuth2 spec requires the exact field name "username".
|
||||
password: password string. The OAuth2 spec requires the exact field name "password".
|
||||
scope: Optional string. Several scopes (each one a string) separated by spaces. E.g.
|
||||
@@ -49,33 +39,75 @@ class OAuth2PasswordRequestForm(BaseModel):
|
||||
using HTTP Basic auth, as: client_id:client_secret
|
||||
client_secret: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
|
||||
using HTTP Basic auth, as: client_id:client_secret
|
||||
|
||||
|
||||
It has the method parse() that returns a model with all the same data and the scopes extracted as a list of strings.
|
||||
"""
|
||||
|
||||
grant_type: str = Schema(None, regex="password")
|
||||
username: str
|
||||
password: str
|
||||
scope: str = ""
|
||||
# Client ID and secret might come from headers
|
||||
client_id: Optional[str] = None
|
||||
client_secret: Optional[str] = None
|
||||
|
||||
def parse(self) -> OAuth2PasswordRequestData:
|
||||
return OAuth2PasswordRequestData(
|
||||
grant_type=self.grant_type,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
scope=self.scope.split(),
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
)
|
||||
def __init__(
|
||||
self,
|
||||
grant_type: str = Form(None, regex="password"),
|
||||
username: str = Form(...),
|
||||
password: str = Form(...),
|
||||
scope: str = Form(""),
|
||||
client_id: Optional[str] = Form(None),
|
||||
client_secret: Optional[str] = Form(None),
|
||||
):
|
||||
self.grant_type = grant_type
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.scopes = scope.split()
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
|
||||
|
||||
class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm):
|
||||
# The OAuth2 spec says it MUST have the value "password"
|
||||
grant_type: str = Schema(..., regex="password")
|
||||
"""
|
||||
This is a dependency class, use it like:
|
||||
|
||||
@app.post("/login")
|
||||
def login(form_data: Oauth2PasswordRequestFormStrict = Depends()):
|
||||
data = form_data.parse()
|
||||
print(data.username)
|
||||
print(data.password)
|
||||
for scope in data.scopes:
|
||||
print(scope)
|
||||
if data.client_id:
|
||||
print(data.client_id)
|
||||
if data.client_secret:
|
||||
print(data.client_secret)
|
||||
return data
|
||||
|
||||
|
||||
It creates the following Form request parameters in your endpoint:
|
||||
|
||||
grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password".
|
||||
This dependency is strict about it. If you want to be permissive, use instead the
|
||||
OAuth2PasswordRequestFormStrict dependency class.
|
||||
username: username string. The OAuth2 spec requires the exact field name "username".
|
||||
password: password string. The OAuth2 spec requires the exact field name "password".
|
||||
scope: Optional string. Several scopes (each one a string) separated by spaces. E.g.
|
||||
"items:read items:write users:read profile openid"
|
||||
client_id: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
|
||||
using HTTP Basic auth, as: client_id:client_secret
|
||||
client_secret: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
|
||||
using HTTP Basic auth, as: client_id:client_secret
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
grant_type: str = Form(..., regex="password"),
|
||||
username: str = Form(...),
|
||||
password: str = Form(...),
|
||||
scope: str = Form(""),
|
||||
client_id: Optional[str] = Form(None),
|
||||
client_secret: Optional[str] = Form(None),
|
||||
):
|
||||
super().__init__(
|
||||
grant_type=grant_type,
|
||||
username=username,
|
||||
password=password,
|
||||
scope=scope,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
)
|
||||
|
||||
|
||||
class OAuth2(SecurityBase):
|
||||
|
||||
13
mkdocs.yml
@@ -28,6 +28,7 @@ nav:
|
||||
- Body - Multiple Parameters: 'tutorial/body-multiple-params.md'
|
||||
- Body - Schema: 'tutorial/body-schema.md'
|
||||
- Body - Nested Models: 'tutorial/body-nested-models.md'
|
||||
- Extra data types: 'tutorial/extra-data-types.md'
|
||||
- Cookie Parameters: 'tutorial/cookie-params.md'
|
||||
- Header Parameters: 'tutorial/header-params.md'
|
||||
- Response Model: 'tutorial/response-model.md'
|
||||
@@ -40,20 +41,24 @@ nav:
|
||||
- Custom Response: 'tutorial/custom-response.md'
|
||||
- Dependencies:
|
||||
- Dependencies Intro: 'tutorial/dependencies/intro.md'
|
||||
- First Steps: 'tutorial/dependencies/first-steps.md'
|
||||
- Second Steps: 'tutorial/dependencies/second-steps.md'
|
||||
- SQL (Relational) Databases: 'tutorial/sql-databases.md'
|
||||
- NoSQL (Distributed / Big Data) Databases: 'tutorial/nosql-databases.md'
|
||||
- First Steps - Functions: 'tutorial/dependencies/first-steps-functions.md'
|
||||
- Classes as Dependencies: 'tutorial/dependencies/classes-as-dependencies.md'
|
||||
- Sub-dependencies: 'tutorial/dependencies/sub-dependencies.md'
|
||||
- Advanced Dependencies: 'tutorial/dependencies/advanced-dependencies.md'
|
||||
- Security:
|
||||
- Security Intro: 'tutorial/security/intro.md'
|
||||
- First Steps: 'tutorial/security/first-steps.md'
|
||||
- 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'
|
||||
- SQL (Relational) Databases: 'tutorial/sql-databases.md'
|
||||
- NoSQL (Distributed / Big Data) Databases: 'tutorial/nosql-databases.md'
|
||||
- Bigger Applications - Multiple Files: 'tutorial/bigger-applications.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'
|
||||
|
||||
markdown_extensions:
|
||||
- markdown.extensions.codehilite:
|
||||
|
||||
118
pending_tests/main.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from fastapi import (
|
||||
Body,
|
||||
Cookie,
|
||||
Depends,
|
||||
FastAPI,
|
||||
File,
|
||||
Form,
|
||||
Header,
|
||||
Path,
|
||||
Query,
|
||||
Security,
|
||||
)
|
||||
from fastapi.security import (
|
||||
HTTPBasic,
|
||||
OAuth2,
|
||||
OAuth2PasswordBearer,
|
||||
OAuth2PasswordRequestForm,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
||||
from starlette.status import HTTP_202_ACCEPTED
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/security")
|
||||
def get_security(sec=Security(HTTPBasic())):
|
||||
return sec
|
||||
|
||||
|
||||
reusable_oauth2 = OAuth2(
|
||||
flows={
|
||||
"password": {
|
||||
"tokenUrl": "/token",
|
||||
"scopes": {"read:user": "Read a User", "write:user": "Create a user"},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/security/oauth2")
|
||||
def get_security_oauth2(sec=Security(reusable_oauth2, scopes=["read:user"])):
|
||||
return sec
|
||||
|
||||
|
||||
@app.post("/token")
|
||||
def post_token(request_data: OAuth2PasswordRequestForm = Form(...)):
|
||||
data = request_data.parse()
|
||||
access_token = data.username + ":" + data.password
|
||||
return {"access_token": access_token}
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
is_offer: bool
|
||||
|
||||
|
||||
class FakeDB:
|
||||
def __init__(self):
|
||||
self.data = {
|
||||
"johndoe": {
|
||||
"username": "johndoe",
|
||||
"password": "shouldbehashed",
|
||||
"fist_name": "John",
|
||||
"last_name": "Doe",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DBConnectionManager:
|
||||
def __init__(self):
|
||||
self.db = FakeDB()
|
||||
|
||||
def __call__(self):
|
||||
return self.db
|
||||
|
||||
|
||||
connection_manager = DBConnectionManager()
|
||||
|
||||
|
||||
class TokenUserData(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class UserInDB(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
fist_name: str
|
||||
last_name: str
|
||||
|
||||
|
||||
def require_token(
|
||||
token: str = Security(reusable_oauth2, scopes=["read:user", "write:user"])
|
||||
):
|
||||
raw_token = token.replace("Bearer ", "")
|
||||
# Never do this plaintext password usage in production
|
||||
username, password = raw_token.split(":")
|
||||
return TokenUserData(username=username, password=password)
|
||||
|
||||
|
||||
def require_user(
|
||||
db: FakeDB = Depends(connection_manager),
|
||||
user_data: TokenUserData = Depends(require_token),
|
||||
):
|
||||
return db.data[user_data.username]
|
||||
|
||||
|
||||
class UserOut(BaseModel):
|
||||
username: str
|
||||
fist_name: str
|
||||
last_name: str
|
||||
|
||||
|
||||
@app.get("/dependency", response_model=UserOut)
|
||||
def get_dependency(user: UserInDB = Depends(require_user)):
|
||||
return user
|
||||
@@ -35,7 +35,8 @@ test = [
|
||||
"mypy",
|
||||
"black",
|
||||
"isort",
|
||||
"requests"
|
||||
"requests",
|
||||
"email_validator"
|
||||
]
|
||||
doc = [
|
||||
"mkdocs",
|
||||
@@ -43,8 +44,8 @@ doc = [
|
||||
"markdown-include"
|
||||
]
|
||||
dev = [
|
||||
"prospector",
|
||||
"rope"
|
||||
"pyjwt",
|
||||
"passlib[bcrypt]"
|
||||
]
|
||||
all = [
|
||||
"requests",
|
||||
|
||||
6
scripts/test-cov-html.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
bash scripts/test.sh --cov-report=html
|
||||
@@ -7,8 +7,7 @@ export VERSION_SCRIPT="import sys; print('%s.%s' % sys.version_info[0:2])"
|
||||
export PYTHON_VERSION=`python -c "$VERSION_SCRIPT"`
|
||||
|
||||
export PYTHONPATH=./docs/src
|
||||
# PYTHONPATH=. pytest --cov=fastapi --cov=tests --cov-fail-under=100 --cov-report=term-missing ${@} --cov-report=html
|
||||
pytest --cov=fastapi --cov=tests --cov=docs/src --cov-report=term-missing ${@} --cov-report=html
|
||||
pytest --cov=fastapi --cov=tests --cov=docs/src --cov-report=term-missing ${@}
|
||||
mypy fastapi --disallow-untyped-defs
|
||||
if [ "${PYTHON_VERSION}" = '3.7' ]; then
|
||||
echo "Skipping 'black' on 3.7. See issue https://github.com/ambv/black/issues/494"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/dog")
|
||||
def get_a_dog():
|
||||
return "Woof"
|
||||
|
||||
|
||||
@router.get("/cat")
|
||||
def get_a_cat():
|
||||
return "Meow"
|
||||
@@ -1,13 +0,0 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/dog")
|
||||
def get_b_dog():
|
||||
return "B Woof"
|
||||
|
||||
|
||||
@router.get("/cat")
|
||||
def get_b_cat():
|
||||
return "B Meow"
|
||||
@@ -1,250 +0,0 @@
|
||||
from fastapi import (
|
||||
Body,
|
||||
Cookie,
|
||||
Depends,
|
||||
FastAPI,
|
||||
File,
|
||||
Form,
|
||||
Header,
|
||||
Path,
|
||||
Query,
|
||||
Security,
|
||||
)
|
||||
from fastapi.security import (
|
||||
HTTPBasic,
|
||||
OAuth2,
|
||||
OAuth2PasswordBearer,
|
||||
OAuth2PasswordRequestForm,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
||||
from starlette.status import HTTP_202_ACCEPTED
|
||||
|
||||
from .endpoints.a import router as router_a
|
||||
from .endpoints.b import router as router_b
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
app.include_router(router_a)
|
||||
app.include_router(router_b, prefix="/b")
|
||||
|
||||
|
||||
@app.get("/cookie")
|
||||
def get_cookie(coo=Cookie(None)):
|
||||
return coo
|
||||
|
||||
|
||||
@app.get("/header")
|
||||
def get_header(head_name=Header(None)):
|
||||
return head_name
|
||||
|
||||
|
||||
@app.get("/header_under")
|
||||
def get_header(head_name=Header(None, convert_underscores=False)):
|
||||
return head_name
|
||||
|
||||
|
||||
@app.get("/security")
|
||||
def get_security(sec=Security(HTTPBasic())):
|
||||
return sec
|
||||
|
||||
|
||||
reusable_oauth2 = OAuth2(
|
||||
flows={
|
||||
"password": {
|
||||
"tokenUrl": "/token",
|
||||
"scopes": {"read:user": "Read a User", "write:user": "Create a user"},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/security/oauth2")
|
||||
def get_security_oauth2(sec=Security(reusable_oauth2, scopes=["read:user"])):
|
||||
return sec
|
||||
|
||||
|
||||
@app.post("/token")
|
||||
def post_token(request_data: OAuth2PasswordRequestForm = Form(...)):
|
||||
data = request_data.parse()
|
||||
access_token = data.username + ":" + data.password
|
||||
return {"access_token": access_token}
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
is_offer: bool
|
||||
|
||||
|
||||
@app.put("/items/{item_id}")
|
||||
def put_item(item_id: str, item: Item):
|
||||
return item
|
||||
|
||||
|
||||
@app.post("/items/")
|
||||
def post_item(item: Item):
|
||||
return item
|
||||
|
||||
|
||||
@app.post("/items-all-params/{item_id}")
|
||||
def post_items_all_params(
|
||||
item_id: str = Path(...),
|
||||
body: Item = Body(...),
|
||||
query_a: int = Query(None),
|
||||
query_b=Query(None),
|
||||
coo: str = Cookie(None),
|
||||
x_head: int = Header(None),
|
||||
x_under: str = Header(None, convert_underscores=False),
|
||||
):
|
||||
return {
|
||||
"item_id": item_id,
|
||||
"body": body,
|
||||
"query_a": query_a,
|
||||
"query_b": query_b,
|
||||
"coo": coo,
|
||||
"x_head": x_head,
|
||||
"x_under": x_under,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/items-all-params-defaults/{item_id}")
|
||||
def post_items_all_params_default(
|
||||
item_id: str,
|
||||
body_item_a: Item,
|
||||
body_item_b: Item,
|
||||
query_a: int,
|
||||
query_b: int,
|
||||
coo: str = Cookie(None),
|
||||
x_head: int = Header(None),
|
||||
x_under: str = Header(None, convert_underscores=False),
|
||||
):
|
||||
return {
|
||||
"item_id": item_id,
|
||||
"body_item_a": body_item_a,
|
||||
"body_item_b": body_item_b,
|
||||
"query_a": query_a,
|
||||
"query_b": query_b,
|
||||
"coo": coo,
|
||||
"x_head": x_head,
|
||||
"x_under": x_under,
|
||||
}
|
||||
|
||||
|
||||
@app.delete("/items/{item_id}")
|
||||
def delete_item(item_id: str):
|
||||
return item_id
|
||||
|
||||
|
||||
@app.options("/options/")
|
||||
def options():
|
||||
return JSONResponse(headers={"x-fastapi": "fast"})
|
||||
|
||||
|
||||
@app.head("/head/")
|
||||
def head():
|
||||
return {"not sent": "nope"}
|
||||
|
||||
|
||||
@app.patch("/patch/{user_id}")
|
||||
def patch(user_id: str, increment: float):
|
||||
return {"user_id": user_id, "total": 5 + increment}
|
||||
|
||||
|
||||
@app.trace("/trace/")
|
||||
def trace():
|
||||
return PlainTextResponse(media_type="message/http")
|
||||
|
||||
|
||||
@app.get("/model", response_model=Item, status_code=HTTP_202_ACCEPTED)
|
||||
def model():
|
||||
return {"name": "Foo", "price": "5.0", "password": "not sent"}
|
||||
|
||||
|
||||
@app.get(
|
||||
"/metadata",
|
||||
tags=["tag1", "tag2"],
|
||||
summary="The summary",
|
||||
description="The description",
|
||||
response_description="Response description",
|
||||
deprecated=True,
|
||||
operation_id="a_very_long_and_strange_operation_id",
|
||||
)
|
||||
def get_meta():
|
||||
return "Foo"
|
||||
|
||||
|
||||
@app.get("/html", content_type=HTMLResponse)
|
||||
def get_html():
|
||||
return """
|
||||
<html>
|
||||
<body>
|
||||
<h1>
|
||||
Some text inside
|
||||
</h1>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
class FakeDB:
|
||||
def __init__(self):
|
||||
self.data = {
|
||||
"johndoe": {
|
||||
"username": "johndoe",
|
||||
"password": "shouldbehashed",
|
||||
"fist_name": "John",
|
||||
"last_name": "Doe",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DBConnectionManager:
|
||||
def __init__(self):
|
||||
self.db = FakeDB()
|
||||
|
||||
def __call__(self):
|
||||
return self.db
|
||||
|
||||
|
||||
connection_manager = DBConnectionManager()
|
||||
|
||||
|
||||
class TokenUserData(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class UserInDB(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
fist_name: str
|
||||
last_name: str
|
||||
|
||||
|
||||
def require_token(
|
||||
token: str = Security(reusable_oauth2, scopes=["read:user", "write:user"])
|
||||
):
|
||||
raw_token = token.replace("Bearer ", "")
|
||||
# Never do this plaintext password usage in production
|
||||
username, password = raw_token.split(":")
|
||||
return TokenUserData(username=username, password=password)
|
||||
|
||||
|
||||
def require_user(
|
||||
db: FakeDB = Depends(connection_manager),
|
||||
user_data: TokenUserData = Depends(require_token),
|
||||
):
|
||||
return db.data[user_data.username]
|
||||
|
||||
|
||||
class UserOut(BaseModel):
|
||||
username: str
|
||||
fist_name: str
|
||||
last_name: str
|
||||
|
||||
|
||||
@app.get("/dependency", response_model=UserOut)
|
||||
def get_dependency(user: UserInDB = Depends(require_user)):
|
||||
return user
|
||||
@@ -1147,10 +1147,12 @@ def test_get_path(path, expected_status, expected_response):
|
||||
def test_swagger_ui():
|
||||
response = client.get("/docs")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"] == "text/html; charset=utf-8"
|
||||
assert "swagger-ui-dist" in response.text
|
||||
|
||||
|
||||
def test_redoc():
|
||||
response = client.get("/redoc")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"] == "text/html; charset=utf-8"
|
||||
assert "redoc@next" in response.text
|
||||
|
||||
360
tests/test_extra_routes.py
Normal file
@@ -0,0 +1,360 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float = None
|
||||
|
||||
|
||||
@app.api_route("/items/{item_id}", methods=["GET"])
|
||||
def get_items(item_id: str):
|
||||
return {"item_id": item_id}
|
||||
|
||||
|
||||
def get_not_decorated(item_id: str):
|
||||
return {"item_id": item_id}
|
||||
|
||||
|
||||
app.add_api_route("/items-not-decorated/{item_id}", get_not_decorated)
|
||||
|
||||
|
||||
@app.delete("/items/{item_id}")
|
||||
def delete_item(item_id: str, item: Item):
|
||||
return {"item_id": item_id, "item": item}
|
||||
|
||||
|
||||
@app.head("/items/{item_id}")
|
||||
def head_item(item_id: str):
|
||||
return JSONResponse(headers={"x-fastapi-item-id": item_id})
|
||||
|
||||
|
||||
@app.options("/items/{item_id}")
|
||||
def options_item(item_id: str):
|
||||
return JSONResponse(headers={"x-fastapi-item-id": item_id})
|
||||
|
||||
|
||||
@app.patch("/items/{item_id}")
|
||||
def patch_item(item_id: str, item: Item):
|
||||
return {"item_id": item_id, "item": item}
|
||||
|
||||
|
||||
@app.trace("/items/{item_id}")
|
||||
def trace_item(item_id: str):
|
||||
return JSONResponse(media_type="message/http")
|
||||
|
||||
|
||||
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": "Get Items Get",
|
||||
"operationId": "get_items_items__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
},
|
||||
"delete": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Delete Item Delete",
|
||||
"operationId": "delete_item_items__item_id__delete",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
"options": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Options Item Options",
|
||||
"operationId": "options_item_items__item_id__options",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
},
|
||||
"head": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Head Item Head",
|
||||
"operationId": "head_item_items__item_id__head",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
},
|
||||
"patch": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Patch Item Patch",
|
||||
"operationId": "patch_item_items__item_id__patch",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
"trace": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Trace Item Trace",
|
||||
"operationId": "trace_item_items__item_id__trace",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
"/items-not-decorated/{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": "Get Not Decorated Get",
|
||||
"operationId": "get_not_decorated_items-not-decorated__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Item": {
|
||||
"title": "Item",
|
||||
"required": ["name"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
},
|
||||
},
|
||||
"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_api_route():
|
||||
response = client.get("/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "foo"}
|
||||
|
||||
|
||||
def test_get_api_route_not_decorated():
|
||||
response = client.get("/items-not-decorated/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "foo"}
|
||||
|
||||
|
||||
def test_delete():
|
||||
response = client.delete("/items/foo", json={"name": "Foo"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "foo", "item": {"name": "Foo", "price": None}}
|
||||
|
||||
|
||||
def test_head():
|
||||
response = client.head("/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["x-fastapi-item-id"] == "foo"
|
||||
|
||||
|
||||
def test_options():
|
||||
response = client.head("/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["x-fastapi-item-id"] == "foo"
|
||||
|
||||
|
||||
def test_patch():
|
||||
response = client.patch("/items/foo", json={"name": "Foo"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "foo", "item": {"name": "Foo", "price": None}}
|
||||
|
||||
|
||||
def test_trace():
|
||||
response = client.request("trace", "/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"] == "message/http"
|
||||
50
tests/test_jsonable_encoder.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import pytest
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
|
||||
class Person:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
|
||||
class Pet:
|
||||
def __init__(self, owner: Person, name: str):
|
||||
self.owner = owner
|
||||
self.name = name
|
||||
|
||||
|
||||
class DictablePerson(Person):
|
||||
def __iter__(self):
|
||||
return ((k, v) for k, v in self.__dict__.items())
|
||||
|
||||
|
||||
class DictablePet(Pet):
|
||||
def __iter__(self):
|
||||
return ((k, v) for k, v in self.__dict__.items())
|
||||
|
||||
|
||||
class Unserializable:
|
||||
def __iter__(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def __dict__(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def test_encode_class():
|
||||
person = Person(name="Foo")
|
||||
pet = Pet(owner=person, name="Firulais")
|
||||
assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}}
|
||||
|
||||
|
||||
def test_encode_dictable():
|
||||
person = DictablePerson(name="Foo")
|
||||
pet = DictablePet(owner=person, name="Firulais")
|
||||
assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}}
|
||||
|
||||
|
||||
def test_encode_unsupported():
|
||||
unserializable = Unserializable()
|
||||
with pytest.raises(ValueError):
|
||||
jsonable_encoder(unserializable)
|
||||
51
tests/test_serialize_response.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float = None
|
||||
owner_ids: List[int] = None
|
||||
|
||||
|
||||
@app.get("/items/invalid", response_model=Item)
|
||||
def get_invalid():
|
||||
return {"name": "invalid", "price": "foo"}
|
||||
|
||||
|
||||
@app.get("/items/innerinvalid", response_model=Item)
|
||||
def get_innerinvalid():
|
||||
return {"name": "double invalid", "price": "foo", "owner_ids": ["foo", "bar"]}
|
||||
|
||||
|
||||
@app.get("/items/invalidlist", response_model=List[Item])
|
||||
def get_invalidlist():
|
||||
return [
|
||||
{"name": "foo"},
|
||||
{"name": "bar", "price": "bar"},
|
||||
{"name": "baz", "price": "baz"},
|
||||
]
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_invalid():
|
||||
with pytest.raises(ValidationError):
|
||||
client.get("/items/invalid")
|
||||
|
||||
|
||||
def test_double_invalid():
|
||||
with pytest.raises(ValidationError):
|
||||
client.get("/items/innerinvalid")
|
||||
|
||||
|
||||
def test_invalid_list():
|
||||
with pytest.raises(ValidationError):
|
||||
client.get("/items/invalidlist")
|
||||
@@ -0,0 +1,34 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from application_configuration.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {
|
||||
"title": "My Super Project",
|
||||
"version": "2.5.0",
|
||||
"description": "This is a very fancy project, with auto docs for the API and everything",
|
||||
},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Items Get",
|
||||
"operationId": "read_items_items__get",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_scheme():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
@@ -172,3 +172,9 @@ def test_post_body(path, body, expected_status, expected_response):
|
||||
response = client.post(path, json=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
|
||||
|
||||
def test_post_broken_body():
|
||||
response = client.post("/items/", data={"name": "Foo", "price": 50.5})
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "There was an error parsing the body"}
|
||||
|
||||
@@ -143,6 +143,5 @@ item_id_not_int = {
|
||||
)
|
||||
def test_post_body(path, body, expected_status, expected_response):
|
||||
response = client.put(path, json=body)
|
||||
print(response.text)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
@@ -8,8 +6,6 @@ from body_schema.tutorial001 import app
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
print(sys.path)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
@@ -111,8 +107,8 @@ openapi_schema = {
|
||||
}
|
||||
|
||||
|
||||
def openapi_schema():
|
||||
response = client.put("/openapi.json")
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
36
tests/test_tutorial/test_custom_response/test_tutorial001.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from custom_response.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Items Get",
|
||||
"operationId": "read_items_items__get",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_scheme():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_get_custom_response():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"item_id": "Foo"}]
|
||||
47
tests/test_tutorial/test_custom_response/test_tutorial004.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from custom_response.tutorial004 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"text/html": {"schema": {"type": "string"}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Items Get",
|
||||
"operationId": "read_items_items__get",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
html_contents = """
|
||||
<html>
|
||||
<head>
|
||||
<title>Some HTML in here</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Look ma! HTML!</h1>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def test_openapi_scheme():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_get_custom_response():
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.text == html_contents
|
||||
@@ -84,6 +84,12 @@ openapi_schema = {
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_status,expected_response",
|
||||
[
|
||||
|
||||
136
tests/test_tutorial/test_extra_data_types/test_tutorial001.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from extra_data_types.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/{item_id}": {
|
||||
"put": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read Items Put",
|
||||
"operationId": "read_items_items__item_id__put",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "Item_Id",
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Body_read_items"}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_read_items": {
|
||||
"title": "Body_read_items",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start_datetime": {
|
||||
"title": "Start_Datetime",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
},
|
||||
"end_datetime": {
|
||||
"title": "End_Datetime",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
},
|
||||
"repeat_at": {
|
||||
"title": "Repeat_At",
|
||||
"type": "string",
|
||||
"format": "time",
|
||||
},
|
||||
"process_after": {
|
||||
"title": "Process_After",
|
||||
"type": "string",
|
||||
"format": "time-delta",
|
||||
},
|
||||
},
|
||||
},
|
||||
"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_extra_types():
|
||||
item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e"
|
||||
data = {
|
||||
"start_datetime": "2018-12-22T14:00:00+00:00",
|
||||
"end_datetime": "2018-12-24T15:00:00+00:00",
|
||||
"repeat_at": "15:30:00",
|
||||
"process_after": 300,
|
||||
}
|
||||
expected_response = data.copy()
|
||||
expected_response.update(
|
||||
{
|
||||
"start_process": "2018-12-22T14:05:00+00:00",
|
||||
"duration": 176_100,
|
||||
"item_id": item_id,
|
||||
}
|
||||
)
|
||||
response = client.put(f"/items/{item_id}", json=data)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == expected_response
|
||||
@@ -1,5 +1,3 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
@@ -8,8 +6,6 @@ from header_params.tutorial001 import app
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
print(sys.path)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from query_params_str_validations.tutorial010 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read Items Get",
|
||||
"operationId": "read_items_items__get",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Query string for the items to search in the database that have a good match",
|
||||
"required": False,
|
||||
"deprecated": True,
|
||||
"schema": {
|
||||
"title": "Query string",
|
||||
"maxLength": 50,
|
||||
"minLength": 3,
|
||||
"pattern": "^fixedquery$",
|
||||
"type": "string",
|
||||
"description": "Query string for the items to search in the database that have a good match",
|
||||
},
|
||||
"name": "item-query",
|
||||
"in": "query",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"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_scheme():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
0
tests/test_tutorial/test_request_files/__init__.py
Normal file
121
tests/test_tutorial/test_request_files/test_tutorial001.py
Normal file
@@ -0,0 +1,121 @@
|
||||
import os
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from request_files.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/files/": {
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Create File Post",
|
||||
"operationId": "create_file_files__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {"$ref": "#/components/schemas/Body_create_file"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_create_file": {
|
||||
"title": "Body_create_file",
|
||||
"required": ["file"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {"title": "File", "type": "string", "format": "binary"}
|
||||
},
|
||||
},
|
||||
"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_scheme():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
file_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "file"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_post_form_no_body():
|
||||
response = client.post("/files/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == file_required
|
||||
|
||||
|
||||
def test_post_body_json():
|
||||
response = client.post("/files/", json={"file": "Foo"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == file_required
|
||||
|
||||
|
||||
def test_post_file(tmpdir):
|
||||
path = os.path.join(tmpdir, "test.txt")
|
||||
with open(path, "wb") as file:
|
||||
file.write(b"<file content>")
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.post("/files/", files={"file": open(path, "rb")})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": 14}
|
||||
0
tests/test_tutorial/test_request_forms/__init__.py
Normal file
147
tests/test_tutorial/test_request_forms/test_tutorial001.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from request_forms.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/login/": {
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Login Post",
|
||||
"operationId": "login_login__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {"$ref": "#/components/schemas/Body_login"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_login": {
|
||||
"title": "Body_login",
|
||||
"required": ["username", "password"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {"title": "Username", "type": "string"},
|
||||
"password": {"title": "Password", "type": "string"},
|
||||
},
|
||||
},
|
||||
"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_scheme():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
password_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
username_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
username_and_password_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "username"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "password"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,body,expected_status,expected_response",
|
||||
[
|
||||
(
|
||||
"/login/",
|
||||
{"username": "Foo", "password": "secret"},
|
||||
200,
|
||||
{"username": "Foo"},
|
||||
),
|
||||
("/login/", {"username": "Foo"}, 422, password_required),
|
||||
("/login/", {"password": "secret"}, 422, username_required),
|
||||
("/login/", None, 422, username_and_password_required),
|
||||
],
|
||||
)
|
||||
def test_post_body_form(path, body, expected_status, expected_response):
|
||||
response = client.post(path, data=body)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
|
||||
|
||||
def test_post_body_json():
|
||||
response = client.post("/login/", json={"username": "Foo", "password": "secret"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == username_and_password_required
|
||||
@@ -0,0 +1,166 @@
|
||||
import os
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from request_forms_and_files.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/files/": {
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Create File Post",
|
||||
"operationId": "create_file_files__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {"$ref": "#/components/schemas/Body_create_file"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Body_create_file": {
|
||||
"title": "Body_create_file",
|
||||
"required": ["file", "token"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {"title": "File", "type": "string", "format": "binary"},
|
||||
"token": {"title": "Token", "type": "string"},
|
||||
},
|
||||
},
|
||||
"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_scheme():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
file_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "file"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
token_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
file_and_token_required = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "file"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "token"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_post_form_no_body():
|
||||
response = client.post("/files/")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == file_and_token_required
|
||||
|
||||
|
||||
def test_post_form_no_file():
|
||||
response = client.post("/files/", data={"token": "foo"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == file_required
|
||||
|
||||
|
||||
def test_post_body_json():
|
||||
response = client.post("/files/", json={"file": "Foo", "token": "Bar"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == file_and_token_required
|
||||
|
||||
|
||||
def test_post_file_no_token(tmpdir):
|
||||
path = os.path.join(tmpdir, "test.txt")
|
||||
with open(path, "wb") as file:
|
||||
file.write(b"<file content>")
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.post("/files/", files={"file": open(path, "rb")})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == token_required
|
||||
|
||||
|
||||
def test_post_file_and_token(tmpdir):
|
||||
path = os.path.join(tmpdir, "test.txt")
|
||||
with open(path, "wb") as file:
|
||||
file.write(b"<file content>")
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
"/files/", data={"token": "foo"}, files={"file": open(path, "rb")}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": 14, "token": "foo"}
|
||||
0
tests/test_tutorial/test_response_model/__init__.py
Normal file
120
tests/test_tutorial/test_response_model/test_tutorial003.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from response_model.tutorial003 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/user/": {
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/UserOut"}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Create User Post",
|
||||
"operationId": "create_user_user__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/UserIn"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"UserOut": {
|
||||
"title": "UserOut",
|
||||
"required": ["username", "email"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {"title": "Username", "type": "string"},
|
||||
"email": {"title": "Email", "type": "string", "format": "email"},
|
||||
"full_name": {"title": "Full_Name", "type": "string"},
|
||||
},
|
||||
},
|
||||
"UserIn": {
|
||||
"title": "UserIn",
|
||||
"required": ["username", "password", "email"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {"title": "Username", "type": "string"},
|
||||
"password": {"title": "Password", "type": "string"},
|
||||
"email": {"title": "Email", "type": "string", "format": "email"},
|
||||
"full_name": {"title": "Full_Name", "type": "string"},
|
||||
},
|
||||
},
|
||||
"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_scheme():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_post_user():
|
||||
response = client.post(
|
||||
"/user/",
|
||||
json={
|
||||
"username": "foo",
|
||||
"password": "fighter",
|
||||
"email": "foo@example.com",
|
||||
"full_name": "Grave Dohl",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"username": "foo",
|
||||
"email": "foo@example.com",
|
||||
"full_name": "Grave Dohl",
|
||||
}
|
||||