Skip to content

Commit

Permalink
Merge branch '217-flask-alembic' into 'main'
Browse files Browse the repository at this point in the history
SQL migrations with flask-alembic

Closes #217

See merge request yaal/canaille!214
  • Loading branch information
azmeuk committed Jan 10, 2025
2 parents 5aecb9a + 733625e commit fed0dc9
Show file tree
Hide file tree
Showing 21 changed files with 579 additions and 34 deletions.
3 changes: 2 additions & 1 deletion canaille/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def create_app(
config: dict = None,
validate: bool = True,
backend=None,
init_backend=None,
env_file: str = None,
env_prefix: str = "",
):
Expand Down Expand Up @@ -119,7 +120,7 @@ def create_app(
sentry_sdk = setup_sentry(app)
try:
setup_logging(app)
backend = setup_backend(app, backend)
backend = setup_backend(app, backend, init_backend)
setup_features(app)
setup_flask_converters(app)
setup_blueprints(app)
Expand Down
2 changes: 1 addition & 1 deletion canaille/app/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def install():
from canaille.app.installation import install

try:
install(current_app.config)
install(current_app)

except ConfigurationException as exc: # pragma: no cover
print(exc)
Expand Down
8 changes: 4 additions & 4 deletions canaille/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def __init__(self, config):
def instance(cls):
return cls._instance

def init_app(self, app):
def init_app(self, app, init_backend=None):
@app.before_request
def before_request():
return self.setup()
Expand All @@ -79,7 +79,7 @@ def session(self, *args, **kwargs):
self.teardown()

@classmethod
def install(self, config):
def install(self, app):
"""Prepare the database to host canaille data."""
raise NotImplementedError()

Expand Down Expand Up @@ -203,7 +203,7 @@ def register_models(self):
models.register(getattr(backend_models, model_name))


def setup_backend(app, backend=None):
def setup_backend(app, backend=None, init_backend=None):
if not backend:
prefix = "CANAILLE_"
available_backends_names = [
Expand All @@ -224,7 +224,7 @@ def setup_backend(app, backend=None):
module, f"{backend_name.title()}Backend", None
) or getattr(module, f"{backend_name.upper()}Backend", None)
backend = backend_class(app.config)
backend.init_app(app)
backend.init_app(app, init_backend)

with app.app_context():
g.backend = backend
Expand Down
6 changes: 3 additions & 3 deletions canaille/backends/ldap/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ def __init__(self, config):
setup_ldap_models(config)

@classmethod
def install(cls, config):
cls.setup_schemas(config)
with cls(config).session():
def install(cls, app):
cls.setup_schemas(app.config)
with cls(app.config).session():
models.Token.install()
models.AuthorizationCode.install()
models.Client.install()
Expand Down
2 changes: 1 addition & 1 deletion canaille/backends/memory/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def attribute_index(self, model, attribute="id"):
)

@classmethod
def install(cls, config):
def install(cls, app):
pass

def setup(self):
Expand Down
53 changes: 34 additions & 19 deletions canaille/backends/sql/backend.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import datetime
from pathlib import Path

from flask import current_app
from flask_alembic import Alembic
from sqlalchemy import create_engine
from sqlalchemy import or_
from sqlalchemy import select
Expand All @@ -15,14 +17,6 @@
Base = declarative_base()


def db_session(db_uri=None, init=False):
engine = create_engine(db_uri, echo=False, future=True)
if init:
Base.metadata.create_all(engine)
session = Session(engine)
return session


class SQLModelEncoder(ModelEncoder):
def default(self, obj):
if isinstance(obj, Password):
Expand All @@ -31,24 +25,45 @@ def default(self, obj):


class SQLBackend(Backend):
engine = None
db_session = None
json_encoder = SQLModelEncoder
alembic = None

def __init__(self, config):
super().__init__(config)
SQLBackend.engine = create_engine(
self.config["CANAILLE_SQL"]["DATABASE_URI"], echo=False, future=True
)
SQLBackend.alembic = Alembic(metadatas=Base.metadata, engines=SQLBackend.engine)

@classmethod
def install(cls, config): # pragma: no cover
engine = create_engine(
config["CANAILLE_SQL"]["DATABASE_URI"],
echo=False,
future=True,
def install(cls, app): # pragma: no cover
cls.init_alembic(app)
SQLBackend.alembic.upgrade()

@classmethod
def init_alembic(cls, app):
app.config["ALEMBIC"] = {
"script_location": str(Path(__file__).resolve().parent / "migrations"),
}
SQLBackend.alembic.init_app(app)

def init_app(self, app, init_backend=None):
super().init_app(app)
self.init_alembic(app)
init_backend = (
app.config["CANAILLE_SQL"]["AUTO_MIGRATE"]
if init_backend is None
else init_backend
)
Base.metadata.create_all(engine)
if init_backend: # pragma: no cover
with app.app_context():
self.alembic.upgrade()

def setup(self, init=False):
def setup(self):
if not self.db_session:
self.db_session = db_session(
self.config["CANAILLE_SQL"]["DATABASE_URI"],
init=init,
)
self.db_session = Session(SQLBackend.engine)

def teardown(self):
pass
Expand Down
11 changes: 11 additions & 0 deletions canaille/backends/sql/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ class SQLSettings(BaseModel):
Defines password hashing scheme in SQL database.
examples : "mssql2000", "ldap_salted_sha1", "pbkdf2_sha512"
"""

AUTO_MIGRATE: bool = True
"""Whether to automatically apply database migrations.
If :data:`True`, database migrations will be automatically applied when Canaille web application is launched.
If :data:`False`, migrations must be applied manually with ``canaille db upgrade``.
.. note::
When running the CLI, migrations will never be applied.
"""
Loading

0 comments on commit fed0dc9

Please sign in to comment.