mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-25 15:18:36 -05:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e3a7699a3 | ||
|
|
8998ccaffb | ||
|
|
2b7f201a44 | ||
|
|
192ebba2a2 | ||
|
|
8880c4cb03 | ||
|
|
6324be684f | ||
|
|
c705685394 | ||
|
|
945f401d8e | ||
|
|
f216d340ec | ||
|
|
a4558e7053 | ||
|
|
298f8478e2 | ||
|
|
b86d163470 | ||
|
|
9e2d37b89c | ||
|
|
97adadd9e1 | ||
|
|
26e3dffb37 | ||
|
|
aa7b4bd101 | ||
|
|
ffc4c716c0 | ||
|
|
ef7b6e8eaf | ||
|
|
596243f4a5 | ||
|
|
766bf1c5aa | ||
|
|
9e748dbca4 |
@@ -20,6 +20,7 @@ after_script:
|
||||
|
||||
deploy:
|
||||
provider: script
|
||||
script: bash scripts/trigger-docker.sh
|
||||
script: bash scripts/deploy.sh
|
||||
on:
|
||||
branch: master
|
||||
tags: true
|
||||
python: "3.6"
|
||||
|
||||
87
Pipfile.lock
generated
87
Pipfile.lock
generated
@@ -41,10 +41,10 @@
|
||||
"sqlite"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:da819f7e00dc7d8c2f0585ec53aa49bae63b366f800506097db2e87972a4d44f"
|
||||
"sha256:d365cff2035c5177ef5fd8c5abf6671da01189521da64848a01251c870daf48f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.2.1"
|
||||
"version": "==0.2.2"
|
||||
},
|
||||
"dataclasses": {
|
||||
"hashes": [
|
||||
@@ -220,10 +220,10 @@
|
||||
},
|
||||
"defusedxml": {
|
||||
"hashes": [
|
||||
"sha256:06d4515a8f8965624d6db922093eb11e77fb8f9a9ebedd1c5d6df5a0fcd0a12c",
|
||||
"sha256:6c0b1461695877ececd6921a6a330e4392790275c5d6e88fc8ea8261445468b1"
|
||||
"sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93",
|
||||
"sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"
|
||||
],
|
||||
"version": "==0.6.0rc1"
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"dnspython": {
|
||||
"hashes": [
|
||||
@@ -281,6 +281,7 @@
|
||||
"hashes": [
|
||||
"sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc"
|
||||
],
|
||||
"markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'pypy'",
|
||||
"version": "==0.0.13"
|
||||
},
|
||||
"idna": {
|
||||
@@ -472,20 +473,20 @@
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:03261a04ace27250cf14f1301969e2cc36ad0343dd437e60007ce42f06ddbaff",
|
||||
"sha256:6a7923e90dd8f8b8e762327e3a4dd814f0bc5581a627010f4e2ec72d906ada0f",
|
||||
"sha256:6a7c2b16ff7dee1cd4a913641d6a8da0cd386be812524f41427ea25f8fe337a6",
|
||||
"sha256:7480db0bc2bb473547c8d519ea549de9f9654170e6f5b34310094ebe5ee1c9dc",
|
||||
"sha256:863774c896f2cdc62a0e2252e9ba7aaeb7da04c0296f47c82b125dce3437c580",
|
||||
"sha256:9a990cf039891a83ee90f130256cc06d09c0793242ea38d0fe33fdc449507123",
|
||||
"sha256:b03573d0cd8c051aa9ef7f47d564cf44bbc5e91e89a7a078b3ca904b3da8855a",
|
||||
"sha256:b10b16d9aa7a01266f14260344fb25849ef0d508c512a916043f77987489aeff",
|
||||
"sha256:b1eab82221c3cc94bf22152e701b3efc9d64f60fac4cab20969a0427e5a78261",
|
||||
"sha256:e663d4424531dc99fb85c947df8a4a107442f53f20a4e0bcefaa1d21c87e1563",
|
||||
"sha256:ffac30f3fa2c9e10118cbb0faa0b7da7edb6e3c24a4048a15446a1f3409884e3"
|
||||
"sha256:2afe51527b1f6cdc4a5f34fc90473109b22bf7f21086ba3e9451857cf11489e6",
|
||||
"sha256:56a16df3e0abb145d8accd5dbb70eba6c4bd26e2f89042b491faa78c9635d1e2",
|
||||
"sha256:5764f10d27b2e93c84f70af5778941b8f4aa1379b2430f85c827e0f5464e8714",
|
||||
"sha256:5bbc86374f04a3aa817622f98e40375ccb28c4836f36b66706cf3c6ccce86eda",
|
||||
"sha256:6a9343089f6377e71e20ca734cd8e7ac25d36478a9df580efabfe9059819bf82",
|
||||
"sha256:6c9851bc4a23dc1d854d3f5dfd5f20a016f8da86bcdbb42687879bb5f86434b0",
|
||||
"sha256:b8e85956af3fcf043d6f87c91cbe8705073fc67029ba6e22d3468bfee42c4823",
|
||||
"sha256:b9a0af8fae490306bc112229000aa0c2ccc837b49d29a5c42e088c132a2334dd",
|
||||
"sha256:bbf643528e2a55df2c1587008d6e3bda5c0445f1240dfa85129af22ae16d7a9a",
|
||||
"sha256:c46ab3438bd21511db0f2c612d89d8344154c0c9494afc7fbc932de514cf8d15",
|
||||
"sha256:f7a83d6bd805855ef83ec605eb01ab4fa42bcef254b13631e451cbb44914a9b0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.700"
|
||||
"version": "==0.701"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
@@ -772,27 +773,28 @@
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:035a54ede6ce1380599b2ce57844c6554666522e376bd111eb940fbc7c3dad23",
|
||||
"sha256:037c35f2741ce3a9ac0d55abfcd119133cbd821fffa4461397718287092d9d15",
|
||||
"sha256:049feae7e9f180b64efacbdc36b3af64a00393a47be22fa9cb6794e68d4e73d3",
|
||||
"sha256:19228f7940beafc1ba21a6e8e070e0b0bfd1457902a3a81709762b8b9039b88d",
|
||||
"sha256:2ea681e91e3550a30c2265d2916f40a5f5d89b59469a20f3bad7d07adee0f7a6",
|
||||
"sha256:3a6b0a78af298d82323660df5497bcea0f0a4a25a0b003afd0ce5af049bd1f60",
|
||||
"sha256:5385da8f3b801014504df0852bf83524599df890387a3c2b17b7caa3d78b1773",
|
||||
"sha256:606d8afa07eef77280c2bf84335e24390055b478392e1975f96286d99d0cb424",
|
||||
"sha256:69245b5b23bbf7fb242c9f8f08493e9ecd7711f063259aefffaeb90595d62287",
|
||||
"sha256:6f6d839ab09830d59b7fa8fb6917023d8cb5498ee1f1dbd82d37db78eb76bc99",
|
||||
"sha256:730888475f5ac0e37c1de4bd05eeb799fdb742697867f524dc8a4cd74bcecc23",
|
||||
"sha256:9819b5162ffc121b9e334923c685b0d0826154e41dfe70b2ede2ce29034c71d8",
|
||||
"sha256:9e60ef9426efab601dd9aa120e4ff560f4461cf8442e9c0a2b92548d52800699",
|
||||
"sha256:af5fbdde0690c7da68e841d7fc2632345d570768ea7406a9434446d7b33b0ee1",
|
||||
"sha256:b64efdbdf3bbb1377562c179f167f3bf301251411eb5ac77dec6b7d32bcda463",
|
||||
"sha256:bac5f444c118aeb456fac1b0b5d14c6a71ea2a42069b09c176f75e9bd4c186f6",
|
||||
"sha256:bda9068aafb73859491e13b99b682bd299c1b5fd50644d697533775828a28ee0",
|
||||
"sha256:d659517ca116e6750101a1326107d3479028c5191f0ecee3c7203c50f5b915b0",
|
||||
"sha256:eddd3fb1f3e0f82e5915a899285a39ee34ce18fd25d89582bc89fc9fb16cd2c6"
|
||||
"sha256:04894d268ba6eab7e093d43107869ad49e7b5ef40d1a94243ea49b352061b200",
|
||||
"sha256:16616ece19daddc586e499a3d2f560302c11f122b9c692bc216e821ae32aa0d0",
|
||||
"sha256:252fdae740964b2d3cdfb3f84dcb4d6247a48a6abe2579e8029ab3be3cdc026c",
|
||||
"sha256:2af80a373af123d0b9f44941a46df67ef0ff7a60f95872412a145f4500a7fc99",
|
||||
"sha256:2c88d0a913229a06282b285f42a31e063c3bf9071ff65c5ea4c12acb6977c6a7",
|
||||
"sha256:2ea99c029ebd4b5a308d915cc7fb95b8e1201d60b065450d5d26deb65d3f2bc1",
|
||||
"sha256:3d2e3ab175fc097d2a51c7a0d3fda442f35ebcc93bb1d7bd9b95ad893e44c04d",
|
||||
"sha256:4766dd695548a15ee766927bf883fb90c6ac8321be5a60c141f18628fb7f8da8",
|
||||
"sha256:56b6978798502ef66625a2e0f80cf923da64e328da8bbe16c1ff928c70c873de",
|
||||
"sha256:5cddb6f8bce14325b2863f9d5ac5c51e07b71b462361fd815d1d7706d3a9d682",
|
||||
"sha256:644ee788222d81555af543b70a1098f2025db38eaa99226f3a75a6854924d4db",
|
||||
"sha256:64cf762049fc4775efe6b27161467e76d0ba145862802a65eefc8879086fc6f8",
|
||||
"sha256:68c362848d9fb71d3c3e5f43c09974a0ae319144634e7a47db62f0f2a54a7fa7",
|
||||
"sha256:6c1f3c6f6635e611d58e467bf4371883568f0de9ccc4606f17048142dec14a1f",
|
||||
"sha256:b213d4a02eec4ddf622f4d2fbc539f062af3788d1f332f028a2e19c42da53f15",
|
||||
"sha256:bb27d4e7805a7de0e35bd0cb1411bc85f807968b2b0539597a49a23b00a622ae",
|
||||
"sha256:c9d414512eaa417aadae7758bc118868cd2396b0e6138c1dd4fda96679c079d3",
|
||||
"sha256:f0937165d1e25477b01081c4763d2d9cdc3b18af69cb259dd4f640c9b900fe5e",
|
||||
"sha256:fb96a6e2c11059ecf84e6741a319f93f683e440e341d4489c9b161eca251cf2a",
|
||||
"sha256:fc71d2d6ae56a091a8d94f33ec9d0f2001d1cb1db423d8b4355debfe9ce689b7"
|
||||
],
|
||||
"version": "==1.3.1"
|
||||
"version": "==1.3.4"
|
||||
},
|
||||
"ujson": {
|
||||
"hashes": [
|
||||
@@ -803,17 +805,17 @@
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
|
||||
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
|
||||
"sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0",
|
||||
"sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3"
|
||||
],
|
||||
"version": "==1.24.1"
|
||||
"version": "==1.24.2"
|
||||
},
|
||||
"uvicorn": {
|
||||
"hashes": [
|
||||
"sha256:d96fb442d9ce9c1dba67360035161d392970b8e6b0ed797d2cefed24abfd78bc"
|
||||
"sha256:181d47abddedd0f6e23eaeed97976bdce9ea1dbff0ec12385309cf4835783f6a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.7.0b2"
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"uvloop": {
|
||||
"hashes": [
|
||||
@@ -828,6 +830,7 @@
|
||||
"sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f",
|
||||
"sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe"
|
||||
],
|
||||
"markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'pypy'",
|
||||
"version": "==0.12.2"
|
||||
},
|
||||
"wcwidth": {
|
||||
|
||||
BIN
docs/img/tutorial/security/image12.png
Normal file
BIN
docs/img/tutorial/security/image12.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -1,5 +1,33 @@
|
||||
## Next release
|
||||
|
||||
## 0.19.0
|
||||
|
||||
* Rename *path operation decorator* parameter `content_type` to `response_class`. PR <a href="https://github.com/tiangolo/fastapi/pull/183" target="_blank">#183</a>.
|
||||
|
||||
* Add docs:
|
||||
* How to use the `jsonable_encoder` in <a href="https://fastapi.tiangolo.com/tutorial/tutorial/encoder/" target="_blank">JSON compatible encoder</a>.
|
||||
* How to <a href="https://fastapi.tiangolo.com/tutorial/tutorial/response-directly/" target="_blank">Return a Response directly</a>.
|
||||
* Update how to use a <a href="https://fastapi.tiangolo.com/tutorial/tutorial/custom-response/" target="_blank">Custom Response Class</a>.
|
||||
* PR <a href="https://github.com/tiangolo/fastapi/pull/184" target="_blank">#184</a>.
|
||||
|
||||
## 0.18.0
|
||||
|
||||
* Add docs for <a href="https://fastapi.tiangolo.com/tutorial/custom-response/" target="_blank">HTTP Basic Auth</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/177" target="_blank">#177</a>.
|
||||
|
||||
* Upgrade HTTP Basic Auth handling with automatic headers (automatic browser login prompt). PR <a href="https://github.com/tiangolo/fastapi/pull/175" target="_blank">#175</a>.
|
||||
|
||||
* Update dependencies for security. PR <a href="https://github.com/tiangolo/fastapi/pull/174" target="_blank">#174</a>.
|
||||
|
||||
* Add docs for <a href="https://fastapi.tiangolo.com/tutorial/middleware/" target="_blank">Middleware</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/173" target="_blank">#173</a>.
|
||||
|
||||
## 0.17.0
|
||||
|
||||
* Make Flit publish from CI. PR <a href="https://github.com/tiangolo/fastapi/pull/170" target="_blank">#170</a>.
|
||||
|
||||
* Add documentation about handling <a href="https://fastapi.tiangolo.com/tutorial/cors/" target="_blank">CORS (Cross-Origin Resource Sharing)</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/169" target="_blank">#169</a>.
|
||||
|
||||
* By default, encode by alias. This allows using Pydantic `alias` parameters working by default. PR <a href="https://github.com/tiangolo/fastapi/pull/168" target="_blank">#168</a>.
|
||||
|
||||
## 0.16.0
|
||||
|
||||
* Upgrade *path operation* `doctsring` parsing to support proper Markdown descriptions. New documentation at <a href="https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#description-from-docstring" target="_blank">Path Operation Configuration</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/163" target="_blank">#163</a>.
|
||||
|
||||
19
docs/src/cors/tutorial001.py
Normal file
19
docs/src/cors/tutorial001.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
origins = [
|
||||
"http://localhost.tiangolo.com",
|
||||
"https://localhost.tiangolo.com",
|
||||
"http:localhost",
|
||||
"http:localhost:8080",
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
@@ -4,6 +4,6 @@ from starlette.responses import UJSONResponse
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/", content_type=UJSONResponse)
|
||||
@app.get("/items/", response_class=UJSONResponse)
|
||||
async def read_items():
|
||||
return [{"item_id": "Foo"}]
|
||||
|
||||
@@ -4,7 +4,7 @@ from starlette.responses import HTMLResponse
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/", content_type=HTMLResponse)
|
||||
@app.get("/items/", response_class=HTMLResponse)
|
||||
async def read_items():
|
||||
return """
|
||||
<html>
|
||||
|
||||
@@ -18,6 +18,6 @@ def generate_html_response():
|
||||
return HTMLResponse(content=html_content, status_code=200)
|
||||
|
||||
|
||||
@app.get("/items/", content_type=HTMLResponse)
|
||||
@app.get("/items/", response_class=HTMLResponse)
|
||||
async def read_items():
|
||||
return generate_html_response()
|
||||
|
||||
22
docs/src/encoder/tutorial001.py
Normal file
22
docs/src/encoder/tutorial001.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel
|
||||
|
||||
fake_db = {}
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
title: str
|
||||
timestamp: datetime
|
||||
description: str = None
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.put("/items/{id}")
|
||||
def update_item(id: str, item: Item):
|
||||
json_compatible_item_data = jsonable_encoder(item)
|
||||
fake_db[id] = json_compatible_item_data
|
||||
15
docs/src/middleware/tutorial001.py
Normal file
15
docs/src/middleware/tutorial001.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import time
|
||||
|
||||
from fastapi import FastAPI
|
||||
from starlette.requests import Request
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def add_process_time_header(request: Request, call_next):
|
||||
start_time = time.time()
|
||||
response = await call_next(request)
|
||||
process_time = time.time() - start_time
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
return response
|
||||
21
docs/src/response_directly/tutorial001.py
Normal file
21
docs/src/response_directly/tutorial001.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
title: str
|
||||
timestamp: datetime
|
||||
description: str = None
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.put("/items/{id}")
|
||||
def update_item(id: str, item: Item):
|
||||
json_compatible_item_data = jsonable_encoder(item)
|
||||
return JSONResponse(content=json_compatible_item_data)
|
||||
20
docs/src/response_directly/tutorial002.py
Normal file
20
docs/src/response_directly/tutorial002.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.responses import Response
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/legacy/")
|
||||
def get_legacy_data():
|
||||
data = """
|
||||
<?xml version="1.0"?>
|
||||
<shampoo>
|
||||
<Header>
|
||||
Apply shampoo here.
|
||||
<Header>
|
||||
<Body>
|
||||
You'll have to use soap here.
|
||||
</Body>
|
||||
</shampoo>
|
||||
"""
|
||||
return Response(content=data, media_type="application/xml")
|
||||
11
docs/src/security/tutorial006.py
Normal file
11
docs/src/security/tutorial006.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPBasic()
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(credentials: HTTPBasicCredentials = Depends(security)):
|
||||
return {"username": credentials.username, "password": credentials.password}
|
||||
22
docs/src/security/tutorial007.py
Normal file
22
docs/src/security/tutorial007.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from fastapi import Depends, FastAPI, HTTPException
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPBasic()
|
||||
|
||||
|
||||
def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
|
||||
if credentials.username != "foo" or credentials.password != "password":
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
return credentials.username
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(username: str = Depends(get_current_username)):
|
||||
return {"username": username}
|
||||
55
docs/tutorial/cors.md
Normal file
55
docs/tutorial/cors.md
Normal file
@@ -0,0 +1,55 @@
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank">CORS or "Cross-Origin Resource Sharing"</a> refers to the situations when a frontend running in a browser has JavaScript code that communicates with a backend, and the backend is in a different "origin" than the frontend.
|
||||
|
||||
## Origin
|
||||
|
||||
An origin is the combination of protocol (`http`, `https`), domain (`myapp.com`, `localhost`, `localhost.tiangolo.com`), and port (`80`, `443`, `8080`).
|
||||
|
||||
So, all these are different origins:
|
||||
|
||||
* `http://localhost`
|
||||
* `https://localhost`
|
||||
* `http://localhost:8080`
|
||||
|
||||
Even if they are all in `localhost`, they use different protocols or ports, so, they are different "origins".
|
||||
|
||||
## Steps
|
||||
|
||||
So, let's say you have a frontend running in your browser at `http://localhost:8080`, and its JavaScript is trying to communicate with a backend running at `http://localhost` (because we don't specify a port, the browser will assume the default port `80`).
|
||||
|
||||
Then, the browser will send an HTTP `OPTIONS` request to the backend, and if the backend sends the appropriate headers authorizing the communication from this different origin (`http://localhost:8080`) then the browser will let the JavaScript in the frontend send its request to the backend.
|
||||
|
||||
To achieve this, the backend must have a list of "allowed origins".
|
||||
|
||||
In this case, it would have to include `http://localhost:8080` for the frontend to work correctly.
|
||||
|
||||
## Wildcards
|
||||
|
||||
It's also possible to declare the list as `"*"` (a "wildcard") to say that all are allowed.
|
||||
|
||||
But that will only allow certain types of communication, excluding everything that involves credentials: Cookies, Authorization headers like those used with Bearer Tokens, etc.
|
||||
|
||||
So, for everything to work correctly, it's better to specify explicitly the allowed origins.
|
||||
|
||||
## Use `CORSMiddleware`
|
||||
|
||||
You can configure it in your **FastAPI** application using Starlette's <a href="https://www.starlette.io/middleware/#corsmiddleware" target="_blank">`CORSMiddleware`</a>.
|
||||
|
||||
* Import it form Starlette.
|
||||
* Create a list of allowed origins (as strings).
|
||||
* Add it as a "middleware" to your **FastAPI** application.
|
||||
|
||||
You can also specify if your backend allows:
|
||||
|
||||
* Credentials (Authorization headers, Cookies, etc).
|
||||
* Specific HTTP methods (`POST`, `PUT`) or all of them with the wildcard `"*"`.
|
||||
* Specific HTTP headers or all of them with the wildcard `"*"`.
|
||||
|
||||
```Python hl_lines="2 6 7 8 9 10 11 13 14 15 16 17 18 19"
|
||||
{!./src/cors/tutorial001.py!}
|
||||
```
|
||||
|
||||
## More info
|
||||
|
||||
For more details of what you can specify in `CORSMiddleware`, check <a href="https://www.starlette.io/middleware/#corsmiddleware" target="_blank">Starlette's `CORSMiddleware` docs</a>.
|
||||
|
||||
For more info about <abbr title="Cross-Origin Resource Sharing">CORS</abbr>, check the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank">Mozilla CORS documentation</a>.
|
||||
@@ -1,98 +1,88 @@
|
||||
!!! warning
|
||||
This is a rather advanced topic.
|
||||
|
||||
|
||||
If you are starting with **FastAPI**, you might not need this.
|
||||
|
||||
By default, **FastAPI** will return the responses using Starlette's `JSONResponse`.
|
||||
|
||||
But you can override it.
|
||||
You can override it by returning a `Response` directly, <a href="https://fastapi.tiangolo.com/tutorial/response-directly/" target="_blank">as seen in a previous section</a>.
|
||||
|
||||
But if you return a `Response` directly, the data won't be automatically converted, and the documentation won't be automatically generated (for example, including the specific "media type", in the HTTP header `Content-Type`).
|
||||
|
||||
But you can also declare the `Response` that you want to be used, in the *path operation decorator*.
|
||||
|
||||
The contents that you return from your *path operation function* will be put inside of that `Response`.
|
||||
|
||||
And if that `Response` has a JSON media type (`application/json`), like is the case with the `JSONResponse` and `UJSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*.
|
||||
|
||||
## Use `UJSONResponse`
|
||||
|
||||
For example, if you are squeezing performance, you can use `ujson` and set the response to be Starlette's `UJSONResponse`.
|
||||
For example, if you are squeezing performance, you can install and use `ujson` and set the response to be Starlette's `UJSONResponse`.
|
||||
|
||||
### Import `UJSONResponse`
|
||||
Import the `Response` class (sub-class) you want to use and declare it in the *path operation decorator*.
|
||||
|
||||
```Python hl_lines="2"
|
||||
```Python hl_lines="2 7"
|
||||
{!./src/custom_response/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! note
|
||||
Notice that you import it directly from `starlette.responses`, not from `fastapi`.
|
||||
|
||||
### Make your path operation use it
|
||||
|
||||
Make your path operation use `UJSONResponse` as the response class using the parameter `content_type`:
|
||||
|
||||
```Python hl_lines="7"
|
||||
{!./src/custom_response/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
The parameter is called `content_type` because it will also be used to define the "media type" of the response.
|
||||
The parameter `response_class` will also be used to define the "media type" of the response.
|
||||
|
||||
And will be documented as such in OpenAPI.
|
||||
In this case, the HTTP header `Content-Type` will be set to `application/json`.
|
||||
|
||||
And it will be documented as such in OpenAPI.
|
||||
|
||||
## HTML Response
|
||||
|
||||
To return a response with HTML directly from **FastAPI**, use `HTMLResponse`.
|
||||
|
||||
### Import `HTMLResponse`
|
||||
* Import `HTMLResponse`.
|
||||
* Pass `HTMLResponse` as the parameter `content_type` of your path operation.
|
||||
|
||||
```Python hl_lines="2"
|
||||
```Python hl_lines="2 7"
|
||||
{!./src/custom_response/tutorial002.py!}
|
||||
```
|
||||
|
||||
!!! note
|
||||
Notice that you import it directly from `starlette.responses`, not from `fastapi`.
|
||||
|
||||
|
||||
### Define your `content_type` class
|
||||
|
||||
Pass `HTMLResponse` as the parameter `content_type` of your path operation:
|
||||
|
||||
```Python hl_lines="7"
|
||||
{!./src/custom_response/tutorial002.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
The parameter is called `content_type` because it will also be used to define the "media type" of the response.
|
||||
The parameter `response_class` will also be used to define the "media type" of the response.
|
||||
|
||||
In this case, the HTTP header `Content-Type` will be set to `text/html`.
|
||||
|
||||
And it will be documented as such in OpenAPI.
|
||||
|
||||
|
||||
### Return a Starlette `Response`
|
||||
|
||||
You can also override the response directly in your path operation.
|
||||
|
||||
If you return an object that is an instance of Starlette's `Response`, it will be used as the response directly.
|
||||
As seen in <a href="https://fastapi.tiangolo.com/tutorial/response-directly/" target="_blank">another section</a>, you can also override the response directly in your path operation, by returning it.
|
||||
|
||||
The same example from above, returning an `HTMLResponse`, could look like:
|
||||
|
||||
```Python hl_lines="7"
|
||||
```Python hl_lines="2 7 19"
|
||||
{!./src/custom_response/tutorial003.py!}
|
||||
```
|
||||
|
||||
!!! info
|
||||
Of course, the `Content-Type` header will come from the the `Response` object your returned.
|
||||
|
||||
!!! warning
|
||||
A `Response` returned directly by your path operation function won't be documented in OpenAPI and won't be visible in the automatic interactive docs.
|
||||
A `Response` returned directly by your path operation function won't be documented in OpenAPI (for example, the `Content-Type` won't be documented) and won't be visible in the automatic interactive docs.
|
||||
|
||||
!!! info
|
||||
Of course, the actual `Content-Type` header, status code, etc, will come from the `Response` object your returned.
|
||||
|
||||
### Document in OpenAPI and override `Response`
|
||||
|
||||
If you want to override the response from inside of the function but at the same time document the "media type" in OpenAPI, you can use the `content_type` parameter AND return a `Response` object.
|
||||
If you want to override the response from inside of the function but at the same time document the "media type" in OpenAPI, you can use the `response_class` parameter AND return a `Response` object.
|
||||
|
||||
The `content_type` class will then be used only to document the OpenAPI path operation, but your `Response` will be used as is.
|
||||
The `response_class` will then be used only to document the OpenAPI path operation, but your `Response` will be used as is.
|
||||
|
||||
#### Return an `HTMLResponse` directly
|
||||
|
||||
For example, it could be something like:
|
||||
|
||||
```Python hl_lines="7 23"
|
||||
```Python hl_lines="7 23 21"
|
||||
{!./src/custom_response/tutorial004.py!}
|
||||
```
|
||||
|
||||
@@ -100,16 +90,10 @@ In this example, the function `generate_html_response()` already generates a Sta
|
||||
|
||||
By returning the result of calling `generate_html_response()`, you are already returning a `Response` that will override the default **FastAPI** behavior.
|
||||
|
||||
#### Declare `HTMLResponse` as `content_type`
|
||||
|
||||
But by declaring it also in the path operation decorator:
|
||||
|
||||
```Python hl_lines="21"
|
||||
{!./src/custom_response/tutorial004.py!}
|
||||
```
|
||||
|
||||
#### OpenAPI knows how to document it
|
||||
|
||||
...**FastAPI** will be able to document it in OpenAPI and in the interactive docs as HTML with `text/html`:
|
||||
But as you passed the `HTMLResponse` in the `response_class`, **FastAPI** will know how to document it in OpenAPI and the interactive docs as HTML with `text/html`:
|
||||
|
||||
<img src="/img/tutorial/custom-response/image01.png">
|
||||
|
||||
## Additional documentation
|
||||
|
||||
You can also declare the media type and many other details in OpenAPI using `responses`: <a href="https://fastapi.tiangolo.com/tutorial/additional-responses/" target="_blank">Additional Responses in OpenAPI</a>.
|
||||
|
||||
32
docs/tutorial/encoder.md
Normal file
32
docs/tutorial/encoder.md
Normal file
@@ -0,0 +1,32 @@
|
||||
There are some cases where you might need to convert a data type (like a Pydantic model) to something compatible with JSON (like a `dict`, `list`, etc).
|
||||
|
||||
For example, if you need to store it in a database.
|
||||
|
||||
For that, **FastAPI** provides a `jsonable_encoder()` function.
|
||||
|
||||
## Using the `jsonable_encoder`
|
||||
|
||||
Let's imagine that you have a database `fake_db` that only receives JSON compatible data.
|
||||
|
||||
For example, it doesn't receive `datetime` objects, as those are not compatible with JSON.
|
||||
|
||||
So, a `datetime` object would have to be converted to a `str` containing the data in <a href="https://en.wikipedia.org/wiki/ISO_8601" target="_blank">ISO format</a>.
|
||||
|
||||
The same way, this database wouldn't receive a Pydantic model (an object with attributes), only a `dict`.
|
||||
|
||||
You can use `jsonable_encoder` for that.
|
||||
|
||||
It receives an object, like a Pydantic model, and returns a JSON compatible version:
|
||||
|
||||
```Python hl_lines="4 21"
|
||||
{!./src/encoder/tutorial001.py!}
|
||||
```
|
||||
|
||||
In this example, it would convert the Pydantic model to a `dict`, and the `datetime` to a `str`.
|
||||
|
||||
The result of calling it is something that can be encoded with the Python standard <a href="https://docs.python.org/3/library/json.html#json.dumps" target="_blank">`json.dumps()`</a>.
|
||||
|
||||
It doesn't return a large `str` containing the data in JSON format (as a string). It returns a Python standard data structure (e.g. a `dict`) with values and sub-values that are all compatible with JSON.
|
||||
|
||||
!!! note
|
||||
`jsonable_encoder` is actually used by **FastAPI** internally to convert data. But it is useful in many other scenarios.
|
||||
54
docs/tutorial/middleware.md
Normal file
54
docs/tutorial/middleware.md
Normal file
@@ -0,0 +1,54 @@
|
||||
You can add middleware to **FastAPI** applications.
|
||||
|
||||
A "middleware" is a function that works with every **request** before it is processed by any specific *path operation*. And also with every **response** before returning it.
|
||||
|
||||
* It takes each **request** that comes to your application.
|
||||
* It can then do something to that **request** or run any needed code.
|
||||
* Then it passes the **request** to be processed by the rest of the application (by some *path operation*).
|
||||
* It then takes the **response** generated by the application (by some *path operation*).
|
||||
* It can do something to that **response** or run any needed code.
|
||||
* Then it returns the **response**.
|
||||
|
||||
## Create a middleware
|
||||
|
||||
To create a middleware you use the decorator `@app.middleware("http")` on top of a function.
|
||||
|
||||
The middleware function receives:
|
||||
|
||||
* The `request`.
|
||||
* A function `call_next` that will receive the `request` as a parameter.
|
||||
* This function will pass the `request` to the corresponding *path operation*.
|
||||
* Then it returns the `response` generated by the corresponding *path operation*.
|
||||
* You can then modify further the `response` before returning it.
|
||||
|
||||
```Python hl_lines="9 10 12 15"
|
||||
{!./src/middleware/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
This technique is used in the tutorial about <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">SQL (Relational) Databases</a>.
|
||||
|
||||
### Before and after the `response`
|
||||
|
||||
You can add code to be run with the `request`, before any *path operation* receives it.
|
||||
|
||||
And also after the `response` is generated, before returning it.
|
||||
|
||||
For example, you could add a custom header `X-Process-Time` containing the time in seconds that it took to process the request and generate a response:
|
||||
|
||||
```Python hl_lines="11 13 14"
|
||||
{!./src/middleware/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Starlette's Middleware
|
||||
|
||||
You can also add any other <a href="https://www.starlette.io/middleware/" target="_blank">Starlette Middleware</a>.
|
||||
|
||||
These are classes instead of plain functions.
|
||||
|
||||
Including:
|
||||
|
||||
* `CORSMiddleware` (described in the next section).
|
||||
* `GZipMiddleware`.
|
||||
* `SentryMiddleware`.
|
||||
* ...and others.
|
||||
63
docs/tutorial/response-directly.md
Normal file
63
docs/tutorial/response-directly.md
Normal file
@@ -0,0 +1,63 @@
|
||||
When you create a **FastAPI** *path operation* you can normally return any data from it: a `dict`, a `list`, a Pydantic model, a database model, etc.
|
||||
|
||||
By default, **FastAPI** would automatically convert that return value to JSON using the <a href="https://fastapi.tiangolo.com/tutorial/encoder/" target="_blank">`jsonable_encoder`</a>.
|
||||
|
||||
Then, behind the scenes, it would put that JSON-compatible data (e.g. a `dict`) inside of a Starlette `JSONResponse` that would be used to send the response to the client.
|
||||
|
||||
But you can return a `JSONResponse` directly from your *path operations*.
|
||||
|
||||
It might be useful, for example, to return custom headers or cookies.
|
||||
|
||||
## Starlette `Response`
|
||||
|
||||
In fact, you can return any <a href="https://www.starlette.io/responses/" target="_blank">Starlette `Response`</a> or any sub-class of it.
|
||||
|
||||
!!! tip
|
||||
`JSONResponse` itself is a sub-class of `Response`.
|
||||
|
||||
And when you return a Starlette `Response`, **FastAPI** will pass it directly.
|
||||
|
||||
It won't do any data conversion with Pydantic models, it won't convert the contents to any type, etc.
|
||||
|
||||
This gives you a lot of flexibility. You can return any data type, override any data declaration or validation, etc.
|
||||
|
||||
## Using the `jsonable_encoder` in a `Response`
|
||||
|
||||
Because **FastAPI** doesn't do any change to a `Response` you return, you have to make sure it's contents are ready for it.
|
||||
|
||||
For example, you cannot put a Pydantic model in a `JSONResponse` without first converting it to a `dict` with all the data types (like `datetime`, `UUID`, etc) converted to JSON-compatible types.
|
||||
|
||||
For those cases, you can use the `jsonable_encoder` to convert your data before passing it to a response:
|
||||
|
||||
```Python hl_lines="4 6 20 21"
|
||||
{!./src/response_directly/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! note
|
||||
Notice that you import it directly from `starlette.responses`, not from `fastapi`.
|
||||
|
||||
## Returning a custom `Response`
|
||||
|
||||
The example above shows all the parts you need, but it's not very useful yet, as you could have just returned the `item` directly, and **FastAPI** would put it in a `JSONResponse` for you, converting it to a `dict`, etc. All that by default.
|
||||
|
||||
Now, let's see how you could use that to return a custom response.
|
||||
|
||||
Let's say you want to return a response that is not available in the default <a href="https://www.starlette.io/responses/" target="_blank">Starlette `Response`s</a>.
|
||||
|
||||
Let's say that you want to return <a href="https://en.wikipedia.org/wiki/XML" target="_blank">XML</a>.
|
||||
|
||||
You could put your XML content in a string, put it in a Starlette Response, and return it:
|
||||
|
||||
```Python hl_lines="2 20"
|
||||
{!./src/response_directly/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
When you return a `Response` directly its data is not validated, converted (serialized), nor documented automatically.
|
||||
|
||||
But you can still <a href="tutorial/additional-responses/" target="_blank">document it</a>.
|
||||
|
||||
In the next sections you will see how to use/declare these custom `Response`s while still having automatic data conversion, documentation, etc.
|
||||
|
||||
You will also see how to use them to set response Headers and Cookies.
|
||||
40
docs/tutorial/security/http-basic-auth.md
Normal file
40
docs/tutorial/security/http-basic-auth.md
Normal file
@@ -0,0 +1,40 @@
|
||||
For the simplest cases, you can use HTTP Basic Auth.
|
||||
|
||||
In HTTP Basic Auth, the application expects a header that contains a username and a password.
|
||||
|
||||
If it doesn't receive it, it returns an HTTP 401 "Unauthorized" error.
|
||||
|
||||
And returns a header `WWW-Authenticate` with a value of `Basic`, and an optional `realm` parameter.
|
||||
|
||||
That tells the browser to show the integrated prompt for a username and password.
|
||||
|
||||
Then, when you type that username and password, the browser sends them in the header automatically.
|
||||
|
||||
## Simple HTTP Basic Auth
|
||||
|
||||
* Import `HTTPBAsic` and `HTTPBasicCredentials`.
|
||||
* Create a "`security` scheme" using `HTTPBAsic`.
|
||||
* Use that `security` with a dependency in your *path operation*.
|
||||
* It returns an object of type `HTTPBasicCredentials`:
|
||||
* It contains the `username` and `password` sent.
|
||||
|
||||
|
||||
```Python hl_lines="2 6 10"
|
||||
{!./src/security/tutorial006.py!}
|
||||
```
|
||||
|
||||
When you try to open the URL for the first time (or click the "Execute" button in the docs) the browser will ask you for your username and password:
|
||||
|
||||
<img src="/img/tutorial/security/image12.png">
|
||||
|
||||
## Check the username
|
||||
|
||||
Here's a more complete example.
|
||||
|
||||
Use a dependency to check if the username and password are correct.
|
||||
|
||||
If the credentials are incorrect, return an `HTTPException` with a status code 401 (the same returned when no credentials are provided) and add the header `WWW-Authenticate` to make the browser show the login prompt again:
|
||||
|
||||
```Python hl_lines="10 11 12 13 14 15 16 17 21"
|
||||
{!./src/security/tutorial007.py!}
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.16.0"
|
||||
__version__ = "0.19.0"
|
||||
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ class FastAPI(Starlette):
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> None:
|
||||
self.router.add_api_route(
|
||||
@@ -136,7 +136,7 @@ class FastAPI(Starlette):
|
||||
methods=methods,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -155,7 +155,7 @@ class FastAPI(Starlette):
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@@ -173,7 +173,7 @@ class FastAPI(Starlette):
|
||||
methods=methods,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
return func
|
||||
@@ -206,7 +206,7 @@ class FastAPI(Starlette):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.get(
|
||||
@@ -221,7 +221,7 @@ class FastAPI(Starlette):
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -239,7 +239,7 @@ class FastAPI(Starlette):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.put(
|
||||
@@ -254,7 +254,7 @@ class FastAPI(Starlette):
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -272,7 +272,7 @@ class FastAPI(Starlette):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.post(
|
||||
@@ -287,7 +287,7 @@ class FastAPI(Starlette):
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -305,7 +305,7 @@ class FastAPI(Starlette):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.delete(
|
||||
@@ -320,7 +320,7 @@ class FastAPI(Starlette):
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -338,7 +338,7 @@ class FastAPI(Starlette):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.options(
|
||||
@@ -353,7 +353,7 @@ class FastAPI(Starlette):
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -371,7 +371,7 @@ class FastAPI(Starlette):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.head(
|
||||
@@ -386,7 +386,7 @@ class FastAPI(Starlette):
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -404,7 +404,7 @@ class FastAPI(Starlette):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.patch(
|
||||
@@ -419,7 +419,7 @@ class FastAPI(Starlette):
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -437,7 +437,7 @@ class FastAPI(Starlette):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.trace(
|
||||
@@ -452,6 +452,6 @@ class FastAPI(Starlette):
|
||||
deprecated=deprecated,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ def jsonable_encoder(
|
||||
obj: Any,
|
||||
include: Set[str] = None,
|
||||
exclude: Set[str] = set(),
|
||||
by_alias: bool = False,
|
||||
by_alias: bool = True,
|
||||
include_none: bool = True,
|
||||
custom_encoder: dict = {},
|
||||
sqlalchemy_safe: bool = True,
|
||||
|
||||
@@ -197,7 +197,7 @@ def get_openapi_path(
|
||||
] = response
|
||||
status_code = str(route.status_code)
|
||||
response_schema = {"type": "string"}
|
||||
if lenient_issubclass(route.content_type, JSONResponse):
|
||||
if lenient_issubclass(route.response_class, JSONResponse):
|
||||
if route.response_field:
|
||||
response_schema, _ = field_schema(
|
||||
route.response_field,
|
||||
@@ -211,7 +211,7 @@ def get_openapi_path(
|
||||
] = route.response_description
|
||||
operation.setdefault("responses", {}).setdefault(
|
||||
status_code, {}
|
||||
).setdefault("content", {}).setdefault(route.content_type.media_type, {})[
|
||||
).setdefault("content", {}).setdefault(route.response_class.media_type, {})[
|
||||
"schema"
|
||||
] = response_schema
|
||||
if all_route_params or route.body_field:
|
||||
|
||||
@@ -40,7 +40,7 @@ def get_app(
|
||||
dependant: Dependant,
|
||||
body_field: Field = None,
|
||||
status_code: int = 200,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_field: Field = None,
|
||||
) -> Callable:
|
||||
assert dependant.call is not None, "dependant.call must me a function"
|
||||
@@ -83,7 +83,7 @@ def get_app(
|
||||
response_data = serialize_response(
|
||||
field=response_field, response=raw_response
|
||||
)
|
||||
return content_type(
|
||||
return response_class(
|
||||
content=response_data,
|
||||
status_code=status_code,
|
||||
background=background_tasks,
|
||||
@@ -110,7 +110,7 @@ class APIRoute(routing.Route):
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
) -> None:
|
||||
assert path.startswith("/"), "Routed paths must always start with '/'"
|
||||
self.path = path
|
||||
@@ -119,7 +119,7 @@ class APIRoute(routing.Route):
|
||||
self.response_model = response_model
|
||||
if self.response_model:
|
||||
assert lenient_issubclass(
|
||||
content_type, JSONResponse
|
||||
response_class, JSONResponse
|
||||
), "To declare a type the response must be a JSON response"
|
||||
response_name = "Response_" + self.name
|
||||
self.response_field: Optional[Field] = Field(
|
||||
@@ -168,7 +168,7 @@ class APIRoute(routing.Route):
|
||||
self.methods = methods
|
||||
self.operation_id = operation_id
|
||||
self.include_in_schema = include_in_schema
|
||||
self.content_type = content_type
|
||||
self.response_class = response_class
|
||||
|
||||
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
|
||||
assert inspect.isfunction(endpoint) or inspect.ismethod(
|
||||
@@ -181,7 +181,7 @@ class APIRoute(routing.Route):
|
||||
dependant=self.dependant,
|
||||
body_field=self.body_field,
|
||||
status_code=self.status_code,
|
||||
content_type=self.content_type,
|
||||
response_class=self.response_class,
|
||||
response_field=self.response_field,
|
||||
)
|
||||
)
|
||||
@@ -204,7 +204,7 @@ class APIRouter(routing.Router):
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> None:
|
||||
route = APIRoute(
|
||||
@@ -221,7 +221,7 @@ class APIRouter(routing.Router):
|
||||
methods=methods,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
self.routes.append(route)
|
||||
@@ -241,7 +241,7 @@ class APIRouter(routing.Router):
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@@ -259,7 +259,7 @@ class APIRouter(routing.Router):
|
||||
methods=methods,
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
return func
|
||||
@@ -298,7 +298,7 @@ class APIRouter(routing.Router):
|
||||
methods=route.methods,
|
||||
operation_id=route.operation_id,
|
||||
include_in_schema=route.include_in_schema,
|
||||
content_type=route.content_type,
|
||||
response_class=route.response_class,
|
||||
name=route.name,
|
||||
)
|
||||
elif isinstance(route, routing.Route):
|
||||
@@ -328,7 +328,7 @@ class APIRouter(routing.Router):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -344,7 +344,7 @@ class APIRouter(routing.Router):
|
||||
methods=["GET"],
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -362,7 +362,7 @@ class APIRouter(routing.Router):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -378,7 +378,7 @@ class APIRouter(routing.Router):
|
||||
methods=["PUT"],
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -396,7 +396,7 @@ class APIRouter(routing.Router):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -412,7 +412,7 @@ class APIRouter(routing.Router):
|
||||
methods=["POST"],
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -430,7 +430,7 @@ class APIRouter(routing.Router):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -446,7 +446,7 @@ class APIRouter(routing.Router):
|
||||
methods=["DELETE"],
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -464,7 +464,7 @@ class APIRouter(routing.Router):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -480,7 +480,7 @@ class APIRouter(routing.Router):
|
||||
methods=["OPTIONS"],
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -498,7 +498,7 @@ class APIRouter(routing.Router):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -514,7 +514,7 @@ class APIRouter(routing.Router):
|
||||
methods=["HEAD"],
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -532,7 +532,7 @@ class APIRouter(routing.Router):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -548,7 +548,7 @@ class APIRouter(routing.Router):
|
||||
methods=["PATCH"],
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -566,7 +566,7 @@ class APIRouter(routing.Router):
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
include_in_schema: bool = True,
|
||||
content_type: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -582,6 +582,6 @@ class APIRouter(routing.Router):
|
||||
methods=["TRACE"],
|
||||
operation_id=operation_id,
|
||||
include_in_schema=include_in_schema,
|
||||
content_type=content_type,
|
||||
response_class=response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import binascii
|
||||
from base64 import b64decode
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.openapi.models import (
|
||||
HTTPBase as HTTPBaseModel,
|
||||
HTTPBearer as HTTPBearerModel,
|
||||
@@ -9,9 +10,8 @@ from fastapi.openapi.models import (
|
||||
from fastapi.security.base import SecurityBase
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from pydantic import BaseModel
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
class HTTPBasicCredentials(BaseModel):
|
||||
@@ -59,15 +59,21 @@ class HTTPBasic(HTTPBase):
|
||||
async def __call__(self, request: Request) -> Optional[HTTPBasicCredentials]:
|
||||
authorization: str = request.headers.get("Authorization")
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
# before implementing headers with 401 errors, wait for: https://github.com/encode/starlette/issues/295
|
||||
# unauthorized_headers = {"WWW-Authenticate": "Basic"}
|
||||
if self.realm:
|
||||
unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
|
||||
else:
|
||||
unauthorized_headers = {"WWW-Authenticate": "Basic"}
|
||||
invalid_user_credentials_exc = HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Invalid authentication credentials"
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials",
|
||||
headers=unauthorized_headers,
|
||||
)
|
||||
if not authorization or scheme.lower() != "basic":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers=unauthorized_headers,
|
||||
)
|
||||
else:
|
||||
return None
|
||||
@@ -87,7 +93,7 @@ class HTTPBearer(HTTPBase):
|
||||
*,
|
||||
bearerFormat: str = None,
|
||||
scheme_name: str = None,
|
||||
auto_error: bool = True
|
||||
auto_error: bool = True,
|
||||
):
|
||||
self.model = HTTPBearerModel(bearerFormat=bearerFormat)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
|
||||
@@ -44,8 +44,10 @@ nav:
|
||||
- Path Operation Configuration: 'tutorial/path-operation-configuration.md'
|
||||
- Path Operation Advanced Configuration: 'tutorial/path-operation-advanced-configuration.md'
|
||||
- Additional Status Codes: 'tutorial/additional-status-codes.md'
|
||||
- Custom Response: 'tutorial/custom-response.md'
|
||||
- Additional Responses: 'tutorial/additional-responses.md'
|
||||
- JSON compatible encoder: 'tutorial/encoder.md'
|
||||
- Return a Response directly: 'tutorial/response-directly.md'
|
||||
- Custom Response Class: 'tutorial/custom-response.md'
|
||||
- Additional Responses in OpenAPI: 'tutorial/additional-responses.md'
|
||||
- Dependencies:
|
||||
- First Steps: 'tutorial/dependencies/first-steps.md'
|
||||
- Classes as Dependencies: 'tutorial/dependencies/classes-as-dependencies.md'
|
||||
@@ -58,6 +60,9 @@ nav:
|
||||
- Simple OAuth2 with Password and Bearer: 'tutorial/security/simple-oauth2.md'
|
||||
- OAuth2 with Password (and hashing), Bearer with JWT tokens: 'tutorial/security/oauth2-jwt.md'
|
||||
- OAuth2 scopes: 'tutorial/security/oauth2-scopes.md'
|
||||
- HTTP Basic Auth: 'tutorial/security/http-basic-auth.md'
|
||||
- Middleware: 'tutorial/middleware.md'
|
||||
- CORS (Cross-Origin Resource Sharing): 'tutorial/cors.md'
|
||||
- Using the Request Directly: 'tutorial/using-request-directly.md'
|
||||
- SQL (Relational) Databases: 'tutorial/sql-databases.md'
|
||||
- Async SQL (Relational) Databases: 'tutorial/async-sql-databases.md'
|
||||
|
||||
7
scripts/deploy.sh
Executable file
7
scripts/deploy.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
bash scripts/publish.sh
|
||||
|
||||
bash scripts/trigger-docker.sh
|
||||
5
scripts/publish.sh
Executable file
5
scripts/publish.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
flit publish
|
||||
@@ -3,7 +3,7 @@ from enum import Enum
|
||||
|
||||
import pytest
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Schema, ValidationError
|
||||
|
||||
|
||||
class Person:
|
||||
@@ -59,6 +59,10 @@ class ModelWithConfig(BaseModel):
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class ModelWithAlias(BaseModel):
|
||||
foo: str = Schema(..., alias="Foo")
|
||||
|
||||
|
||||
def test_encode_class():
|
||||
person = Person(name="Foo")
|
||||
pet = Pet(owner=person, name="Firulais")
|
||||
@@ -85,3 +89,13 @@ def test_encode_custom_json_encoders_model():
|
||||
def test_encode_model_with_config():
|
||||
model = ModelWithConfig(role=RoleEnum.admin)
|
||||
assert jsonable_encoder(model) == {"role": "admin"}
|
||||
|
||||
|
||||
def test_encode_model_with_alias_raises():
|
||||
with pytest.raises(ValidationError):
|
||||
model = ModelWithAlias(foo="Bar")
|
||||
|
||||
|
||||
def test_encode_model_with_alias():
|
||||
model = ModelWithAlias(Foo="Bar")
|
||||
assert jsonable_encoder(model) == {"Foo": "Bar"}
|
||||
|
||||
@@ -67,7 +67,8 @@ def test_security_http_basic_invalid_credentials():
|
||||
response = client.get(
|
||||
"/users/me", headers={"Authorization": "Basic notabase64token"}
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.status_code == 401
|
||||
assert response.headers["WWW-Authenticate"] == "Basic"
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
|
||||
|
||||
@@ -75,5 +76,6 @@ def test_security_http_basic_non_basic_credentials():
|
||||
payload = b64encode(b"johnsecret").decode("ascii")
|
||||
auth_header = f"Basic {payload}"
|
||||
response = client.get("/users/me", headers={"Authorization": auth_header})
|
||||
assert response.status_code == 403
|
||||
assert response.status_code == 401
|
||||
assert response.headers["WWW-Authenticate"] == "Basic"
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
|
||||
@@ -7,7 +7,7 @@ from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPBasic()
|
||||
security = HTTPBasic(realm="simple")
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
@@ -56,15 +56,17 @@ def test_security_http_basic():
|
||||
|
||||
def test_security_http_basic_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.status_code == 401
|
||||
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
|
||||
|
||||
|
||||
def test_security_http_basic_invalid_credentials():
|
||||
response = client.get(
|
||||
"/users/me", headers={"Authorization": "Basic notabase64token"}
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.status_code == 401
|
||||
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
|
||||
|
||||
@@ -72,5 +74,6 @@ def test_security_http_basic_non_basic_credentials():
|
||||
payload = b64encode(b"johnsecret").decode("ascii")
|
||||
auth_header = f"Basic {payload}"
|
||||
response = client.get("/users/me", headers={"Authorization": auth_header})
|
||||
assert response.status_code == 403
|
||||
assert response.status_code == 401
|
||||
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
69
tests/test_tutorial/test_security/test_tutorial006.py
Normal file
69
tests/test_tutorial/test_security/test_tutorial006.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from base64 import b64encode
|
||||
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from security.tutorial006 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"HTTPBasic": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_http_basic():
|
||||
auth = HTTPBasicAuth(username="john", password="secret")
|
||||
response = client.get("/users/me", auth=auth)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "john", "password": "secret"}
|
||||
|
||||
|
||||
def test_security_http_basic_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
assert response.status_code == 401
|
||||
assert response.headers["WWW-Authenticate"] == "Basic"
|
||||
|
||||
|
||||
def test_security_http_basic_invalid_credentials():
|
||||
response = client.get(
|
||||
"/users/me", headers={"Authorization": "Basic notabase64token"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
assert response.headers["WWW-Authenticate"] == "Basic"
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
|
||||
|
||||
def test_security_http_basic_non_basic_credentials():
|
||||
payload = b64encode(b"johnsecret").decode("ascii")
|
||||
auth_header = f"Basic {payload}"
|
||||
response = client.get("/users/me", headers={"Authorization": auth_header})
|
||||
assert response.status_code == 401
|
||||
assert response.headers["WWW-Authenticate"] == "Basic"
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
Reference in New Issue
Block a user