Skip to content

More work on the docs page (still incomplete) #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions docs/explanation.md

This file was deleted.

152 changes: 152 additions & 0 deletions docs/explanation/database.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# The Database file (db.py)

The database setup is fairly straightforward, we will go through it line by
line.

## Imports

```python linenums="1"
"""Set up the database connection and session.""" ""
from collections.abc import AsyncGenerator
from typing import Any

from sqlalchemy import MetaData
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from sqlalchemy.orm import DeclarativeBase
```

Lines 1 to 11 are the imports. The only thing to note here is that we are using
the `AsyncGenerator` type hint for the `get_db` function. This is because we are
using the `yield` keyword in the function, which makes it a generator. The
`AsyncGenerator` type hint is a special type hint that is used for asynchronous
generators.

## Database Connection String

```python linenums="13"
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost/postgres"
# DATABASE_URL = "sqlite+aiosqlite:///./test.db"
```

We set a variable to be used later which contains the database URL. We are using
PostgreSQL, but you can use any database that SQLAlchemy supports. The commented
out line is for SQLite, which is a good choice for testing. You can comment out
the PostgreSQL line (**13**) and uncomment the SQLite line (**14**) to use
SQLite instead.

This is a basic connection string, in reality you would want to use environment
variables to store the user/password and database name.

## The Base Class

```python linenums="20"
class Base(DeclarativeBase):
"""Base class for SQLAlchemy models.

All other models should inherit from this class.
"""

metadata = MetaData(
naming_convention={
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
)
```

This takes the `DeclarativeBase` class from SQLAlchemy and adds a `metadata`
attribute to it. This is used to define the naming convention for the database
tables. **This is not required**, but it is a good idea to set this up for
consistency.

We will use this class as the base class for all of our future models.

## The database engine and session

```python linenums="37"
async_engine = create_async_engine(DATABASE_URL, echo=False)
```

Here on line 37 we create the database engine. The `create_async_engine`
function takes the database URL and returns an engine, the connection to the
database. The `echo` parameter is set to `False` to prevent SQLAlchemy from
outputting all of the SQL commands it is running. Note that it uses the
`DATABASE_URL` variable we set earlier.

```python linenums="38"
async_session = async_sessionmaker(async_engine, expire_on_commit=False)
```

Next, we create the session. The `async_sessionmaker` function takes the engine
and returns a session. The `expire_on_commit` parameter is set to `False` to
prevent SQLAlchemy from expiring objects on commit. This is required for
`asyncpg` to work properly.

We will NOT use this session directly, instead we will use the `get_db` function
below to get and release a session.

## The `get_db()` function

```python linenums="41"
async def get_db() -> AsyncGenerator[AsyncSession, Any]:
"""Get a database session.

To be used for dependency injection.
"""
async with async_session() as session, session.begin():
yield session
```

This function is used to get a database session as a generator function. This
function is used for dependency injection, which is a fancy way of saying that
we will use it to pass the database session to other functions. Since we have
used the `with` statement, the session will be automatically closed (and data
comitted) when the function returns, usually after the related route is
complete.

!!! note
Note that in line **46** we are using a combined `with` statement. This
is a shortcut for using two nested `with` statements, one for the
`async_session` and one for the `session.begin()`.

## The `init_models()` function

This function is used to create the database tables. It is called by the
`lifespan()` function at startup.

!!! note
This function is only used in our demo, in a real application you would
use a migration tool like
[Alembic](https://alembic.sqlalchemy.org/en/latest/){:target="_blank"}
instead.

```python linenums="50"
async def init_models() -> None:
"""Create tables if they don't already exist.

In a real-life example we would use Alembic to manage migrations.
"""
async with async_engine.begin() as conn:
# await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
```

This function shows how to run a `syncronous` function in an `async` context
using the `async_engine` object directly instead of the `async_session` object.
On line **57** we use the `run_sync` method to run the `create_all` method of
the `Base.metadata` object (a syncronous function). This will create all of the
tables defined in the models.

If you want to drop the tables and recreate them every time the server restarts,
you can uncomment line **56**. This is obviously not much good for production
use, but it can be useful for testing.

Next, we will look at the models themselves and the Schemas used to validate
them within FastAPI.
31 changes: 31 additions & 0 deletions docs/explanation/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Introduction

This section will attempt to explain the code in this repository. It is not
meant to be a tutorial on how to use FastAPI or SQLAlchemy, but rather an
explanation of how to get the two to work together **Asynchronously**.

## Caveats

This is a very simple example of a REST API. It is not meant to be used in
production, it is meant to be a simple example of how to use FastAPI and
SQLAlchemy together **Asynchronously**. As such there are some things that you
would
not do in a production environment, such as:

- Using SQLite as the database
- Manual database migrations, instead of using a tool like Alembic
- No validation of the data being sent to the API
- No check for duplicate email addresses
- The code layout is not optimal. The relevant files are all in the root
directory, instead of being in a `src` directory or similar, and the routes
would be better in a separate file.

The above is not an exhaustive list!

## Relevant Files

- `main.py` - The main file that runs the program and contains the routes
- `db.py` - This file contains the database connection and functions
- `models.py` - This file contains the database models
- `schema.py` - This defines the Pydantic schemas for the models, used for
validation and serialization.
4 changes: 4 additions & 0 deletions docs/explanation/main.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The Main Application and Routes

!!! danger "To be Added"
This section is not yet written.
4 changes: 4 additions & 0 deletions docs/explanation/models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Models and Schemas

!!! danger "To be Added"
This section is not yet written.
45 changes: 33 additions & 12 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,42 @@

## Introduction

I've been using [FastAPI][fastapi]{:target="_blank"} and
This repository contains a very simple example how to use FastAPI with Async
SQLAlchemy 2.0, in `ORM` mode. I'll probably add an example for `Core` mode
also. No effort has been made to make this a production ready application, it's
just a simple demo since at the time of writing there were few clear examples of
how to do this.

Last update 29th January 2024, and tested to work with the following versions:

- Python 3.9+
- FastAPI 0.109.0
- SQLAlchemy 2.0.25

## Why use Raw SQLAlchemy?

I was using [FastAPI][fastapi]{:target="_blank"} and
[SQLAlchemy][sqla]{:target="_blank"} combined with
[encode/databases][databases]{:target="_blank"} for a while now.
[encode/databases][databases]{:target="_blank"} for a while. This worked fine
originally but I felt I needed a bit more control over the database session
management.

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

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

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

[fastapi]:https://fastapi.tiangolo.com/
[fastapi]: https://fastapi.tiangolo.com/
[sqla]: https://www.sqlalchemy.org/
[databases]:https://www.encode.io/databases/
[async-sqla]:https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html
[databases]: https://www.encode.io/databases/
[async-sqla]: https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html
[fastapi-template]: https://github.com/seapagan/fastapi-template
33 changes: 23 additions & 10 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@

## Installation

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

Install the dependencies:

```console
poetry install
uv sync
```

Then switch to the virtual environment:

```console
poetry shell
# On Linux:
source .venv/bin/activate

# On Windows:
.venv\Scripts\activate
```

## Usage
Expand All @@ -24,11 +30,18 @@ Run the server using `Uvicorn`:
uvicorn main:app --reload
```

> You can also run the server by just executing the `main.py` file:
>
> ```console
> python main.py
> ```
!!! note
You can also run the server by just executing the `main.py` file:

```console
python main.py
```

or using the included `POE` alias:

```console
poe serve
```

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

[poetry]: https://python-poetry.org/
[uv]: https://docs.astral.sh/uv/
[postgres]:https://www.postgresql.org/
[docker]:https://www.docker.com/
[sqlite]:https://www.sqlite.org/
Expand Down
13 changes: 11 additions & 2 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ theme:
features:
- navigation.footer
- navigation.expand
- navigation.prune
- content.code.copy
- content.code.annotate

extra:
social:
Expand All @@ -34,13 +37,19 @@ plugins:
markdown_extensions:
- admonition
- pymdownx.snippets
- pymdownx.superfences
- md_in_html
- pymdownx.highlight:
linenums: false
linenums: true
auto_title: false
- attr_list

nav:
- Home: index.md
- Usage: usage.md
- Code Description: explanation.md
- Code Description:
- Introduction: explanation/intro.md
- Database Setup: explanation/database.md
- Models and Schemas: explanation/models.md
- Application and Routes: explanation/main.md
- License: license.md
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ markdown.help = "Run markdown checks"
"docs:serve:all".cmd = "mkdocs serve"
"docs:serve:all".help = "Serve documentation locally on all interfaces"

# generate the CHANGELOG.md file
changelog.cmd = "github-changelog-md"
changelog.help = "Generate the CHANGELOG.md file"

[tool.pymarkdown]
plugins.md014.enabled = false
plugins.md046.enabled = false
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ github-changelog-md==0.9.5
greenlet==3.1.1
h11==0.16.0
htmlmin2==0.1.13
httpcore==1.0.7
httpcore==1.0.9
httptools==0.6.4
httpx==0.28.1
identify==2.6.9
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ fastapi==0.115.12
fastapi-cli==0.0.7
greenlet==3.1.1
h11==0.16.0
httpcore==1.0.7
httpcore==1.0.9
httptools==0.6.4
httpx==0.28.1
idna==3.10
Expand Down
Loading