Skip to content

Commit

Permalink
fix: Fix issue where reflecting the model would happen before migrati…
Browse files Browse the repository at this point in the history
…ons (#73)

Closes #72
  • Loading branch information
bvanelli authored Sep 12, 2024
1 parent a341f5a commit 3913ced
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 8 deletions.
13 changes: 8 additions & 5 deletions actual/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ def __exit__(self, exc_type, exc_val, exc_tb):
@property
def session(self) -> Session:
if not self._session:
raise ActualError("No session defined. Use `with Actual() as actual:` construct to generate one.")
raise ActualError(
"No session defined. Use `with Actual() as actual:` construct to generate one.\n"
"If you are already using the context manager, try setting a file to use the session."
)
return self._session

def set_file(self, file_id: Union[str, RemoteFileListDTO]) -> RemoteFileListDTO:
Expand Down Expand Up @@ -149,6 +152,8 @@ def run_migrations(self, migration_files: list[str]):
conn.execute(f"INSERT INTO __migrations__ (id) VALUES ({file_id});")
conn.commit()
conn.close()
# update the metadata by reflecting the model
self._meta = reflect_model(self.engine)

def create_budget(self, budget_name: str):
"""Creates a budget using the remote server default database and migrations. If password is provided, the
Expand Down Expand Up @@ -176,14 +181,12 @@ def create_budget(self, budget_name: str):
}
)
self._file = RemoteFileListDTO(name=budget_name, fileId=file_id, groupId=None, deleted=0, encryptKeyId=None)
# create engine for downloaded database and run migrations
self.run_migrations(migration_files[1:])
# generate a session
self.engine = create_engine(f"sqlite:///{self._data_dir}/db.sqlite")
# create engine for downloaded database and run migrations
self.run_migrations(migration_files[1:])
if self._in_context:
self._session = strong_reference_session(Session(self.engine, **self._sa_kwargs))
# reflect the session
self._meta = reflect_model(self.engine)
# create a clock
self.load_clock()

Expand Down
2 changes: 1 addition & 1 deletion actual/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__version_info__ = ("0", "4", "0")
__version_info__ = ("0", "4", "1")
__version__ = ".".join(__version_info__)
9 changes: 8 additions & 1 deletion tests/test_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from actual import ActualError
from actual import Actual, ActualError
from actual.database import Notes
from actual.queries import (
create_account,
Expand Down Expand Up @@ -198,3 +198,10 @@ def test_default_imported_payee(session):
session.flush()
assert t.payee.name == "foo"
assert t.imported_description == "foo"


def test_session_error(mocker):
mocker.patch("actual.Actual.validate")
with Actual(token="foo") as actual:
with pytest.raises(ActualError, match="No session defined"):
print(actual.session) # try to access the session, should raise an exception
22 changes: 21 additions & 1 deletion tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import datetime

import pytest
from sqlalchemy import delete, select
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_for_logs

from actual import Actual, js_migration_statements
from actual.database import __TABLE_COLUMNS_MAP__, reflect_model
from actual.database import __TABLE_COLUMNS_MAP__, Dashboard, Migrations, reflect_model
from actual.exceptions import ActualDecryptionError, ActualError, AuthorizationError
from actual.queries import (
create_transaction,
Expand Down Expand Up @@ -156,6 +157,25 @@ def test_header_login():
assert response_login.data.token == response_header_login.data.token


def test_session_reflection_after_migrations():
with DockerContainer("actualbudget/actual-server:24.9.0").with_exposed_ports(5006) as container:
port = container.get_exposed_port(5006)
wait_for_logs(container, "Listening on :::5006...")
with Actual(f"http://localhost:{port}", password="mypass", bootstrap=True) as actual:
actual.create_budget("My Budget")
actual.upload_budget()
# add a dashboard entry
actual.session.add(Dashboard(id="123", x=1, y=2))
actual.commit()
# revert the last migration like it never happened
Dashboard.__table__.drop(actual.engine)
actual.session.exec(delete(Migrations).where(Migrations.id == 1722804019000))
actual.session.commit()
# now try to download the budget, it should not fail
with Actual(f"http://localhost:{port}", file="My Budget", password="mypass") as actual:
assert len(actual.session.exec(select(Dashboard)).all()) > 2 # there are two default dashboards


def test_empty_query_migrations():
# empty queries should not fail
assert js_migration_statements("await db.runQuery('');") == []
Expand Down

0 comments on commit 3913ced

Please sign in to comment.