Skip to content
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

#1224 #1225 Upgrade to SQLAlchemy v2 #1694

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
6 changes: 5 additions & 1 deletion .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ fileignoreconfig:
- filename: tests/app/db.py
checksum: df9a020fece4a10601c2e3200e663eed4bf327e2f0b16eac2d028aff56bca2ef
- filename: .github/workflows/dev-tests.yml
checksum: 0700d2eba3138fc34ef5bf2787ce5403cbd06797200922d35ce1f3f6ff8aaf74
checksum: 0700d2eba3138fc34ef5bf2787ce5403cbd06797200922d35ce1f3f6ff8aaf74
- filename: app/model/user.py
checksum: 3933f8bd02c0719c941b140659cf033ed74ac6e187e95ad9eadee6cddecade9c
- filename: app/models.py
checksum: 428afbe66c93668cc253813d6265b7211199fd3739e33e28767e0c006d411fcf
4 changes: 1 addition & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ install-safety:

check-dependencies: install-safety ## Scan dependencies for security vulnerabilities
# Ignored issues not described here are documented in requirements-app.txt.
# 12 Dec 2023: 51668 is fixed with >= 2.0.0b1 of SQLAlchemy. Ongoing refactor to upgrade.

safety check -r requirements.txt --full-report -i 51668
safety check -r requirements.txt --full-report

.PHONY:
help \
Expand Down
2 changes: 1 addition & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def create_app(application):
init_app(application)
request_helper.init_app(application)

# https://flask-sqlalchemy.palletsprojects.com/en/3.0.x/api/#flask_sqlalchemy.SQLAlchemy.init_app
# https://flask-sqlalchemy.palletsprojects.com/en/3.1.x/quickstart/#configure-the-extension
db.init_app(application)

migrate.init_app(application, db=db)
Expand Down
13 changes: 10 additions & 3 deletions app/db.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
# https://flask-sqlalchemy.palletsprojects.com/en/3.0.x/api/
from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy
from sqlalchemy.orm import DeclarativeBase


class Base(DeclarativeBase):
pass


class SQLAlchemy(_SQLAlchemy):
"""Subclass SQLAlchemy in order to override create_engine options."""
"""
Subclass SQLAlchemy in order to override create_engine options.
https://flask-sqlalchemy.palletsprojects.com/en/3.1.x/quickstart/#initialize-the-extension
"""

def apply_driver_hacks(
self,
Expand All @@ -19,4 +26,4 @@ def apply_driver_hacks(
)


db = SQLAlchemy()
db = SQLAlchemy(model_class=Base)
48 changes: 29 additions & 19 deletions app/history_meta.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
"""Versioned mixin class and other utilities.
"""
Versioned mixin class and other utilities.
This is an adapted version of:
https://bitbucket.org/zzzeek/sqlalchemy/raw/master/examples/versioned_history/history_meta.py
It does not use the create_version function from the orginal which looks for changes to models
It does not use the create_version function from the orginal, which looks for changes to models,
as we just insert a copy of a model to the history table on create or update.
Also it does not add a created_at timestamp to the history table as we already have created_at
It does not add a created_at timestamp to the history table because we already have created_at
and updated_at timestamps.
Lastly when to create a version is done manually in dao_utils version decorator and not via
Lastly, when to create a version is done manually in dao_utils version decorator and not via
session events.
"""

import datetime
from app import db
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import mapper, attributes, object_mapper
from sqlalchemy.orm import attributes, object_mapper, registry
from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty
from sqlalchemy import Table, Column, ForeignKeyConstraint, Integer
from sqlalchemy import util
Expand All @@ -38,6 +39,10 @@ def _is_versioning_col(col):


def _history_mapper(local_mapper): # noqa (C901 too complex)
"""
Set the "__history_mapper__" attribute on the class associated with "local_mapper".
"""

cls = local_mapper.class_

# set the "active_history" flag
Expand Down Expand Up @@ -123,7 +128,8 @@ def _col_copy(col):
bases = local_mapper.base_mapper.class_.__bases__
versioned_cls = type.__new__(type, '%sHistory' % cls.__name__, bases, {})

m = mapper(
# m = mapper_registry.map_imperatively(
m = cls.registry.map_imperatively(
versioned_cls,
table,
inherits=super_history_mapper,
Expand All @@ -138,19 +144,23 @@ def _col_copy(col):
local_mapper.add_property('version', local_mapper.local_table.c.version)


class Versioned(object):
@declared_attr
def __mapper_cls__(cls):
def map(
cls,
*arg,
**kw,
):
mp = mapper(cls, *arg, **kw)
_history_mapper(mp)
return mp
class Versioned:
# @declared_attr
# def __mapper_cls__(cls):
# return
# def the_map(
# cls,
# *arg,
# **kw,
# ):
# mp = mapper_registry.map_imperatively(cls, *arg, **kw)
# mp = cls.registry.map_imperatively(cls, *arg, **kw)

# Set the __history_mapper__ attribute of the mapper.
# _history_mapper(mp)
# return mp

return map
# return the_map

@classmethod
def get_history_model(cls):
Expand Down
37 changes: 19 additions & 18 deletions app/model/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
import uuid

from sqlalchemy import CheckConstraint, select
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import mapped_column
from app import DATETIME_FORMAT
from app.db import db
from app.encryption import hashpw, check_hash
Expand All @@ -28,27 +29,27 @@ def __init__(
if idp_name and idp_id:
self.idp_ids.append(IdentityProviderIdentifier(self.id, idp_name, str(idp_id)))

id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = db.Column(db.String, nullable=False, index=True, unique=False)
email_address = db.Column(db.String(255), nullable=False, index=True, unique=True)
created_at = db.Column(db.DateTime, index=False, unique=False, nullable=False, default=datetime.datetime.utcnow)
updated_at = db.Column(db.DateTime, index=False, unique=False, nullable=True, onupdate=datetime.datetime.utcnow)
_password = db.Column(db.String, index=False, unique=False, nullable=True)
mobile_number = db.Column(db.String, index=False, unique=False, nullable=True)
password_changed_at = db.Column(
id = mapped_column(db.UUID, primary_key=True, default=uuid.uuid4)
name = mapped_column(db.String, nullable=False, index=True, unique=False)
email_address = mapped_column(db.String(255), nullable=False, index=True, unique=True)
created_at = mapped_column(db.DateTime, index=False, unique=False, nullable=False, default=datetime.datetime.utcnow)
updated_at = mapped_column(db.DateTime, index=False, unique=False, nullable=True, onupdate=datetime.datetime.utcnow)
_password = mapped_column(db.String, index=False, unique=False, nullable=True)
mobile_number = mapped_column(db.String, index=False, unique=False, nullable=True)
password_changed_at = mapped_column(
db.DateTime, index=False, unique=False, nullable=True, default=datetime.datetime.utcnow
)
logged_in_at = db.Column(db.DateTime, nullable=True)
failed_login_count = db.Column(db.Integer, nullable=False, default=0)
state = db.Column(db.String, nullable=False, default='pending')
platform_admin = db.Column(db.Boolean, nullable=False, default=False)
current_session_id = db.Column(UUID(as_uuid=True), nullable=True)
auth_type = db.Column(
logged_in_at = mapped_column(db.DateTime, nullable=True)
failed_login_count = mapped_column(db.Integer, nullable=False, default=0)
state = mapped_column(db.String, nullable=False, default='pending')
platform_admin = mapped_column(db.Boolean, nullable=False, default=False)
current_session_id = mapped_column(db.UUID, nullable=True)
auth_type = mapped_column(
db.String, db.ForeignKey('auth_type.name'), index=True, nullable=True, default=EMAIL_AUTH_TYPE
)
blocked = db.Column(db.Boolean, nullable=False, default=False)
additional_information = db.Column(JSONB(none_as_null=True), nullable=True, default={})
_identity_provider_user_id = db.Column(
blocked = mapped_column(db.Boolean, nullable=False, default=False)
additional_information = mapped_column(JSONB(none_as_null=True), nullable=True, default={})
_identity_provider_user_id = mapped_column(
'identity_provider_user_id', db.String, index=True, unique=True, nullable=True
)
idp_ids = db.relationship('IdentityProviderIdentifier', cascade='all, delete-orphan')
Expand Down
Loading