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

feat: add versioning for type details #105

Merged
merged 2 commits into from
Feb 14, 2024
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
54 changes: 39 additions & 15 deletions src/entities/listeners.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,53 @@
from typing import List
from sqlalchemy import Connection, Table, event, inspect
from sqlalchemy.orm import Mapper

from .models.dao import Base, FinancialInstitutionDao
from .models.dao import Base, FinancialInstitutionDao, SblTypeMappingDao
from entities.engine.engine import engine


def inspect_fi(fi: FinancialInstitutionDao):
changes = {}
new_version = fi.version + 1 if fi.version else 1
state = inspect(fi)
for attr in state.attrs:
if attr.key == "event_time":
continue
if attr.key == "sbl_institution_types":
field_changes = inspect_type_fields(attr.value)
if attr.history.has_changes() or field_changes:
old_types = {"old": [o.as_db_dict() for o in attr.history.deleted]} if attr.history.deleted else {}
new_types = (
{"new": [{**n.as_db_dict(), "version": new_version} for n in attr.history.added]}
if attr.history.added
else {}
)
changes[attr.key] = {**old_types, **new_types, "field_changes": field_changes}
elif attr.history.has_changes():
changes[attr.key] = {"old": attr.history.deleted, "new": attr.history.added}
return changes


def inspect_type_fields(types: List[SblTypeMappingDao], fields: List[str] = ["details"]):
changes = []
for t in types:
state = inspect(t)
attr_changes = {
attr.key: {"old": attr.history.deleted, "new": attr.history.added}
for attr in state.attrs
if attr.key in fields and attr.history.has_changes()
}
if attr_changes:
changes.append({**t.as_db_dict(), **attr_changes})
return changes


def _setup_fi_history(fi_history: Table, mapping_history: Table):
def _insert_history(
mapper: Mapper[FinancialInstitutionDao], connection: Connection, target: FinancialInstitutionDao
):
new_version = target.version + 1 if target.version else 1
changes = {}
state = inspect(target)
for attr in state.attrs:
if attr.key == "event_time":
continue
attr_hist = attr.load_history()
if not attr_hist.has_changes():
continue
if attr.key == "sbl_institution_types":
old_types = [o.as_db_dict() for o in attr_hist.deleted]
new_types = [{**n.as_db_dict(), "version": new_version} for n in attr_hist.added]
changes[attr.key] = {"old": old_types, "new": new_types}
else:
changes[attr.key] = {"old": attr_hist.deleted, "new": attr_hist.added}
changes = inspect_fi(target)
if changes:
target.version = new_version
for t in target.sbl_institution_types:
Expand Down
63 changes: 59 additions & 4 deletions tests/entities/test_listeners.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from unittest.mock import Mock
import pytest
from unittest.mock import Mock, call
from pytest_mock import MockerFixture

from sqlalchemy import Connection, Table
from sqlalchemy import Connection, Insert, Table
from sqlalchemy.orm import Mapper, InstanceState, AttributeState
from sqlalchemy.orm.attributes import History

from entities.models.dao import FinancialInstitutionDao, SBLInstitutionTypeDao, SblTypeMappingDao

Expand Down Expand Up @@ -37,6 +39,14 @@ class TestListeners:
modified_by="test_user_id",
)

@pytest.fixture(autouse=True)
def setup(self):
self.fi_history.reset_mock()
self.fi_history.columns = {"name": "test"}
self.mapping_history.reset_mock()
self.mapper.reset_mock()
self.connection.reset_mock()

def test_fi_history_listener(self, mocker: MockerFixture):
jcadam14 marked this conversation as resolved.
Show resolved Hide resolved
inspect_mock = mocker.patch("entities.listeners.inspect")
attr_mock1: AttributeState = Mock(AttributeState)
Expand All @@ -45,10 +55,55 @@ def test_fi_history_listener(self, mocker: MockerFixture):
attr_mock2.key = "event_time"
state_mock: InstanceState = Mock(InstanceState)
state_mock.attrs = [attr_mock1, attr_mock2]
self.fi_history.columns = {"name": "test"}
inspect_mock.return_value = state_mock
fi_listener = _setup_fi_history(self.fi_history, self.mapping_history)
fi_listener(self.mapper, self.connection, self.target)
inspect_mock.assert_called_once_with(self.target)
attr_mock1.load_history.assert_called_once()
self.fi_history.insert.assert_called_once()
self.mapping_history.insert.assert_called_once()

def _get_fi_inspect_mock(self):
fi_attr_mock: AttributeState = Mock(AttributeState)
fi_attr_mock.key = "sbl_institution_types"
fi_attr_mock.value = self.target.sbl_institution_types
fi_attr_mock.history = History(added=[], deleted=[], unchanged=[])
fi_state_mock: InstanceState = Mock(InstanceState)
fi_state_mock.attrs = [fi_attr_mock]
return fi_state_mock

def _get_mapping_inspect_mock(self):
mapping_attr_mock: AttributeState = Mock(AttributeState)
mapping_attr_mock.key = "details"
mapping_attr_mock.history = History(added=["new type"], deleted=["old type"], unchanged=[])
mapping_state_mock: InstanceState = Mock(InstanceState)
mapping_state_mock.attrs = [mapping_attr_mock]
return mapping_state_mock

def test_fi_mapping_changed(self, mocker: MockerFixture):
inspect_mock = mocker.patch("entities.listeners.inspect")
fi_state_mock = self._get_fi_inspect_mock()
mapping_state_mock = self._get_mapping_inspect_mock()

def inspect_side_effect(inspect_target):
if inspect_target == self.target:
return fi_state_mock
elif inspect_target == self.target.sbl_institution_types[0]:
return mapping_state_mock

inspect_mock.side_effect = inspect_side_effect
fi_insert_mock = Mock(Insert)
self.fi_history.insert.return_value = fi_insert_mock
mapping_insert_mock = Mock(Insert)
self.mapping_history.insert.return_value = mapping_insert_mock
fi_listener = _setup_fi_history(self.fi_history, self.mapping_history)
fi_listener(self.mapper, self.connection, self.target)
inspect_mock.assert_has_calls([call(self.target), call(self.target.sbl_institution_types[0])])
self.fi_history.insert.assert_called_once()
self.mapping_history.insert.assert_called_once()
fi_insert_mock.values.assert_called_once()
args, _ = fi_insert_mock.values.call_args
insert_data = args[0]
assert insert_data["changeset"]["sbl_institution_types"]["field_changes"][0]["details"] == {
"old": ["old type"],
"new": ["new type"],
}
8 changes: 8 additions & 0 deletions tests/migrations/test_migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,11 @@ def test_tables_not_exist_migrate_down_to_base(alembic_runner: MigrationContext,
assert "denied_domains" not in tables
assert "financial_institutions" not in tables
assert "financial_institution_domains" not in tables


def test_fi_history_tables_8106d83ff594(alembic_runner: MigrationContext, alembic_engine: Engine):
alembic_runner.migrate_up_to("8106d83ff594")
inspector = sqlalchemy.inspect(alembic_engine)
tables = inspector.get_table_names()
assert "financial_institutions_history" in tables
assert "fi_to_type_mapping_history" in tables
22 changes: 22 additions & 0 deletions tests/migrations/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,25 @@ def test_fi_types_table_6826f05140cd(alembic_runner: MigrationContext, alembic_e
columns_names = [column.get("name") for column in columns]

assert columns_names == expected_columns


def test_fi_versioning_tables_3f893e52d05c(alembic_runner: MigrationContext, alembic_engine: Engine):
alembic_runner.migrate_up_to("3f893e52d05c")
inspector = sqlalchemy.inspect(alembic_engine)
fi_columns = inspector.get_columns("financial_institutions")
assert "version" in [column.get("name") for column in fi_columns]
mapping_columns = inspector.get_columns("fi_to_type_mapping")
assert "version" in [column.get("name") for column in mapping_columns]


def test_fi_history_table_columns_8106d83ff594(alembic_runner: MigrationContext, alembic_engine: Engine):
alembic_runner.migrate_up_to("8106d83ff594")
inspector = sqlalchemy.inspect(alembic_engine)
fi_columns = inspector.get_columns("financial_institutions")
mapping_columns = inspector.get_columns("fi_to_type_mapping")
fi_history_columns = inspector.get_columns("financial_institutions_history")
mapping_history_columns = inspector.get_columns("fi_to_type_mapping_history")
assert {column.get("name") for column in fi_columns}.issubset({column.get("name") for column in fi_history_columns})
assert {column.get("name") for column in mapping_columns}.issubset(
{column.get("name") for column in mapping_history_columns}
)
Loading