Skip to content

Commit 0c611a3

Browse files
authored
Switch to sqlalchemy (#2)
* feat: Activated alembic with async sqlalchemy * fix: added alembic.ini * fix: added base song model * feat: changes initial service startup to async sql driver * feat: assembled first asyncio alchemy query * feat: added UserRead model * feat: restructured tables using sqlalchemy * feat: restructured tag creations * feat: restructured city creations * feat: restructured get cities query * feat: added many to many adding boilerplate * fix: added inner join for city * feat: first asyncio test * feat: used pytest_asyncio * feat: configured test database * fix: changed auth module to fully async * fix: changed celery task to fully async * doc: updated README
1 parent d3cd0e6 commit 0c611a3

29 files changed

+604
-698
lines changed

.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
POSTGRES_DB=foo
22
POSTGRES_USER=payam
33
POSTGRES_PASSWORD=payam
4-
DATABASE_URL=postgresql://payam:payam@pdb:5432/foo
4+
DATABASE_URL=postgresql+asyncpg://payam:payam@pdb:5432/foo
55
# Redis
66
REDIS_HOST=redis
77
REDIS_PORT=6379

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,5 @@ logs*
4343
.vscode/
4444
celerybeat-schedule.bak
4545
celerybeat-schedule.dat
46-
celerybeat-schedule.dir
46+
celerybeat-schedule.dir
47+
.database.db

Dockerfile

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
FROM python:3.11-slim
33

44
# set working directory
5-
WORKDIR /app/
5+
WORKDIR /files/
66

77
# set environment variables
88
ENV PYTHONDONTWRITEBYTECODE 1
99
ENV PYTHONUNBUFFERED 1
1010

1111
# Install dependencies
1212
RUN apt-get update && \
13+
apt-get install -y dos2unix &&\
1314
apt-get install -y --no-install-recommends curl && \
1415
curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python && \
1516
ln -s /opt/poetry/bin/poetry /usr/local/bin/poetry && \
@@ -19,7 +20,7 @@ RUN apt-get update && \
1920
rm -rf /var/lib/apt/lists/*
2021

2122
# Copy only the necessary files for dependency installation
22-
COPY pyproject.toml poetry.lock* /app/
23+
COPY pyproject.toml poetry.lock* /files/
2324

2425
# Allow installing dev dependencies to run tests
2526
ARG INSTALL_DEV=false
@@ -29,7 +30,8 @@ RUN if [ "$INSTALL_DEV" = "true" ] ; then poetry install --no-root ; else poetry
2930
COPY . .
3031

3132
# set executable permissions in a single RUN command
32-
RUN chmod +x /app/scripts/start.sh /app/scripts/prestart.sh
33+
RUN chmod +x /files/scripts/start.sh /files/scripts/prestart.sh
34+
RUN dos2unix /files/scripts/start.sh /files/scripts/prestart.sh
3335

3436
# define the command to run the application
35-
CMD ["/app/scripts/start.sh"]
37+
CMD ["/files/scripts/start.sh"]

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# FastAPI + SQLModel + Alembic + Celery + MongoDB + Redis + jwt Auth
22

3-
This project is an opinionated boilerplate for **FastAPI** micro framework that uses **_SQLAlchemy_**,
4-
_**SQLModel**_, **_PostgresSQL_**, _**Alembic**_, **_Celery_**, **_MongoDB_**, _**Redis**_, **_Docker_** and *
5-
*_jwt Authentication_**. You can use this ready to
3+
This project is an opinionated boilerplate for **FastAPI** micro framework that uses,
4+
_**Asynchronous SQLAlchemy**_, **_PostgresSQL_**, _**Alembic**_, **_Celery_**, **_MongoDB_**, _**Redis**_, **_Docker_** and **_jwt Authentication_**. You can use this ready to
65
use sample and don't worry about CI pipelines and running database migrations and tests inside a FastAPI project.
76

87
## Add new tables to PostgresSQL database :
@@ -22,13 +21,16 @@ Create `__init__.py` file and a empty `models.py` file inside folder
2221
and paste this sample content inside `models.py` file:
2322

2423
```python
25-
from sqlmodel import Field, SQLModel
24+
from sqlalchemy import Column, Integer, String
25+
from sqlalchemy.orm import Mapped, declarative_base
2626
27+
Base = declarative_base()
2728
28-
class Artist(SQLModel, table=True):
29-
id: int = Field(default=None, nullable=False, primary_key=True)
30-
name: str
31-
city: str
29+
30+
class Artist(Base):
31+
id: Mapped[int] = Column(Integer, primary_key=True)
32+
name: Mapped[str] = Column(String, primary_key=True)
33+
city: Mapped[str] = Column(String, primary_key=True)
3234
```
3335

3436
go to `migrations/env.py` folder in root directory and add this content to it:

alembic.ini

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[alembic]
44
# path to migration scripts
5-
script_location = migrations
5+
script_location = alembic
66

77
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
88
# Uncomment the line below if you want the files to be prepended with date and time
@@ -14,9 +14,9 @@ prepend_sys_path = .
1414

1515
# timezone to use when rendering the date within the migration file
1616
# as well as the filename.
17-
# If specified, requires the python-dateutil library that can be
18-
# installed by adding `alembic[tz]` to the pip requirements
19-
# string value is passed to dateutil.tz.gettz()
17+
# If specified, requires the python>=3.9 or backports.zoneinfo library.
18+
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
19+
# string value is passed to ZoneInfo()
2020
# leave blank for localtime
2121
# timezone =
2222

@@ -34,10 +34,10 @@ prepend_sys_path = .
3434
# sourceless = false
3535

3636
# version location specification; This defaults
37-
# to migrations/versions. When using multiple version
37+
# to alembic/versions. When using multiple version
3838
# directories, initial revisions must be specified with --version-path.
3939
# The path separator used here should be the separator specified by "version_path_separator" below.
40-
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
40+
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
4141

4242
# version path separator; As mentioned above, this is the character used to split
4343
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
@@ -58,9 +58,8 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
5858
# are written from script.py.mako
5959
# output_encoding = utf-8
6060

61+
# sqlalchemy.url = driver://user:pass@localhost/dbname
6162

62-
#sqlalchemy.url = sqlite+aiosqlite:///database.db
63-
#sqlalchemy.url = postgresql+asyncpg://payam:payam@pdb:5432/foo
6463

6564
[post_write_hooks]
6665
# post_write_hooks defines scripts or Python functions that are run
@@ -73,6 +72,12 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
7372
# black.entrypoint = black
7473
# black.options = -l 79 REVISION_SCRIPT_FILENAME
7574

75+
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
76+
# hooks = ruff
77+
# ruff.type = exec
78+
# ruff.executable = %(here)s/.venv/bin/ruff
79+
# ruff.options = --fix REVISION_SCRIPT_FILENAME
80+
7681
# Logging configuration
7782
[loggers]
7883
keys = root,sqlalchemy,alembic

alembic/README

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Generic single-database configuration with an async dbapi.

migrations/env.py renamed to alembic/env.py

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
1+
import asyncio
12
import os
23
from logging.config import fileConfig
34

5+
from sqlalchemy import pool
6+
from sqlalchemy.engine import Connection
7+
from sqlalchemy.ext.asyncio import async_engine_from_config
8+
49
from alembic import context
5-
from sqlalchemy import engine_from_config, pool
6-
from sqlmodel import SQLModel # NEW
7-
from app.auth.models import User # NEW
8-
from app.songs.models import Song, Tag, SongTag # NEW
10+
from app.db import Base
11+
from app.auth.models import User # New
12+
from app.songs.models import Song, City, Tag, SongTag # New
913

1014
# this is the Alembic Config object, which provides
1115
# access to the values within the .ini file in use.
1216
config = context.config
1317
config.set_main_option(
14-
"sqlalchemy.url", os.environ.get("DATABASE_URL") or "sqlite:///database.db"
15-
) # NEW
16-
# config.set_main_option('sqlalchemy.url', "sqlite:///database.db") # NEW
17-
18-
19-
# sqlite+aiosqlite:///database.db
18+
"sqlalchemy.url", os.environ.get("DATABASE_URL") or "sqlite+aiosqlite:///database.db"
19+
)
2020
# Interpret the config file for Python logging.
2121
# This line sets up loggers basically.
2222
if config.config_file_name is not None:
@@ -26,8 +26,7 @@
2626
# for 'autogenerate' support
2727
# from myapp import mymodel
2828
# target_metadata = mymodel.Base.metadata
29-
# target_metadata = None
30-
target_metadata = SQLModel.metadata # NEW
29+
target_metadata = Base.metadata
3130

3231

3332
# other values from the config, defined by the needs of env.py,
@@ -48,9 +47,9 @@ def run_migrations_offline() -> None:
4847
script output.
4948
5049
"""
51-
# url = config.get_main_option("sqlalchemy.url") #NEW
50+
url = config.get_main_option("sqlalchemy.url")
5251
context.configure(
53-
# url=url, #NEW
52+
url=url,
5453
target_metadata=target_metadata,
5554
literal_binds=True,
5655
dialect_opts={"paramstyle": "named"},
@@ -60,24 +59,35 @@ def run_migrations_offline() -> None:
6059
context.run_migrations()
6160

6261

63-
def run_migrations_online() -> None:
64-
"""Run migrations in 'online' mode.
62+
def do_run_migrations(connection: Connection) -> None:
63+
context.configure(connection=connection, target_metadata=target_metadata)
64+
65+
with context.begin_transaction():
66+
context.run_migrations()
6567

66-
In this scenario we need to create an Engine
68+
69+
async def run_async_migrations() -> None:
70+
"""In this scenario we need to create an Engine
6771
and associate a connection with the context.
6872
6973
"""
70-
connectable = engine_from_config(
74+
75+
connectable = async_engine_from_config(
7176
config.get_section(config.config_ini_section, {}),
7277
prefix="sqlalchemy.",
7378
poolclass=pool.NullPool,
7479
)
7580

76-
with connectable.connect() as connection:
77-
context.configure(connection=connection, target_metadata=target_metadata)
81+
async with connectable.connect() as connection:
82+
await connection.run_sync(do_run_migrations)
83+
84+
await connectable.dispose()
85+
86+
87+
def run_migrations_online() -> None:
88+
"""Run migrations in 'online' mode."""
7889

79-
with context.begin_transaction():
80-
context.run_migrations()
90+
asyncio.run(run_async_migrations())
8191

8292

8393
if context.is_offline_mode():
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ from typing import Sequence, Union
99

1010
from alembic import op
1111
import sqlalchemy as sa
12-
import sqlmodel # NEW
1312
${imports if imports else ""}
1413

1514
# revision identifiers, used by Alembic.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""init
2+
3+
Revision ID: 180cc3782c77
4+
Revises:
5+
Create Date: 2024-03-07 18:57:29.839982
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = '180cc3782c77'
16+
down_revision: Union[str, None] = None
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
# ### commands auto generated by Alembic - please adjust! ###
23+
op.create_table('cities',
24+
sa.Column('id', sa.Integer(), nullable=False),
25+
sa.Column('name', sa.String(), nullable=True),
26+
sa.PrimaryKeyConstraint('id')
27+
)
28+
op.create_table('tags',
29+
sa.Column('id', sa.Integer(), nullable=False),
30+
sa.Column('title', sa.String(), nullable=True),
31+
sa.Column('description', sa.String(), nullable=True),
32+
sa.PrimaryKeyConstraint('id')
33+
)
34+
op.create_table('users',
35+
sa.Column('id', sa.Integer(), nullable=False),
36+
sa.Column('username', sa.String(), nullable=True),
37+
sa.Column('email', sa.String(), nullable=True),
38+
sa.Column('full_name', sa.String(), nullable=True),
39+
sa.Column('password', sa.String(), nullable=True),
40+
sa.PrimaryKeyConstraint('id')
41+
)
42+
op.create_table('songs',
43+
sa.Column('id', sa.Integer(), nullable=False),
44+
sa.Column('name', sa.String(), nullable=True),
45+
sa.Column('artist', sa.String(), nullable=True),
46+
sa.Column('description', sa.String(), nullable=True),
47+
sa.Column('year', sa.Integer(), nullable=True),
48+
sa.Column('city_id', sa.Integer(), nullable=True),
49+
sa.ForeignKeyConstraint(['city_id'], ['cities.id'], ),
50+
sa.PrimaryKeyConstraint('id')
51+
)
52+
op.create_table('song_tag',
53+
sa.Column('song_id', sa.Integer(), nullable=False),
54+
sa.Column('tag_id', sa.Integer(), nullable=False),
55+
sa.ForeignKeyConstraint(['song_id'], ['songs.id'], ),
56+
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),
57+
sa.PrimaryKeyConstraint('song_id', 'tag_id')
58+
)
59+
# ### end Alembic commands ###
60+
61+
62+
def downgrade() -> None:
63+
# ### commands auto generated by Alembic - please adjust! ###
64+
op.drop_table('song_tag')
65+
op.drop_table('songs')
66+
op.drop_table('users')
67+
op.drop_table('tags')
68+
op.drop_table('cities')
69+
# ### end Alembic commands ###

0 commit comments

Comments
 (0)