Skip to content

Commit

Permalink
✨ Add docs and tests for encode/databases (#107)
Browse files Browse the repository at this point in the history
* ✨ Add docs and tests for encode/databases

* ➕ Add testing-only dependency, databases
  • Loading branch information
tiangolo committed Mar 25, 2019
1 parent 5a6e47b commit 1c2ecbb
Show file tree
Hide file tree
Showing 9 changed files with 431 additions and 20 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ uvicorn = "*"
[packages]
starlette = "==0.11.1"
pydantic = "==0.21.0"
databases = {extras = ["sqlite"],version = "*"}

[requires]
python_version = "3.6"
Expand Down
90 changes: 71 additions & 19 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 65 additions & 0 deletions docs/src/async_sql_databases/tutorial001.py
Original file line number Diff line number Diff line change
@@ -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}
160 changes: 160 additions & 0 deletions docs/tutorial/async-sql-databases.md
Original file line number Diff line number Diff line change
@@ -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>.

0 comments on commit 1c2ecbb

Please sign in to comment.