mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-27 16:21:06 -05:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0f7961b65 | ||
|
|
1c2ecbb89a | ||
|
|
5a6e47bd49 | ||
|
|
58872dca74 | ||
|
|
9b04593260 | ||
|
|
6d77e2ac5f | ||
|
|
b16ca54c30 | ||
|
|
834723cf2c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ site
|
||||
coverage.xml
|
||||
.netlify
|
||||
test.db
|
||||
log.txt
|
||||
|
||||
1
Pipfile
1
Pipfile
@@ -27,6 +27,7 @@ uvicorn = "*"
|
||||
[packages]
|
||||
starlette = "==0.11.1"
|
||||
pydantic = "==0.21.0"
|
||||
databases = {extras = ["sqlite"],version = "*"}
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
|
||||
90
Pipfile.lock
generated
90
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "676c6ae13691eef64abe6638f833cb8a330612521d3fad08718b240328b4877a"
|
||||
"sha256": "24b3b7b88d3cbe671ddbe296e64c15f8558f0e5d5df977200119872a363aac13"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -16,6 +16,37 @@
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"aiocontextvars": {
|
||||
"hashes": [
|
||||
"sha256:1e0ff5837c8b01c36a1107acdd0baf7853ebdf6c9fc43e8e311f4be37ac2038a",
|
||||
"sha256:6ff7aee14f549d52f0446cbb84d0deddcd3fc677bcf8fbc2ce13f5756d2064dc"
|
||||
],
|
||||
"markers": "python_version < '3.7'",
|
||||
"version": "==0.2.1"
|
||||
},
|
||||
"aiosqlite": {
|
||||
"hashes": [
|
||||
"sha256:af4fed9e778756fa0ffffc7a8b14c4d7b1a57155dc5669f18e45107313f6019e"
|
||||
],
|
||||
"version": "==0.9.0"
|
||||
},
|
||||
"contextvars": {
|
||||
"hashes": [
|
||||
"sha256:2341042e1c03a271813e07dba29b6b60fa85c1005ea5ed1638a076cf50b4d625"
|
||||
],
|
||||
"markers": "python_version < '3.7'",
|
||||
"version": "==2.3"
|
||||
},
|
||||
"databases": {
|
||||
"extras": [
|
||||
"sqlite"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:4a0f15669c390a04b439972426350c0ae921ddc08c42bd54f125eb2fb86ee728"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
"dataclasses": {
|
||||
"hashes": [
|
||||
"sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f",
|
||||
@@ -24,6 +55,22 @@
|
||||
"markers": "python_version < '3.7'",
|
||||
"version": "==0.6"
|
||||
},
|
||||
"immutables": {
|
||||
"hashes": [
|
||||
"sha256:1e4f4513254ef11e0230a558ee0dcb4551b914993c330005d15338da595d3750",
|
||||
"sha256:228e38dc7a810ba4ff88909908ac47f840e5dc6c4c0da6b25009c626a9ae771c",
|
||||
"sha256:2ae88fbfe1d04f4e5859c924e97313edf70e72b4f19871bf329b96a67ede9ba0",
|
||||
"sha256:2d32b61c222cba1dd11f0faff67c7fb6204ef1982454e1b5b001d4b79966ef17",
|
||||
"sha256:35af186bfac5b62522fdf2cab11120d7b0547f405aa399b6a1e443cf5f5e318c",
|
||||
"sha256:63023fa0cceedc62e0d1535cd4ca7a1f6df3120a6d8e5c34e89037402a6fd809",
|
||||
"sha256:6bf5857f42a96331fd0929c357dc0b36a72f339f3b6acaf870b149c96b141f69",
|
||||
"sha256:7bb1590024a032c7a57f79faf8c8ff5e91340662550d2980e0177f67e66e9c9c",
|
||||
"sha256:7c090687d7e623d4eca22962635b5e1a1ee2d6f9a9aca2f3fb5a184a1ffef1f2",
|
||||
"sha256:bc36a0a8749881eebd753f696b081bd51145e4d77291d671d2e2f622e5b65d2f",
|
||||
"sha256:d9fc6a236018d99af6453ead945a6bb55f98d14b1801a2c229dd993edc753a00"
|
||||
],
|
||||
"version": "==0.6"
|
||||
},
|
||||
"pydantic": {
|
||||
"hashes": [
|
||||
"sha256:93fa585402e7c8c01623ea8af6ca23363e8b4c6a020b7a2de9e99fa29d642d50",
|
||||
@@ -32,6 +79,12 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.21.0"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:781fb7b9d194ed3fc596b8f0dd4623ff160e3e825dd8c15472376a438c19598b"
|
||||
],
|
||||
"version": "==1.3.1"
|
||||
},
|
||||
"starlette": {
|
||||
"hashes": [
|
||||
"sha256:9d48b35d1fc7521d59ae53c421297ab3878d3c7cd4b75266d77f6c73cccb78bb"
|
||||
@@ -242,11 +295,11 @@
|
||||
},
|
||||
"ipython": {
|
||||
"hashes": [
|
||||
"sha256:06de667a9e406924f97781bda22d5d76bfb39762b678762d86a466e63f65dc39",
|
||||
"sha256:5d3e020a6b5f29df037555e5c45ab1088d6a7cf3bd84f47e0ba501eeb0c3ec82"
|
||||
"sha256:b038baa489c38f6d853a3cfc4c635b0cda66f2864d136fe8f40c1a6e334e2a6b",
|
||||
"sha256:f5102c1cd67e399ec8ea66bcebe6e3968ea25a8977e53f012963e5affeb1fe38"
|
||||
],
|
||||
"markers": "python_version >= '3.3'",
|
||||
"version": "==7.3.0"
|
||||
"version": "==7.4.0"
|
||||
},
|
||||
"ipython-genutils": {
|
||||
"hashes": [
|
||||
@@ -264,11 +317,11 @@
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
"sha256:18c796c2cd35eb1a1d3f012a214a542790a1aed95e29768bdcb9f2197eccbd0b",
|
||||
"sha256:96151fca2c6e736503981896495d344781b60d18bfda78dc11b290c6125ebdb6"
|
||||
"sha256:08f8e3f0f0b7249e9fad7e5c41e2113aba44969798a26452ee790c06f155d4ec",
|
||||
"sha256:4e9e9c4bd1acd66cf6c36973f29b031ec752cbfd991c69695e4e259f9a756927"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.3.15"
|
||||
"version": "==4.3.16"
|
||||
},
|
||||
"jedi": {
|
||||
"hashes": [
|
||||
@@ -399,11 +452,11 @@
|
||||
},
|
||||
"mkdocs-material": {
|
||||
"hashes": [
|
||||
"sha256:762a71f82c1e291c3ff067cecd9d581557da777332fd98bc0af20fd5ab4a2dd0",
|
||||
"sha256:b2c7174ecaa81fb1d62a5f4906f99fa0e7062ced8f9a14ec4f60b1bef9feebbf"
|
||||
"sha256:0b394aa034b25a09a5874ae2a6ccc426fd81f5764e0991217b169e31cb0c1c0e",
|
||||
"sha256:f5bb80a2c16d045d380edb2c5b05636af1bb709cb859bfaa9d01063a11df803f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.2"
|
||||
"version": "==4.1.0"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
@@ -662,7 +715,6 @@
|
||||
"hashes": [
|
||||
"sha256:781fb7b9d194ed3fc596b8f0dd4623ff160e3e825dd8c15472376a438c19598b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.1"
|
||||
},
|
||||
"terminado": {
|
||||
@@ -688,15 +740,15 @@
|
||||
},
|
||||
"tornado": {
|
||||
"hashes": [
|
||||
"sha256:1a58f2d603476d5e462f7c28ca1dbb5ac7e51348b27a9cac849cdec3471101f8",
|
||||
"sha256:33f93243cd46dd398e5d2bbdd75539564d1f13f25d704cfc7541db74066d6695",
|
||||
"sha256:34e59401afcecf0381a28228daad8ed3275bcb726810654612d5e9c001f421b7",
|
||||
"sha256:35817031611d2c296c69e5023ea1f9b5720be803e3bb119464bb2a0405d5cd70",
|
||||
"sha256:666b335cef5cc2759c21b7394cff881f71559aaf7cb8c4458af5bb6cb7275b47",
|
||||
"sha256:81203efb26debaaef7158187af45bc440796de9fb1df12a75b65fae11600a255",
|
||||
"sha256:de274c65f45f6656c375cdf1759dbf0bc52902a1e999d12a35eb13020a641a53"
|
||||
"sha256:1174dcb84d08887b55defb2cda1986faeeea715fff189ef3dc44cce99f5fca6b",
|
||||
"sha256:2613fab506bd2aedb3722c8c64c17f8f74f4070afed6eea17f20b2115e445aec",
|
||||
"sha256:44b82bc1146a24e5b9853d04c142576b4e8fa7a92f2e30bc364a85d1f75c4de2",
|
||||
"sha256:457fcbee4df737d2defc181b9073758d73f54a6cfc1f280533ff48831b39f4a8",
|
||||
"sha256:49603e1a6e24104961497ad0c07c799aec1caac7400a6762b687e74c8206677d",
|
||||
"sha256:8c2f40b99a8153893793559919a355d7b74649a11e59f411b0b0a1793e160bc0",
|
||||
"sha256:e1d897889c3b5a829426b7d52828fb37b28bc181cd598624e65c8be40ee3f7fa"
|
||||
],
|
||||
"version": "==6.0.1"
|
||||
"version": "==6.0.2"
|
||||
},
|
||||
"traitlets": {
|
||||
"hashes": [
|
||||
|
||||
BIN
docs/img/tutorial/async-sql-databases/image01.png
Normal file
BIN
docs/img/tutorial/async-sql-databases/image01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
@@ -1,5 +1,17 @@
|
||||
## Next release
|
||||
|
||||
## 0.10.1
|
||||
|
||||
* Add docs and tests for <a href="https://github.com/encode/databases" target="_blank">encode/databases</a>. New docs at: <a href="https://fastapi.tiangolo.com/tutorial/async-sql-databases/" target="_blank">Async SQL (Relational) Databases</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/107" target="_blank">#107</a>.
|
||||
|
||||
## 0.10.0
|
||||
|
||||
* Add support for Background Tasks in *path operation functions* and dependencies. New documentation about <a href="https://fastapi.tiangolo.com/tutorial/background-tasks/" target="_blank">Background Tasks is here</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/103" target="_blank">#103</a>.
|
||||
|
||||
* Add support for `.websocket_route()` in `APIRouter`. PR <a href="https://github.com/tiangolo/fastapi/pull/100" target="_blank">#100</a> by <a href="https://github.com/euri10" target="_blank">@euri10</a>.
|
||||
|
||||
* New docs section about <a href="https://fastapi.tiangolo.com/tutorial/events/" target="_blank">Events: startup - shutdown</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/99" target="_blank">#99</a>.
|
||||
|
||||
## 0.9.1
|
||||
|
||||
* Document receiving <a href="https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#query-parameter-list-multiple-values" target="_blank">Multiple values with the same query parameter</a> and <a href="https://fastapi.tiangolo.com/tutorial/header-params/#duplicate-headers" target="_blank">Duplicate headers</a>. PR <a href="https://github.com/tiangolo/fastapi/pull/95" target="_blank">#95</a>.
|
||||
|
||||
65
docs/src/async_sql_databases/tutorial001.py
Normal file
65
docs/src/async_sql_databases/tutorial001.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from typing import List
|
||||
|
||||
import databases
|
||||
import sqlalchemy
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
# SQLAlchemy specific code, as with any other app
|
||||
DATABASE_URL = "sqlite:///./test.db"
|
||||
# DATABASE_URL = "postgresql://user:password@postgresserver/db"
|
||||
|
||||
database = databases.Database(DATABASE_URL)
|
||||
|
||||
metadata = sqlalchemy.MetaData()
|
||||
|
||||
notes = sqlalchemy.Table(
|
||||
"notes",
|
||||
metadata,
|
||||
sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
|
||||
sqlalchemy.Column("text", sqlalchemy.String),
|
||||
sqlalchemy.Column("completed", sqlalchemy.Boolean),
|
||||
)
|
||||
|
||||
|
||||
engine = sqlalchemy.create_engine(
|
||||
DATABASE_URL, connect_args={"check_same_thread": False}
|
||||
)
|
||||
metadata.create_all(engine)
|
||||
|
||||
|
||||
class NoteIn(BaseModel):
|
||||
text: str
|
||||
completed: bool
|
||||
|
||||
|
||||
class Note(BaseModel):
|
||||
id: int
|
||||
text: str
|
||||
completed: bool
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
await database.connect()
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown():
|
||||
await database.disconnect()
|
||||
|
||||
|
||||
@app.get("/notes/", response_model=List[Note])
|
||||
async def read_notes():
|
||||
query = notes.select()
|
||||
return await database.fetch_all(query)
|
||||
|
||||
|
||||
@app.post("/notes/", response_model=Note)
|
||||
async def create_note(note: NoteIn):
|
||||
query = notes.insert().values(text=note.text, completed=note.completed)
|
||||
last_record_id = await database.execute(query)
|
||||
return {**note.dict(), "id": last_record_id}
|
||||
15
docs/src/background_tasks/tutorial001.py
Normal file
15
docs/src/background_tasks/tutorial001.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from fastapi import BackgroundTasks, FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
def write_notification(email: str, message=""):
|
||||
with open("log.txt", mode="w") as email_file:
|
||||
content = f"notification for {email}: {message}"
|
||||
email_file.write(content)
|
||||
|
||||
|
||||
@app.post("/send-notification/{email}")
|
||||
async def send_notification(email: str, background_tasks: BackgroundTasks):
|
||||
background_tasks.add_task(write_notification, email, message="some notification")
|
||||
return {"message": "Notification sent in the background"}
|
||||
24
docs/src/background_tasks/tutorial002.py
Normal file
24
docs/src/background_tasks/tutorial002.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from fastapi import BackgroundTasks, Depends, FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
def write_log(message: str):
|
||||
with open("log.txt", mode="a") as log:
|
||||
log.write(message)
|
||||
|
||||
|
||||
def get_query(background_tasks: BackgroundTasks, q: str = None):
|
||||
if q:
|
||||
message = f"found query: {q}\n"
|
||||
background_tasks.add_task(write_log, message)
|
||||
return q
|
||||
|
||||
|
||||
@app.post("/send-notification/{email}")
|
||||
async def send_notification(
|
||||
email: str, background_tasks: BackgroundTasks, q: str = Depends(get_query)
|
||||
):
|
||||
message = f"message to {email}\n"
|
||||
background_tasks.add_task(write_log, message)
|
||||
return {"message": "Message sent"}
|
||||
16
docs/src/events/tutorial001.py
Normal file
16
docs/src/events/tutorial001.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
items = {}
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
items["foo"] = {"name": "Fighters"}
|
||||
items["bar"] = {"name": "Tenders"}
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def read_item(item_id: str):
|
||||
return items[item_id]
|
||||
14
docs/src/events/tutorial002.py
Normal file
14
docs/src/events/tutorial002.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
def startup_event():
|
||||
with open("log.txt", mode="a") as log:
|
||||
log.write("Application shutdown")
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items():
|
||||
return [{"name": "Foo"}]
|
||||
160
docs/tutorial/async-sql-databases.md
Normal file
160
docs/tutorial/async-sql-databases.md
Normal file
@@ -0,0 +1,160 @@
|
||||
You can also use <a href="https://github.com/encode/databases" target="_blank">`encode/databases`</a> with **FastAPI** to connect to databases using `async` and `await`.
|
||||
|
||||
It is compatible with:
|
||||
|
||||
* PostgreSQL
|
||||
* MySQL
|
||||
* SQLite
|
||||
|
||||
In this example, we'll use **SQLite**, because it uses a single file and Python has integrated support. So, you can copy this example and run it as is.
|
||||
|
||||
Later, for your production application, you might want to use a database server like **PostgreSQL**.
|
||||
|
||||
!!! tip
|
||||
You could adopt ideas from the previous section about <a href="/tutorial/sql-databases/" target="_blank">SQLAlchemy ORM</a>, like using utility functions to perform operations in the database, independent of your **FastAPI** code.
|
||||
|
||||
This section doesn't apply those ideas, to be equivalent to the counterpart in <a href="https://www.starlette.io/database/" target="_blank">Starlette</a>.
|
||||
|
||||
## Import and set up `SQLAlchemy`
|
||||
|
||||
* Import `SQLAlchemy`.
|
||||
* Create a `metadata` object.
|
||||
* Create a table `notes` using the `metadata` object.
|
||||
|
||||
```Python hl_lines="4 14 16 17 18 19 20 21 22"
|
||||
{!./src/async_sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Notice that all this code is pure SQLAlchemy Core.
|
||||
|
||||
`databases` is not doing anything here yet.
|
||||
|
||||
## Import and set up `databases`
|
||||
|
||||
* Import `databases`.
|
||||
* Create a `DATABASE_URL`.
|
||||
* Create a `database` object.
|
||||
|
||||
```Python hl_lines="3 9 12"
|
||||
{!./src/async_sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! tip
|
||||
If you where connecting to a different database (e.g. PostgreSQL), you would need to change the `DATABASE_URL`.
|
||||
|
||||
## Create the tables
|
||||
|
||||
In this case, we are creating the tables in the same Python file, but in production, you would probably want to create them with Alembic, integrated with migrations, etc.
|
||||
|
||||
Here, this section would run directly, right before starting your **FastAPI** application.
|
||||
|
||||
* Create an `engine`.
|
||||
* Create all the tables from the `metadata` object.
|
||||
|
||||
```Python hl_lines="25 26 27 28"
|
||||
{!./src/async_sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Create models
|
||||
|
||||
Create Pydantic models for:
|
||||
|
||||
* Notes to be created (`NoteIn`).
|
||||
* Notes to be returned (`Note`).
|
||||
|
||||
```Python hl_lines="31 32 33 36 37 38 39"
|
||||
{!./src/async_sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
By creating these Pydantic models, the input data will be validated, serialized (converted), and annotated (documented).
|
||||
|
||||
So, you will be able to see it all in the interactive API docs.
|
||||
|
||||
## Connect and disconnect
|
||||
|
||||
* Create your `FastAPI` application.
|
||||
* Create event handlers to connect and disconnect from the database.
|
||||
|
||||
```Python hl_lines="42 45 46 47 50 51 52"
|
||||
{!./src/async_sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Read notes
|
||||
|
||||
Create the *path operation function* to read notes:
|
||||
|
||||
```Python hl_lines="55 56 57 58"
|
||||
{!./src/async_sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Notice that as we communicate with the database using `await`, the *path operation function* is declared with `async`.
|
||||
|
||||
### Notice the `response_model=List[Note]`
|
||||
|
||||
It uses `typing.List`.
|
||||
|
||||
That documents (and validates, serializes, filters) the output data, as a `list` of `Note`s.
|
||||
|
||||
## Create notes
|
||||
|
||||
Create the *path operation function* to create notes:
|
||||
|
||||
```Python hl_lines="61 62 63 64 65"
|
||||
{!./src/async_sql_databases/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Notice that as we communicate with the database using `await`, the *path operation function* is declared with `async`.
|
||||
|
||||
### About `{**note.dict(), "id": last_record_id}`
|
||||
|
||||
`note` is a Pydantic `Note` object.
|
||||
|
||||
`note.dict()` returns a `dict` with its data, something like:
|
||||
|
||||
```Python
|
||||
{
|
||||
"text": "Some note",
|
||||
"completed": False,
|
||||
}
|
||||
```
|
||||
|
||||
but it doesn't have the `id` field.
|
||||
|
||||
So we create a new `dict`, that contains the key-value pairs from `note.dict()` with:
|
||||
|
||||
```Python
|
||||
{**note.dict()}
|
||||
```
|
||||
|
||||
`**note.dict()` "unpacks" the key value pairs directly, so, `{**note.dict()}` would be, more or less, a copy of `note.dict()`.
|
||||
|
||||
And then, we extend that copy `dict`, adding another key-value pair: `"id": last_record_id`:
|
||||
|
||||
```Python
|
||||
{**note.dict(), "id": last_record_id}
|
||||
```
|
||||
|
||||
So, the final result returned would be something like:
|
||||
|
||||
```Python
|
||||
{
|
||||
"id": 1,
|
||||
"text": "Some note",
|
||||
"completed": False,
|
||||
}
|
||||
```
|
||||
|
||||
## Check it
|
||||
|
||||
You can copy this code as is, and see the docs at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
|
||||
|
||||
There you can see all your API documented and interact with it:
|
||||
|
||||
<img src="/img/tutorial/async-sql-databases/image01.png">
|
||||
|
||||
## More info
|
||||
|
||||
You can read more about <a href="https://github.com/encode/databases" target="_blank">`encode/databases` at its GitHub page</a>.
|
||||
86
docs/tutorial/background-tasks.md
Normal file
86
docs/tutorial/background-tasks.md
Normal file
@@ -0,0 +1,86 @@
|
||||
You can define background tasks to be run *after* returning a response.
|
||||
|
||||
This is useful for operations that need to happen after a request, but that the client doesn't really have to be waiting for the operation to complete before receiving his response.
|
||||
|
||||
This includes, for example:
|
||||
|
||||
* Email notifications sent after performing an action:
|
||||
* As connecting to an email server and sending an email tends to be "slow" (several seconds), you can return the response right away and send the email notification in the background.
|
||||
* Processing data:
|
||||
* For example, let's say you receive a file that must go through a slow process, you can return a response of "Accepted" (HTTP 202) and process it in the background.
|
||||
|
||||
## Using `BackgroundTasks`
|
||||
|
||||
First, import `BackgroundTasks` and define a parameter in your *path operation function* with a type declaration of `BackgroundTasks`:
|
||||
|
||||
```Python hl_lines="1 13"
|
||||
{!./src/background_tasks/tutorial001.py!}
|
||||
```
|
||||
|
||||
**FastAPI** will create the object of type `BackgroundTasks` for you and pass it as that parameter.
|
||||
|
||||
!!! tip
|
||||
You declare a parameter of `BackgroundTasks` and use it in a very similar way as to when <a href="/tutorial/using-request-directly/" target="_blank">using the `Request` directly</a>.
|
||||
|
||||
|
||||
## Create a task function
|
||||
|
||||
Create a function to be run as the background task.
|
||||
|
||||
It is just a standard function that can receive parameters.
|
||||
|
||||
It can be an `async def` or normal `def` function, **FastAPI** will know how to handle it correctly.
|
||||
|
||||
In this case, the task function will write to a file (simulating sending an email).
|
||||
|
||||
And as the write operation doesn't use `async` and `await`, we define the function with normal `def`:
|
||||
|
||||
```Python hl_lines="6 7 8 9"
|
||||
{!./src/background_tasks/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Add the background task
|
||||
|
||||
Inside of your *path operation function*, pass your task function to the *background tasks* object with the method `.add_task()`:
|
||||
|
||||
```Python hl_lines="14"
|
||||
{!./src/background_tasks/tutorial001.py!}
|
||||
```
|
||||
|
||||
`.add_task()` receives as arguments:
|
||||
|
||||
* A task function to be run in the background (`write_notification`).
|
||||
* Any sequence of arguments that should be passed to the task function in order (`email`).
|
||||
* Any keyword arguments that should be passed to the task function (`message="some notification"`).
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
Using `BackgroundTasks` also works with the dependency injection system, you can declare a parameter of type `BackgroundTasks` at multiple levels: in a *path operation function*, in a dependency (dependable), in a sub-dependency, etc.
|
||||
|
||||
**FastAPI** knows what to do in each case and how to re-use the same object, so that all the background tasks are merged together and are run in the background afterwards:
|
||||
|
||||
```Python hl_lines="11 14 20 23"
|
||||
{!./src/background_tasks/tutorial002.py!}
|
||||
```
|
||||
|
||||
In this example, the messages will be written to the `log.txt` file *after* the response is sent.
|
||||
|
||||
If there was a query in the request, it will be written to the log in a background task.
|
||||
|
||||
And then another background task generated at the *path operation function* will write a message using the `email` path parameter.
|
||||
|
||||
## Technical Details
|
||||
|
||||
The class `BackgroundTasks` comes directly from <a href="https://www.starlette.io/background/" target="_blank">`starlette.background`</a>.
|
||||
|
||||
It is imported/included directly into FastAPI so that you can import it from `fastapi` and avoid accidentally importing the alternative `BackgroundTask` (without the `s` at the end) from `starlette.background`.
|
||||
|
||||
By only using `BackgroundTasks` (and not `BackgroundTask`), it's then possible to use it as a *path operation function* parameter and have **FastAPI** handle the rest for you, just like when using the `Request` object directly.
|
||||
|
||||
It's still possible to use `BackgroundTask` alone in FastAPI, but you have to create the object in your code and return a Starlette `Response` including it.
|
||||
|
||||
You can see more details in <a href="https://www.starlette.io/background/" target="_blank">Starlette's official docs for Background Tasks</a>.
|
||||
|
||||
## Recap
|
||||
|
||||
Import and use `BackgroundTasks` with parameters in *path operation functions* and dependencies to add background tasks.
|
||||
43
docs/tutorial/events.md
Normal file
43
docs/tutorial/events.md
Normal file
@@ -0,0 +1,43 @@
|
||||
|
||||
You can define event handlers (functions) that need to be executed before the application starts up, or when the application is shutting down.
|
||||
|
||||
These functions can be declared with `async def` or normal `def`.
|
||||
|
||||
## `startup` event
|
||||
|
||||
To add a function that should be run before the application starts, declare it with the event `"startup"`:
|
||||
|
||||
```Python hl_lines="8"
|
||||
{!./src/events/tutorial001.py!}
|
||||
```
|
||||
|
||||
In this case, the `startup` event handler function will initialize the items "database" (just a `dict`) with some values.
|
||||
|
||||
You can add more than one event handler function.
|
||||
|
||||
And your application won't start receiving requests until all the `startup` event handlers have completed.
|
||||
|
||||
## `shutdown` event
|
||||
|
||||
To add a function that should be run when the application is shutting down, declare it with the event `"shutdown"`:
|
||||
|
||||
```Python hl_lines="6"
|
||||
{!./src/events/tutorial002.py!}
|
||||
```
|
||||
|
||||
Here, the `shutdown` event handler function will write a text line `"Application shutdown"` to a file `log.txt`.
|
||||
|
||||
!!! info
|
||||
In the `open()` function, the `mode="a"` means "append", so, the line will be added after whatever is on that file, without overwriting the previous contents.
|
||||
|
||||
!!! tip
|
||||
Notice that in this case we are using a standard Python `open()` function that interacts with a file.
|
||||
|
||||
So, it involves I/O (input/output), that requires "waiting" for things to be written to disk.
|
||||
|
||||
But `open()` doesn't use `async` and `await`.
|
||||
|
||||
So, we declare the event handler function with standard `def` instead of `async def`.
|
||||
|
||||
!!! info
|
||||
You can read more about these event handlers in <a href="https://www.starlette.io/events/" target="_blank">Starlette's Events' docs</a>.
|
||||
@@ -1,6 +1,8 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.9.1"
|
||||
__version__ = "0.10.1"
|
||||
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
from .applications import FastAPI
|
||||
from .datastructures import UploadFile
|
||||
|
||||
@@ -26,6 +26,7 @@ class Dependant:
|
||||
name: str = None,
|
||||
call: Callable = None,
|
||||
request_param_name: str = None,
|
||||
background_tasks_param_name: str = None,
|
||||
) -> None:
|
||||
self.path_params = path_params or []
|
||||
self.query_params = query_params or []
|
||||
@@ -35,5 +36,6 @@ class Dependant:
|
||||
self.dependencies = dependencies or []
|
||||
self.security_requirements = security_schemes or []
|
||||
self.request_param_name = request_param_name
|
||||
self.background_tasks_param_name = background_tasks_param_name
|
||||
self.name = name
|
||||
self.call = call
|
||||
|
||||
@@ -3,7 +3,18 @@ import inspect
|
||||
from copy import deepcopy
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import Any, Callable, Dict, List, Mapping, Sequence, Tuple, Type, Union
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import params
|
||||
@@ -16,6 +27,7 @@ from pydantic.errors import MissingError
|
||||
from pydantic.fields import Field, Required, Shape
|
||||
from pydantic.schema import get_annotation_from_schema
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from starlette.background import BackgroundTasks
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.datastructures import UploadFile
|
||||
from starlette.requests import Headers, QueryParams, Request
|
||||
@@ -125,6 +137,8 @@ def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant:
|
||||
)
|
||||
elif lenient_issubclass(param.annotation, Request):
|
||||
dependant.request_param_name = param_name
|
||||
elif lenient_issubclass(param.annotation, BackgroundTasks):
|
||||
dependant.background_tasks_param_name = param_name
|
||||
elif not isinstance(param.default, params.Depends):
|
||||
add_param_to_body_fields(param=param, dependant=dependant)
|
||||
return dependant
|
||||
@@ -215,13 +229,20 @@ def is_coroutine_callable(call: Callable) -> bool:
|
||||
|
||||
|
||||
async def solve_dependencies(
|
||||
*, request: Request, dependant: Dependant, body: Dict[str, Any] = None
|
||||
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
|
||||
*,
|
||||
request: Request,
|
||||
dependant: Dependant,
|
||||
body: Dict[str, Any] = None,
|
||||
background_tasks: BackgroundTasks = None,
|
||||
) -> Tuple[Dict[str, Any], List[ErrorWrapper], Optional[BackgroundTasks]]:
|
||||
values: Dict[str, Any] = {}
|
||||
errors: List[ErrorWrapper] = []
|
||||
for sub_dependant in dependant.dependencies:
|
||||
sub_values, sub_errors = await solve_dependencies(
|
||||
request=request, dependant=sub_dependant, body=body
|
||||
sub_values, sub_errors, background_tasks = await solve_dependencies(
|
||||
request=request,
|
||||
dependant=sub_dependant,
|
||||
body=body,
|
||||
background_tasks=background_tasks,
|
||||
)
|
||||
if sub_errors:
|
||||
errors.extend(sub_errors)
|
||||
@@ -258,7 +279,11 @@ async def solve_dependencies(
|
||||
errors.extend(body_errors)
|
||||
if dependant.request_param_name:
|
||||
values[dependant.request_param_name] = request
|
||||
return values, errors
|
||||
if dependant.background_tasks_param_name:
|
||||
if background_tasks is None:
|
||||
background_tasks = BackgroundTasks()
|
||||
values[dependant.background_tasks_param_name] = background_tasks
|
||||
return values, errors, background_tasks
|
||||
|
||||
|
||||
def request_params_to_args(
|
||||
|
||||
@@ -68,7 +68,7 @@ def get_app(
|
||||
raise HTTPException(
|
||||
status_code=400, detail="There was an error parsing the body"
|
||||
)
|
||||
values, errors = await solve_dependencies(
|
||||
values, errors, background_tasks = await solve_dependencies(
|
||||
request=request, dependant=dependant, body=body
|
||||
)
|
||||
if errors:
|
||||
@@ -83,11 +83,17 @@ def get_app(
|
||||
else:
|
||||
raw_response = await run_in_threadpool(dependant.call, **values)
|
||||
if isinstance(raw_response, Response):
|
||||
if raw_response.background is None:
|
||||
raw_response.background = background_tasks
|
||||
return raw_response
|
||||
response_data = serialize_response(
|
||||
field=response_field, response=raw_response
|
||||
)
|
||||
return content_type(content=response_data, status_code=status_code)
|
||||
return content_type(
|
||||
content=response_data,
|
||||
status_code=status_code,
|
||||
background=background_tasks,
|
||||
)
|
||||
|
||||
return app
|
||||
|
||||
@@ -271,6 +277,10 @@ class APIRouter(routing.Router):
|
||||
include_in_schema=route.include_in_schema,
|
||||
name=route.name,
|
||||
)
|
||||
elif isinstance(route, routing.WebSocketRoute):
|
||||
self.add_websocket_route(
|
||||
prefix + route.path, route.endpoint, name=route.name
|
||||
)
|
||||
|
||||
def get(
|
||||
self,
|
||||
|
||||
@@ -57,12 +57,15 @@ nav:
|
||||
- OAuth2 with Password (and hashing), Bearer with JWT tokens: 'tutorial/security/oauth2-jwt.md'
|
||||
- Using the Request Directly: 'tutorial/using-request-directly.md'
|
||||
- SQL (Relational) Databases: 'tutorial/sql-databases.md'
|
||||
- Async SQL (Relational) Databases: 'tutorial/async-sql-databases.md'
|
||||
- NoSQL (Distributed / Big Data) Databases: 'tutorial/nosql-databases.md'
|
||||
- Bigger Applications - Multiple Files: 'tutorial/bigger-applications.md'
|
||||
- Background Tasks: 'tutorial/background-tasks.md'
|
||||
- Sub Applications - Behind a Proxy: 'tutorial/sub-applications-proxy.md'
|
||||
- Application Configuration: 'tutorial/application-configuration.md'
|
||||
- GraphQL: 'tutorial/graphql.md'
|
||||
- WebSockets: 'tutorial/websockets.md'
|
||||
- 'Events: startup - shutdown': 'tutorial/events.md'
|
||||
- Debugging: 'tutorial/debugging.md'
|
||||
- Concurrency and async / await: 'async.md'
|
||||
- Deployment: 'deployment.md'
|
||||
|
||||
@@ -37,7 +37,8 @@ test = [
|
||||
"isort",
|
||||
"requests",
|
||||
"email_validator",
|
||||
"sqlalchemy"
|
||||
"sqlalchemy",
|
||||
"databases[sqlite]",
|
||||
]
|
||||
doc = [
|
||||
"mkdocs",
|
||||
|
||||
131
tests/test_tutorial/test_async_sql_databases/test_tutorial001.py
Normal file
131
tests/test_tutorial/test_async_sql_databases/test_tutorial001.py
Normal file
@@ -0,0 +1,131 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from async_sql_databases.tutorial001 import app
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/notes/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "Response_Read_Notes",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/Note"},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"summary": "Read Notes Get",
|
||||
"operationId": "read_notes_notes__get",
|
||||
},
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Note"}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Create Note Post",
|
||||
"operationId": "create_note_notes__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/NoteIn"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"NoteIn": {
|
||||
"title": "NoteIn",
|
||||
"required": ["text", "completed"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {"title": "Text", "type": "string"},
|
||||
"completed": {"title": "Completed", "type": "boolean"},
|
||||
},
|
||||
},
|
||||
"Note": {
|
||||
"title": "Note",
|
||||
"required": ["id", "text", "completed"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"title": "Id", "type": "integer"},
|
||||
"text": {"title": "Text", "type": "string"},
|
||||
"completed": {"title": "Completed", "type": "boolean"},
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_create_read():
|
||||
with TestClient(app) as client:
|
||||
note = {"text": "Foo bar", "completed": False}
|
||||
response = client.post("/notes/", json=note)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["text"] == note["text"]
|
||||
assert data["completed"] == note["completed"]
|
||||
assert "id" in data
|
||||
response = client.get(f"/notes/")
|
||||
assert response.status_code == 200
|
||||
assert data in response.json()
|
||||
@@ -0,0 +1,19 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from background_tasks.tutorial001 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test():
|
||||
log = Path("log.txt")
|
||||
if log.is_file():
|
||||
os.remove(log) # pragma: no cover
|
||||
response = client.post("/send-notification/foo@example.com")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Notification sent in the background"}
|
||||
with open("./log.txt") as f:
|
||||
assert "notification for foo@example.com: some notification" in f.read()
|
||||
@@ -0,0 +1,19 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from background_tasks.tutorial002 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test():
|
||||
log = Path("log.txt")
|
||||
if log.is_file():
|
||||
os.remove(log) # pragma: no cover
|
||||
response = client.post("/send-notification/foo@example.com?q=some-query")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Message sent"}
|
||||
with open("./log.txt") as f:
|
||||
assert "found query: some-query\nmessage to foo@example.com" in f.read()
|
||||
0
tests/test_tutorial/test_events/__init__.py
Normal file
0
tests/test_tutorial/test_events/__init__.py
Normal file
79
tests/test_tutorial/test_events/test_tutorial001.py
Normal file
79
tests/test_tutorial/test_events/test_tutorial001.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from events.tutorial001 import app
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/{item_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read Item Get",
|
||||
"operationId": "read_item_items__item_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_events():
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
response = client.get("/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"name": "Fighters"}
|
||||
34
tests/test_tutorial/test_events/test_tutorial002.py
Normal file
34
tests/test_tutorial/test_events/test_tutorial002.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from events.tutorial002 import app
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Items Get",
|
||||
"operationId": "read_items_items__get",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_events():
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
response = client.get("/items/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"name": "Foo"}]
|
||||
with open("log.txt") as log:
|
||||
assert "Application shutdown" in log.read()
|
||||
53
tests/test_ws_router.py
Normal file
53
tests/test_ws_router.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
from starlette.websockets import WebSocket
|
||||
|
||||
router = APIRouter()
|
||||
prefix_router = APIRouter()
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.websocket_route("/")
|
||||
async def index(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
await websocket.send_text("Hello, world!")
|
||||
await websocket.close()
|
||||
|
||||
|
||||
@router.websocket_route("/router")
|
||||
async def routerindex(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
await websocket.send_text("Hello, router!")
|
||||
await websocket.close()
|
||||
|
||||
|
||||
@prefix_router.websocket_route("/")
|
||||
async def routerprefixindex(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
await websocket.send_text("Hello, router with prefix!")
|
||||
await websocket.close()
|
||||
|
||||
|
||||
app.include_router(router)
|
||||
app.include_router(prefix_router, prefix="/prefix")
|
||||
|
||||
|
||||
def test_app():
|
||||
client = TestClient(app)
|
||||
with client.websocket_connect("/") as websocket:
|
||||
data = websocket.receive_text()
|
||||
assert data == "Hello, world!"
|
||||
|
||||
|
||||
def test_router():
|
||||
client = TestClient(app)
|
||||
with client.websocket_connect("/router") as websocket:
|
||||
data = websocket.receive_text()
|
||||
assert data == "Hello, router!"
|
||||
|
||||
|
||||
def test_prefix_router():
|
||||
client = TestClient(app)
|
||||
with client.websocket_connect("/prefix/") as websocket:
|
||||
data = websocket.receive_text()
|
||||
assert data == "Hello, router with prefix!"
|
||||
Reference in New Issue
Block a user