Skip to content

Commit db0a977

Browse files
committed
docs: add intro and database pages
Signed-off-by: Grant Ramsay <seapagan@gmail.com>
1 parent 173a94e commit db0a977

File tree

8 files changed

+217
-12
lines changed

8 files changed

+217
-12
lines changed

docs/explanation.md

Lines changed: 0 additions & 4 deletions
This file was deleted.

docs/explanation/database.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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 SQLite
39+
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) # noqa: ERA001
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+
Next, we will look at the models themselves and the Schemas used to validate
148+
them within FastAPI.

docs/explanation/intro.md

Lines changed: 31 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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/usage.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
## Installation
44

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

9+
Install the dependencies:
10+
911
```console
1012
poetry install
1113
```
@@ -24,11 +26,18 @@ Run the server using `Uvicorn`:
2426
uvicorn main:app --reload
2527
```
2628

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

3342
Then open your browser at
3443
[http://localhost:8000](http://localhost:8000){:target="_blank"}.

mkdocs.yml

Lines changed: 11 additions & 2 deletions
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

Lines changed: 4 additions & 0 deletions
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

0 commit comments

Comments
 (0)