Skip to content

Commit 05b48e0

Browse files
authored
Merge pull request #7 from seapagan/complete-docs
Complete the docs
2 parents 173a94e + c102118 commit 05b48e0

12 files changed

+270
-36
lines changed

docs/explanation.md

-4
This file was deleted.

docs/explanation/database.md

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# The Database file (db.py)
2+
3+
The database setup is fairly straightforward, we will go through it line by
4+
line.
5+
6+
## Imports
7+
8+
```python linenums="1"
9+
"""Set up the database connection and session.""" ""
10+
from collections.abc import AsyncGenerator
11+
from typing import Any
12+
13+
from sqlalchemy import MetaData
14+
from sqlalchemy.ext.asyncio import (
15+
AsyncSession,
16+
async_sessionmaker,
17+
create_async_engine,
18+
)
19+
from sqlalchemy.orm import DeclarativeBase
20+
```
21+
22+
Lines 1 to 11 are the imports. The only thing to note here is that we are using
23+
the `AsyncGenerator` type hint for the `get_db` function. This is because we are
24+
using the `yield` keyword in the function, which makes it a generator. The
25+
`AsyncGenerator` type hint is a special type hint that is used for asynchronous
26+
generators.
27+
28+
## Database Connection String
29+
30+
```python linenums="13"
31+
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost/postgres"
32+
# DATABASE_URL = "sqlite+aiosqlite:///./test.db"
33+
```
34+
35+
We set a variable to be used later which contains the database URL. We are using
36+
PostgreSQL, but you can use any database that SQLAlchemy supports. The commented
37+
out line is for SQLite, which is a good choice for testing. You can comment out
38+
the PostgreSQL line (**13**) and uncomment the SQLite line (**14**) to use
39+
SQLite instead.
40+
41+
This is a basic connection string, in reality you would want to use environment
42+
variables to store the user/password and database name.
43+
44+
## The Base Class
45+
46+
```python linenums="20"
47+
class Base(DeclarativeBase):
48+
"""Base class for SQLAlchemy models.
49+
50+
All other models should inherit from this class.
51+
"""
52+
53+
metadata = MetaData(
54+
naming_convention={
55+
"ix": "ix_%(column_0_label)s",
56+
"uq": "uq_%(table_name)s_%(column_0_name)s",
57+
"ck": "ck_%(table_name)s_%(constraint_name)s",
58+
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
59+
"pk": "pk_%(table_name)s",
60+
}
61+
)
62+
```
63+
64+
This takes the `DeclarativeBase` class from SQLAlchemy and adds a `metadata`
65+
attribute to it. This is used to define the naming convention for the database
66+
tables. **This is not required**, but it is a good idea to set this up for
67+
consistency.
68+
69+
We will use this class as the base class for all of our future models.
70+
71+
## The database engine and session
72+
73+
```python linenums="37"
74+
async_engine = create_async_engine(DATABASE_URL, echo=False)
75+
```
76+
77+
Here on line 37 we create the database engine. The `create_async_engine`
78+
function takes the database URL and returns an engine, the connection to the
79+
database. The `echo` parameter is set to `False` to prevent SQLAlchemy from
80+
outputting all of the SQL commands it is running. Note that it uses the
81+
`DATABASE_URL` variable we set earlier.
82+
83+
```python linenums="38"
84+
async_session = async_sessionmaker(async_engine, expire_on_commit=False)
85+
```
86+
87+
Next, we create the session. The `async_sessionmaker` function takes the engine
88+
and returns a session. The `expire_on_commit` parameter is set to `False` to
89+
prevent SQLAlchemy from expiring objects on commit. This is required for
90+
`asyncpg` to work properly.
91+
92+
We will NOT use this session directly, instead we will use the `get_db` function
93+
below to get and release a session.
94+
95+
## The `get_db()` function
96+
97+
```python linenums="41"
98+
async def get_db() -> AsyncGenerator[AsyncSession, Any]:
99+
"""Get a database session.
100+
101+
To be used for dependency injection.
102+
"""
103+
async with async_session() as session, session.begin():
104+
yield session
105+
```
106+
107+
This function is used to get a database session as a generator function. This
108+
function is used for dependency injection, which is a fancy way of saying that
109+
we will use it to pass the database session to other functions. Since we have
110+
used the `with` statement, the session will be automatically closed (and data
111+
comitted) when the function returns, usually after the related route is
112+
complete.
113+
114+
!!! note
115+
Note that in line **46** we are using a combined `with` statement. This
116+
is a shortcut for using two nested `with` statements, one for the
117+
`async_session` and one for the `session.begin()`.
118+
119+
## The `init_models()` function
120+
121+
This function is used to create the database tables. It is called by the
122+
`lifespan()` function at startup.
123+
124+
!!! note
125+
This function is only used in our demo, in a real application you would
126+
use a migration tool like
127+
[Alembic](https://alembic.sqlalchemy.org/en/latest/){:target="_blank"}
128+
instead.
129+
130+
```python linenums="50"
131+
async def init_models() -> None:
132+
"""Create tables if they don't already exist.
133+
134+
In a real-life example we would use Alembic to manage migrations.
135+
"""
136+
async with async_engine.begin() as conn:
137+
# await conn.run_sync(Base.metadata.drop_all)
138+
await conn.run_sync(Base.metadata.create_all)
139+
```
140+
141+
This function shows how to run a `syncronous` function in an `async` context
142+
using the `async_engine` object directly instead of the `async_session` object.
143+
On line **57** we use the `run_sync` method to run the `create_all` method of
144+
the `Base.metadata` object (a syncronous function). This will create all of the
145+
tables defined in the models.
146+
147+
If you want to drop the tables and recreate them every time the server restarts,
148+
you can uncomment line **56**. This is obviously not much good for production
149+
use, but it can be useful for testing.
150+
151+
Next, we will look at the models themselves and the Schemas used to validate
152+
them within FastAPI.

docs/explanation/intro.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Introduction
2+
3+
This section will attempt to explain the code in this repository. It is not
4+
meant to be a tutorial on how to use FastAPI or SQLAlchemy, but rather an
5+
explanation of how to get the two to work together **Asynchronously**.
6+
7+
## Caveats
8+
9+
This is a very simple example of a REST API. It is not meant to be used in
10+
production, it is meant to be a simple example of how to use FastAPI and
11+
SQLAlchemy together **Asynchronously**. As such there are some things that you
12+
would
13+
not do in a production environment, such as:
14+
15+
- Using SQLite as the database
16+
- Manual database migrations, instead of using a tool like Alembic
17+
- No validation of the data being sent to the API
18+
- No check for duplicate email addresses
19+
- The code layout is not optimal. The relevant files are all in the root
20+
directory, instead of being in a `src` directory or similar, and the routes
21+
would be better in a separate file.
22+
23+
The above is not an exhaustive list!
24+
25+
## Relevant Files
26+
27+
- `main.py` - The main file that runs the program and contains the routes
28+
- `db.py` - This file contains the database connection and functions
29+
- `models.py` - This file contains the database models
30+
- `schema.py` - This defines the Pydantic schemas for the models, used for
31+
validation and serialization.

docs/explanation/main.md

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# The Main Application and Routes
2+
3+
!!! danger "To be Added"
4+
This section is not yet written.

docs/explanation/models.md

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Models and Schemas
2+
3+
!!! danger "To be Added"
4+
This section is not yet written.

docs/index.md

+33-12
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,42 @@
22

33
## Introduction
44

5-
I've been using [FastAPI][fastapi]{:target="_blank"} and
5+
This repository contains a very simple example how to use FastAPI with Async
6+
SQLAlchemy 2.0, in `ORM` mode. I'll probably add an example for `Core` mode
7+
also. No effort has been made to make this a production ready application, it's
8+
just a simple demo since at the time of writing there were few clear examples of
9+
how to do this.
10+
11+
Last update 29th January 2024, and tested to work with the following versions:
12+
13+
- Python 3.9+
14+
- FastAPI 0.109.0
15+
- SQLAlchemy 2.0.25
16+
17+
## Why use Raw SQLAlchemy?
18+
19+
I was using [FastAPI][fastapi]{:target="_blank"} and
620
[SQLAlchemy][sqla]{:target="_blank"} combined with
7-
[encode/databases][databases]{:target="_blank"} for a while now.
21+
[encode/databases][databases]{:target="_blank"} for a while. This worked fine
22+
originally but I felt I needed a bit more control over the database session
23+
management.
824

9-
The `databases` package is a great wrapper around `SQLAlchemy` that allows you
10-
to use async/await with SQLAlchemy.
25+
!!! info
26+
The [databases][databases]{:target="_blank"} package is a great wrapper
27+
around `SQLAlchemy` that allows you to use async/await for database
28+
operations. It also has a nice way of managing the database session, which
29+
is why I used it originally.
1130

12-
However, this does not seem be be actively maintained anymore. So I decided to
13-
give the new [Async SQLAlchemy][async-sqla]{:target="_blank"} a try instead.
31+
However, this did not seem be be actively maintained at the time, so I decided
32+
to give the newer [Async SQLAlchemy][async-sqla]{:target="_blank"} a try
33+
instead.
1434

15-
This repository contains a very simple example how to use FastAPI with Async
16-
SQLAlchemy 2.0, in `ORM` mode. I'll probably add an example for `Core` mode
17-
also.
35+
This repository is the result of my exprimentation while converting my
36+
[FastAPI-template][fastapi-template]{:target="_blank"} project to use `Async
37+
SQLAlchemy` instead of `databases`.
1838

19-
[fastapi]:https://fastapi.tiangolo.com/
39+
[fastapi]: https://fastapi.tiangolo.com/
2040
[sqla]: https://www.sqlalchemy.org/
21-
[databases]:https://www.encode.io/databases/
22-
[async-sqla]:https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html
41+
[databases]: https://www.encode.io/databases/
42+
[async-sqla]: https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html
43+
[fastapi-template]: https://github.com/seapagan/fastapi-template

docs/usage.md

+23-10
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,24 @@
22

33
## Installation
44

5-
Clone the repository from [here][repo]{:target="_blank"} and install the
6-
dependencies. This project uses [Poetry][poetry]{:target="_blank"} for
5+
Clone [this repository][repo]{:target="_blank"} and install the
6+
dependencies. This project uses [uv][uv]{:target="_blank"} for
77
dependency management which should be installed on your system first.
88

9+
Install the dependencies:
10+
911
```console
10-
poetry install
12+
uv sync
1113
```
1214

1315
Then switch to the virtual environment:
1416

1517
```console
16-
poetry shell
18+
# On Linux:
19+
source .venv/bin/activate
20+
21+
# On Windows:
22+
.venv\Scripts\activate
1723
```
1824

1925
## Usage
@@ -24,11 +30,18 @@ Run the server using `Uvicorn`:
2430
uvicorn main:app --reload
2531
```
2632

27-
> You can also run the server by just executing the `main.py` file:
28-
>
29-
> ```console
30-
> python main.py
31-
> ```
33+
!!! note
34+
You can also run the server by just executing the `main.py` file:
35+
36+
```console
37+
python main.py
38+
```
39+
40+
or using the included `POE` alias:
41+
42+
```console
43+
poe serve
44+
```
3245

3346
Then open your browser at
3447
[http://localhost:8000](http://localhost:8000){:target="_blank"}.
@@ -80,7 +93,7 @@ SQLite database.
8093
DATABASE_URL = "sqlite+aiosqlite:///./test.db"
8194
```
8295

83-
[poetry]: https://python-poetry.org/
96+
[uv]: https://docs.astral.sh/uv/
8497
[postgres]:https://www.postgresql.org/
8598
[docker]:https://www.docker.com/
8699
[sqlite]:https://www.sqlite.org/

mkdocs.yml

+11-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ theme:
1010
features:
1111
- navigation.footer
1212
- navigation.expand
13+
- navigation.prune
14+
- content.code.copy
15+
- content.code.annotate
1316

1417
extra:
1518
social:
@@ -34,13 +37,19 @@ plugins:
3437
markdown_extensions:
3538
- admonition
3639
- pymdownx.snippets
40+
- pymdownx.superfences
41+
- md_in_html
3742
- pymdownx.highlight:
38-
linenums: false
43+
linenums: true
3944
auto_title: false
4045
- attr_list
4146

4247
nav:
4348
- Home: index.md
4449
- Usage: usage.md
45-
- Code Description: explanation.md
50+
- Code Description:
51+
- Introduction: explanation/intro.md
52+
- Database Setup: explanation/database.md
53+
- Models and Schemas: explanation/models.md
54+
- Application and Routes: explanation/main.md
4655
- License: license.md

pyproject.toml

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ markdown.help = "Run markdown checks"
5757
"docs:serve:all".cmd = "mkdocs serve"
5858
"docs:serve:all".help = "Serve documentation locally on all interfaces"
5959

60+
# generate the CHANGELOG.md file
61+
changelog.cmd = "github-changelog-md"
62+
changelog.help = "Generate the CHANGELOG.md file"
63+
6064
[tool.pymarkdown]
6165
plugins.md014.enabled = false
6266
plugins.md046.enabled = false

requirements-dev.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ github-changelog-md==0.9.5
3131
greenlet==3.1.1
3232
h11==0.16.0
3333
htmlmin2==0.1.13
34-
httpcore==1.0.7
34+
httpcore==1.0.9
3535
httptools==0.6.4
3636
httpx==0.28.1
3737
identify==2.6.9

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ fastapi==0.115.12
1515
fastapi-cli==0.0.7
1616
greenlet==3.1.1
1717
h11==0.16.0
18-
httpcore==1.0.7
18+
httpcore==1.0.9
1919
httptools==0.6.4
2020
httpx==0.28.1
2121
idna==3.10

0 commit comments

Comments
 (0)