Skip to content
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
36 changes: 20 additions & 16 deletions actual/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ def __enter__(self) -> Actual:
def __exit__(self, exc_type, exc_val, exc_tb):
if self._session:
self._session.close()
if self.engine:
self.engine.dispose()
self._in_context = False

@property
Expand Down Expand Up @@ -186,22 +188,23 @@ def run_migrations(self, migration_files: list[str]):
the base database, and the following files are migrations. Migrations can also be `.js` files. In this case,
we have to extract and execute queries from the standard JS.
"""
conn = sqlite3.connect(self._data_dir / "db.sqlite")
for file in migration_files:
if not file.startswith("migrations"):
continue # in case db.sqlite file gets passed as one of the migrations files
file_id = file.split("_")[0].split("/")[1]
if conn.execute(f"SELECT id FROM __migrations__ WHERE id = '{file_id}';").fetchall():
continue # skip migration as it was already ran
migration = self.data_file(file) # retrieves file from actual server
sql_statements = migration.decode()
if file.endswith(".js"):
# there is one migration which is Javascript. All entries inside db.execQuery(`...`) must be executed
exec_entries = js_migration_statements(sql_statements)
sql_statements = "\n".join(exec_entries)
conn.executescript(sql_statements)
conn.execute(f"INSERT INTO __migrations__ (id) VALUES ({file_id});")
conn.commit()
with sqlite3.connect(self._data_dir / "db.sqlite") as conn:
for file in migration_files:
if not file.startswith("migrations"):
continue # in case db.sqlite file gets passed as one of the migrations files
file_id = file.split("_")[0].split("/")[1]
if conn.execute(f"SELECT id FROM __migrations__ WHERE id = '{file_id}';").fetchall():
continue # skip migration as it was already ran
migration = self.data_file(file) # retrieves file from actual server
sql_statements = migration.decode()
if file.endswith(".js"):
# There is at least one migration which is a Javascript file.
# All entries inside db.execQuery(`...`) must be executed
exec_entries = js_migration_statements(sql_statements)
sql_statements = "\n".join(exec_entries)
conn.executescript(sql_statements)
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)
Expand Down Expand Up @@ -287,6 +290,7 @@ def cleanup(self):
VACUUM;
"""
)
conn.close()

def export_data(self, output_file: str | PathLike[str] | IO[bytes] | None = None, cleanup: bool = True) -> bytes:
"""
Expand Down
57 changes: 29 additions & 28 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime
import json
import os
import pathlib

import pytest
Expand Down Expand Up @@ -87,7 +88,7 @@ def test_init_interactive(actual_server, mocker):
def test_load_config(actual_server):
cfg = Config.load()
assert cfg.default_context == "test"
assert str(default_config_path()).endswith(".actualpy/config.yaml")
assert str(default_config_path()).endswith(".actualpy" + os.sep + "config.yaml")
# if the context does not exist, it should fail to load the server
cfg.default_context = "foo"
with pytest.raises(ValueError, match="Could not find budget with context"):
Expand Down Expand Up @@ -115,14 +116,13 @@ def test_metadata(actual_server):
def test_accounts(actual_server):
result = invoke(["accounts"])
assert result.exit_code == 0
assert result.stdout == (
" Accounts \n"
"┏━━━━━━━━━━━━━━┳━━━━━━━━━┓\n"
"┃ Account Name ┃ Balance ┃\n"
"┡━━━━━━━━━━━━━━╇━━━━━━━━━┩\n"
"│ Bank │ 50.00 │\n"
"└──────────────┴─────────┘\n"
)

# Split the output by line and assert that all expected data is present
lines = result.stdout.split("\n")
assert "Accounts" in lines[0]
assert "Account Name" in lines[2] and "Balance" in lines[2]
assert "Bank" in lines[4] and "50.00" in lines[4]

# make sure json is valid
result = invoke(["-o", "json", "accounts"])
assert json.loads(result.stdout) == [{"name": "Bank", "balance": 50.00}]
Expand All @@ -131,15 +131,17 @@ def test_accounts(actual_server):
def test_transactions(actual_server):
result = invoke(["transactions"])
assert result.exit_code == 0
assert result.stdout == (
" Transactions \n"
"┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┓\n"
"┃ Date ┃ Payee ┃ Notes ┃ Category ┃ Amount ┃\n"
"┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━┩\n"
"│ 2024-12-24 │ Shopping Center │ Christmas Gifts │ Gifts │ -100.00 │\n"
"│ 2024-09-05 │ Starting Balance │ │ Starting │ 150.00 │\n"
"└────────────┴──────────────────┴─────────────────┴──────────┴─────────┘\n"
)

# Split the output by line and assert that all expected data is present
lines = result.stdout.split("\n")
assert "Transactions" in lines[0]
for f in ["Date", "Payee", "Notes", "Category", "Amount"]:
assert f in lines[2]
for f in ["2024-12-24", "Shopping Center", "Christmas Gifts", "Gifts", "-100.00"]:
assert f in lines[4]
for f in ["2024-09-05", "Starting Balance", "Starting", "150.00"]:
assert f in lines[5]

# make sure json is valid
result = invoke(["-o", "json", "transactions"])
assert {
Expand All @@ -154,16 +156,15 @@ def test_transactions(actual_server):
def test_payees(actual_server):
result = invoke(["payees"])
assert result.exit_code == 0
assert result.stdout == (
" Payees \n"
"┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓\n"
"┃ Name ┃ Balance ┃\n"
"┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩\n"
"│ │ 0.00 │\n" # this is the payee for the account
"│ Starting Balance │ 150.00 │\n"
"│ Shopping Center │ -100.00 │\n"
"└──────────────────┴─────────┘\n"
)

# Split the output by line and assert that all expected data is present
lines = result.stdout.split("\n")
assert "Payees" in lines[0]
assert "Name" in lines[2] and "Balance" in lines[2]
assert "0.00" in lines[4]
assert "Starting Balance" in lines[5] and "150.00" in lines[5]
assert "Shopping Center" in lines[6] and "-100.00" in lines[6]

# make sure json is valid
result = invoke(["-o", "json", "payees"])
assert {"name": "Shopping Center", "balance": -100.00} in json.loads(result.stdout)
Expand Down
Loading