Skip to content

Commit 62b691e

Browse files
committed
Add Alembic for database migrations.
1 parent ab2e03a commit 62b691e

14 files changed

+278
-24
lines changed

.pre-commit-config.yaml

-5
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,3 @@ repos:
3636
entry: bandit backend
3737
language: python
3838
pass_filenames: false
39-
- name: pydocstyle
40-
id: pydoctsyle
41-
entry: pydocstyle backend
42-
language: python
43-
pass_filenames: false

.pylintrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ extension-pkg-whitelist=
1515
fail-under=8.0
1616

1717
# Files or directories to be skipped. They should be base names, not paths.
18-
ignore=CVS
18+
ignore=CVS,versions
1919

2020
# Files or directories matching the regex patterns are skipped. The regex
2121
# matches against base names, not paths.

README.md

+19
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,22 @@ bash keycloak/init.sh
2323
In the output of the script you will find the client secret. Copy it and put it the `.env`.
2424

2525
You can then access the keycloak console and login with the admin credentials: http://localhost:8080
26+
27+
## Running database migrations using [Alembic](https://alembic.sqlalchemy.org).
28+
29+
First, modify your models in the file `database/models.py`.
30+
Then run the following command to generate the migration:
31+
32+
```bash
33+
docker-compose exec backend alembic revision --autogenerate -m "<Your message>"
34+
```
35+
36+
> :warning: Check this [page](https://alembic.sqlalchemy.org/en/latest/autogenerate.html) to see what alembic detects for the Autogenerate
37+
38+
Check the migration file in the `alembic/versions` folder. If you are happy with it, you can run the migration:
39+
40+
```bash
41+
docker-compose exec backend alembic upgrade head
42+
```
43+
44+
Your database is now up to date !

backend/alembic.ini

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# A generic, single database configuration.
2+
3+
[alembic]
4+
# path to migration scripts
5+
script_location = alembic
6+
7+
# template used to generate migration files
8+
# file_template = %%(rev)s_%%(slug)s
9+
10+
# sys.path path, will be prepended to sys.path if present.
11+
# defaults to the current working directory.
12+
prepend_sys_path = .
13+
14+
# timezone to use when rendering the date
15+
# within the migration file as well as the filename.
16+
# string value is passed to dateutil.tz.gettz()
17+
# leave blank for localtime
18+
# timezone =
19+
20+
# max length of characters to apply to the
21+
# "slug" field
22+
# truncate_slug_length = 40
23+
24+
# set to 'true' to run the environment during
25+
# the 'revision' command, regardless of autogenerate
26+
# revision_environment = false
27+
28+
# set to 'true' to allow .pyc and .pyo files without
29+
# a source .py file to be detected as revisions in the
30+
# versions/ directory
31+
# sourceless = false
32+
33+
# version location specification; this defaults
34+
# to alembic/versions. When using multiple version
35+
# directories, initial revisions must be specified with --version-path
36+
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
37+
38+
# the output encoding used when revision files
39+
# are written from script.py.mako
40+
# output_encoding = utf-8
41+
42+
sqlalchemy.url =
43+
44+
45+
[post_write_hooks]
46+
# post_write_hooks defines scripts or Python functions that are run
47+
# on newly generated revision scripts. See the documentation for further
48+
# detail and examples
49+
50+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
51+
# hooks = black
52+
# black.type = console_scripts
53+
# black.entrypoint = black
54+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
55+
56+
# Logging configuration
57+
[loggers]
58+
keys = root,sqlalchemy,alembic
59+
60+
[handlers]
61+
keys = console
62+
63+
[formatters]
64+
keys = generic
65+
66+
[logger_root]
67+
level = WARN
68+
handlers = console
69+
qualname =
70+
71+
[logger_sqlalchemy]
72+
level = WARN
73+
handlers =
74+
qualname = sqlalchemy.engine
75+
76+
[logger_alembic]
77+
level = INFO
78+
handlers =
79+
qualname = alembic
80+
81+
[handler_console]
82+
class = StreamHandler
83+
args = (sys.stderr,)
84+
level = NOTSET
85+
formatter = generic
86+
87+
[formatter_generic]
88+
format = %(levelname)-5.5s [%(name)s] %(message)s
89+
datefmt = %H:%M:%S

backend/alembic/README

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Generic single-database configuration.

backend/alembic/env.py

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import os
2+
from logging.config import fileConfig
3+
4+
from alembic.context import context
5+
from sqlalchemy import engine_from_config, pool
6+
7+
from app.database.models import Base
8+
9+
# this is the Alembic Config object, which provides
10+
# access to the values within the .ini file in use.
11+
config = context.config
12+
config.set_main_option("sqlalchemy.url", os.environ.get("DATABASE_URL"))
13+
14+
# Interpret the config file for Python logging.
15+
# This line sets up loggers basically.
16+
fileConfig(config.config_file_name)
17+
18+
# add your model's MetaData object here
19+
# for 'autogenerate' support
20+
# from myapp import mymodel
21+
# target_metadata = mymodel.Base.metadata
22+
23+
target_metadata = Base.metadata
24+
25+
# other values from the config, defined by the needs of env.py,
26+
# can be acquired:
27+
# my_important_option = config.get_main_option("my_important_option")
28+
# ... etc.
29+
30+
31+
def run_migrations_offline():
32+
"""Run migrations in 'offline' mode.
33+
34+
This configures the context with just a URL
35+
and not an Engine, though an Engine is acceptable
36+
here as well. By skipping the Engine creation
37+
we don't even need a DBAPI to be available.
38+
39+
Calls to context.execute() here emit the given string to the
40+
script output.
41+
42+
"""
43+
url = config.get_main_option("sqlalchemy.url")
44+
context.configure(
45+
url=url,
46+
target_metadata=target_metadata,
47+
literal_binds=True,
48+
dialect_opts={"paramstyle": "named"},
49+
)
50+
51+
with context.begin_transaction():
52+
context.run_migrations()
53+
54+
55+
def run_migrations_online():
56+
"""Run migrations in 'online' mode.
57+
58+
In this scenario we need to create an Engine
59+
and associate a connection with the context.
60+
61+
"""
62+
connectable = engine_from_config(
63+
config.get_section(config.config_ini_section),
64+
prefix="sqlalchemy.",
65+
poolclass=pool.NullPool,
66+
)
67+
68+
with connectable.connect() as connection:
69+
context.configure(connection=connection, target_metadata=target_metadata)
70+
71+
with context.begin_transaction():
72+
context.run_migrations()
73+
74+
75+
if context.is_offline_mode():
76+
run_migrations_offline()
77+
else:
78+
run_migrations_online()

backend/alembic/script.py.mako

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""${message}
2+
3+
Revision ID: ${up_revision}
4+
Revises: ${down_revision | comma,n}
5+
Create Date: ${create_date}
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
${imports if imports else ""}
11+
12+
# revision identifiers, used by Alembic.
13+
revision = ${repr(up_revision)}
14+
down_revision = ${repr(down_revision)}
15+
branch_labels = ${repr(branch_labels)}
16+
depends_on = ${repr(depends_on)}
17+
18+
19+
def upgrade():
20+
${upgrades if upgrades else "pass"}
21+
22+
23+
def downgrade():
24+
${downgrades if downgrades else "pass"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Create target and picture
2+
3+
Revision ID: 8fd03ac6d77d
4+
Revises:
5+
Create Date: 2021-05-08 20:05:59.130362
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "8fd03ac6d77d"
13+
down_revision = None
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
op.create_table(
21+
"targets",
22+
sa.Column("id", sa.Integer(), nullable=False),
23+
sa.Column("first_name", sa.String(), nullable=True),
24+
sa.Column("last_name", sa.String(), nullable=True),
25+
sa.Column("age", sa.Integer(), nullable=True),
26+
sa.PrimaryKeyConstraint("id"),
27+
)
28+
op.create_index(op.f("ix_targets_id"), "targets", ["id"], unique=False)
29+
op.create_table(
30+
"pictures",
31+
sa.Column("id", sa.Integer(), nullable=False),
32+
sa.Column("path", sa.String(), nullable=True),
33+
sa.Column("target_id", sa.Integer(), nullable=True),
34+
sa.ForeignKeyConstraint(
35+
["target_id"],
36+
["targets.id"],
37+
),
38+
sa.PrimaryKeyConstraint("id"),
39+
)
40+
op.create_index(op.f("ix_pictures_id"), "pictures", ["id"], unique=False)
41+
# ### end Alembic commands ###
42+
43+
44+
def downgrade():
45+
# ### commands auto generated by Alembic - please adjust! ###
46+
op.drop_index(op.f("ix_pictures_id"), table_name="pictures")
47+
op.drop_table("pictures")
48+
op.drop_index(op.f("ix_targets_id"), table_name="targets")
49+
op.drop_table("targets")
50+
# ### end Alembic commands ###

backend/app/database/session.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,7 @@
55
from sqlalchemy.ext.declarative import declarative_base
66
from sqlalchemy.orm import sessionmaker
77

8-
SQLALCHEMY_DATABASE_URL = (
9-
"postgresql://"
10-
+ os.environ.get("POSTGRES_USER", "app")
11-
+ ":"
12-
+ os.environ.get("POSTGRES_PASSWORD", "password")
13-
+ "@postgres:5432/app"
14-
)
8+
SQLALCHEMY_DATABASE_URL = os.environ.get("DATABASE_URL")
159

1610
engine = create_engine(SQLALCHEMY_DATABASE_URL)
1711
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

backend/app/main.py

-3
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,13 @@
44
from fastapi.responses import Response
55
from simber import Logger
66

7-
from app.database import models
8-
from app.database.session import engine
97
from app.router import auth, targets
108
from app.service.keycloak import verify_token
119

1210
LOG_FORMAT = "{levelname} [{filename}:{lineno}]:"
1311
logger = Logger(__name__, log_path="/logs/api.log")
1412
logger.update_format(LOG_FORMAT)
1513

16-
models.Base.metadata.create_all(bind=engine)
1714

1815
app = FastAPI(docs_url="/api/docs", openapi_url="/api/openapi")
1916

backend/app/router/auth.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,6 @@
99

1010
@router.post("/token")
1111
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
12-
"""Login user.
13-
14-
Args:
15-
form_data: form with username and password values
16-
17-
Returns:
18-
Access token and refresh token with their expiry times
19-
"""
12+
"""Login user."""
2013
token = await authenticate_user(form_data.username, form_data.password)
2114
return token

backend/entrypoint.sh

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
#!/bin/sh
22

3+
alembic upgrade head
34
python app/main.py

backend/requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ simber==0.2.1
66
SQLAlchemy==1.4.14
77
pydantic==1.8.1
88
psycopg2==2.8.6
9+
alembic==1.6.2

docker-compose.yaml

+12
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,25 @@ services:
2222
- .env
2323
environment:
2424
PYTHONPATH: .
25+
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_USER}
2526
volumes:
2627
- ./backend:/app
2728
- ./logs/backend:/logs
2829
depends_on:
2930
- postgres
3031
- keycloak
3132

33+
frontend:
34+
container_name: frontend
35+
build:
36+
context: frontend
37+
dockerfile: Dockerfile
38+
volumes:
39+
- ./frontend/app:/app
40+
environment:
41+
NODE_ENV: development
42+
CHOKIDAR_USEPOLLING: "true"
43+
3244
postgres:
3345
container_name: postgres
3446
image: postgres

0 commit comments

Comments
 (0)