Compare commits

..

21 Commits

Author SHA1 Message Date
Sebastián Ramírez
8e3a7699a3 🔖 Release 0.19.0 2019-04-26 13:48:30 +04:00
Sebastián Ramírez
8998ccaffb 📝 Update release notes 2019-04-26 13:45:38 +04:00
Sebastián Ramírez
2b7f201a44 📝 Add docs about returning a response directly and encoder (#184) 2019-04-26 13:40:23 +04:00
Sebastián Ramírez
192ebba2a2 ♻️ Rename parameter content_type to response_class (#183) 2019-04-26 13:11:16 +04:00
Sebastián Ramírez
8880c4cb03 🔖 Release 0.18.0 2019-04-22 21:08:43 +04:00
Sebastián Ramírez
6324be684f 📝 Update release notes 2019-04-21 22:31:43 +04:00
Sebastián Ramírez
c705685394 Add docs for HTTP Basic Auth and tests (#177) 2019-04-21 22:30:58 +04:00
Sebastián Ramírez
945f401d8e 📝 Update release notes 2019-04-21 21:46:00 +04:00
Sebastián Ramírez
f216d340ec Add automatic header handling for HTTP Basic Auth (#175)
*  Add automatic header handling for HTTP Basic Auth

* 🎨 Remove obsolete comment
2019-04-21 21:44:25 +04:00
Sebastián Ramírez
a4558e7053 📝 Update release notes 2019-04-21 20:21:53 +04:00
Sebastián Ramírez
298f8478e2 🔒 Fix development dependencies security (#174) 2019-04-21 20:20:25 +04:00
Sebastián Ramírez
b86d163470 📝 Rename additional response OpenAPI declarations 2019-04-21 20:13:26 +04:00
Sebastián Ramírez
9e2d37b89c 📝 Update release notes 2019-04-21 19:57:07 +04:00
Sebastián Ramírez
97adadd9e1 📝 Add docs for middleware (#173) 2019-04-21 19:56:20 +04:00
Sebastián Ramírez
26e3dffb37 🚀 Deploy when tagged using Python 3.6 2019-04-20 22:16:07 +04:00
Sebastián Ramírez
aa7b4bd101 🔖 0.17.0 2019-04-20 22:12:55 +04:00
Sebastián Ramírez
ffc4c716c0 🚀 Make Flit publish from CI (#170) 2019-04-20 22:09:35 +04:00
Sebastián Ramírez
ef7b6e8eaf 📝 Update Release Notes 2019-04-20 21:15:03 +04:00
Sebastián Ramírez
596243f4a5 Add docs about CORS (#169) 2019-04-20 21:13:01 +04:00
Sebastián Ramírez
766bf1c5aa 📝 Update release notes 2019-04-20 20:31:44 +04:00
Sebastián Ramírez
9e748dbca4 By default, encode by alias (#168) 2019-04-20 20:29:54 +04:00
33 changed files with 665 additions and 164 deletions

View File

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

@@ -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": {

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

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

View 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=["*"],
)

View File

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

View File

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

View File

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

View 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

View 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

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

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

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

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -e
flit publish

View File

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

View File

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

View File

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

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