diff --git a/README.md b/README.md index eeb30bc..3cd5306 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ SQLite Export for YNAB - Export YNAB Budget Data to SQLite ## What This Does -Export your [YNAB](https://ynab.com/) budget to a local [SQLite](https://www.sqlite.org/) DB. Then you can query your budget with any tools compatible with SQLite. +Export all your [YNAB](https://ynab.com/) plans to a local [SQLite](https://www.sqlite.org/) DB. Then you can query your data with any tools compatible with SQLite. ## Installation @@ -24,7 +24,7 @@ Provision a [YNAB Personal Access Token](https://api.ynab.com/#personal-access-t $ export YNAB_PERSONAL_ACCESS_TOKEN="..." ``` -Run the tool from the terminal to download your budget: +Run the tool from the terminal to download your plans: ```console $ sqlite-export-for-ynab @@ -60,7 +60,7 @@ asyncio.run(sync(token, db, full_refresh)) The relations are defined in [create-relations.sql](sqlite_export_for_ynab/ddl/create-relations.sql). They are 1:1 with [YNAB's OpenAPI Spec](https://api.ynab.com/papi/open_api_spec.yaml) (ex: transactions, accounts, etc) with some additions: 1. Some objects are pulled out into their own tables so they can be more cleanly modeled in SQLite (ex: subtransactions, loan account periodic values). -1. Foreign keys are added as needed (ex: budget ID, transaction ID) so data across budgets remains separate. +1. Foreign keys are added as needed (ex: plan ID, transaction ID) so data across plans remains separate. 1. Two new views called `flat_transactions` and `scheduled_flat_transactions`. These allow you to query split and non-split transactions easily, without needing to also query `subtransactions` and `scheduled_subtransactions` respectively. They also include fields to improve quality of life (ex: `amount_major` to convert from [YNAB's milliunits](https://api.ynab.com/#formats) to [major units](https://en.wikipedia.org/wiki/ISO_4217) i.e. dollars) and filter out deleted transactions/subtransactions. ## Querying @@ -69,35 +69,35 @@ You can issue queries with typical SQLite tools. *`sqlite-export-for-ynab` delib ### Sample Queries -To get the top 5 payees by spending per budget, you could do: +To get the top 5 payees by spending per plan, you could do: ```sql WITH ranked_payees AS ( SELECT - b.name AS budget_name + pl."name" AS plan_name , t.payee_name AS payee , SUM(t.amount_major) AS net_spent , ROW_NUMBER() OVER ( PARTITION BY - b.id + pl.id ORDER BY SUM(t.amount) ASC ) AS rnk FROM flat_transactions AS t - INNER JOIN budgets AS b - ON t.budget_id = b.id + INNER JOIN plans AS pl + ON t.plan_id = pl.id WHERE t.payee_name != 'Starting Balance' AND t.transfer_account_id IS NULL GROUP BY - b.id + pl.id , t.payee_id ) SELECT - budget_name + plan_name , payee , net_spent FROM @@ -105,7 +105,7 @@ FROM WHERE rnk <= 5 ORDER BY - budget_name ASC + plan_name ASC , net_spent DESC ; ``` @@ -114,44 +114,44 @@ To get duplicate payees, or payees with no transactions: ```sql SELECT DISTINCT - b.name AS budget - , dupes.name AS payee + pl."name" AS "plan" + , dupes."name" AS payee FROM ( SELECT DISTINCT - p.budget_id - , p.name + p.plan_id + , p."name" FROM payees AS p LEFT JOIN flat_transactions AS ft ON - p.budget_id = ft.budget_id + p.plan_id = ft.plan_id AND p.id = ft.payee_id LEFT JOIN scheduled_flat_transactions AS sft ON - p.budget_id = sft.budget_id + p.plan_id = sft.plan_id AND p.id = sft.payee_id WHERE TRUE AND ft.payee_id IS NULL AND sft.payee_id IS NULL AND p.transfer_account_id IS NULL - AND p.name != 'Reconciliation Balance Adjustment' - AND p.name != 'Manual Balance Adjustment' + AND p."name" != 'Reconciliation Balance Adjustment' + AND p."name" != 'Manual Balance Adjustment' AND NOT p.deleted UNION ALL SELECT - budget_id - , name + plan_id + , "name" FROM payees WHERE NOT deleted - GROUP BY budget_id, name + GROUP BY plan_id, "name" HAVING COUNT(*) > 1 ) AS dupes -INNER JOIN budgets AS b - ON dupes.budget_id = b.id -ORDER BY budget, payee +INNER JOIN plans AS pl + ON dupes.plan_id = pl.id +ORDER BY "plan", payee ; ``` @@ -159,11 +159,11 @@ To count the spend for a category (ex: "Apps") between this month and the next 1 ```sql SELECT - budget_id + plan_id , SUM(amount_major) AS amount_major FROM ( SELECT - budget_id + plan_id , amount_major FROM flat_transactions WHERE @@ -171,7 +171,7 @@ FROM ( AND SUBSTR(`date`, 1, 7) = SUBSTR(DATE(), 1, 7) UNION ALL SELECT - budget_id + plan_id , amount_major * ( CASE WHEN frequency = 'monthly' THEN 11 diff --git a/sqlite_export_for_ynab/_main.py b/sqlite_export_for_ynab/_main.py index 8cba274..5458d35 100644 --- a/sqlite_export_for_ynab/_main.py +++ b/sqlite_export_for_ynab/_main.py @@ -41,7 +41,7 @@ | Literal["scheduled_subtransactions"] ) _ALL_RELATIONS = frozenset( - ("budgets", "flat_transactions", "scheduled_flat_transactions") + ("plans", "flat_transactions", "scheduled_flat_transactions") + tuple(lit.__args__[0] for lit in _EntryTable.__args__) ) @@ -61,7 +61,7 @@ async def async_main(argv: Sequence[str] | None = None) -> int: parser.add_argument( "--full-refresh", action="store_true", - help="**DROP ALL TABLES** and fetch all budget data again.", + help="**DROP ALL TABLES** and fetch all data again.", ) parser.add_argument( "--version", action="version", version=f"%(prog)s {version(_PACKAGE)}" @@ -98,9 +98,9 @@ def default_db_path() -> Path: async def sync(token: str, db: Path, full_refresh: bool) -> None: async with aiohttp.ClientSession() as session: - budgets = (await YnabClient(token, session)("budgets"))["budgets"] + plans = (await YnabClient(token, session)("plans"))["plans"] - budget_ids = [b["id"] for b in budgets] + plan_ids = [plan["id"] for plan in plans] if not db.exists(): db.parent.mkdir(parents=True, exist_ok=True) @@ -122,17 +122,17 @@ async def sync(token: str, db: Path, full_refresh: bool) -> None: con.commit() print("Done") - print("Fetching budget data...") + print("Fetching plan data...") lkos = get_last_knowledge_of_server(cur) async with aiohttp.ClientSession() as session: - with tldm(desc="Budget Data", total=len(budgets) * 5) as pbar: + with tldm(desc="Plan Data", total=len(plans) * 5) as pbar: yc = ProgressYnabClient(YnabClient(token, session), pbar) - account_jobs = jobs(yc, "accounts", budget_ids, lkos) - cat_jobs = jobs(yc, "categories", budget_ids, lkos) - payee_jobs = jobs(yc, "payees", budget_ids, lkos) - txn_jobs = jobs(yc, "transactions", budget_ids, lkos) - sched_txn_jobs = jobs(yc, "scheduled_transactions", budget_ids, lkos) + account_jobs = jobs(yc, "accounts", plan_ids, lkos) + cat_jobs = jobs(yc, "categories", plan_ids, lkos) + payee_jobs = jobs(yc, "payees", plan_ids, lkos) + txn_jobs = jobs(yc, "transactions", plan_ids, lkos) + sched_txn_jobs = jobs(yc, "scheduled_transactions", plan_ids, lkos) data = await asyncio.gather( *account_jobs, *cat_jobs, *payee_jobs, *txn_jobs, *sched_txn_jobs @@ -150,8 +150,10 @@ async def sync(token: str, db: Path, full_refresh: bool) -> None: all_sched_txn_data = data[la + lc + lp + lt :] new_lkos = { - bid: t["server_knowledge"] - for bid, t in zip(budget_ids, all_txn_data, strict=True) + plan_id: transaction_data["server_knowledge"] + for plan_id, transaction_data in zip( + plan_ids, all_txn_data, strict=True + ) } print("Done") @@ -164,19 +166,21 @@ async def sync(token: str, db: Path, full_refresh: bool) -> None: ): print("No new data fetched") else: - print("Inserting budget data...") - insert_budgets(cur, budgets, new_lkos) - for bid, account_data in zip(budget_ids, all_account_data, strict=True): - insert_accounts(cur, bid, account_data["accounts"]) - for bid, cat_data in zip(budget_ids, all_cat_data, strict=True): - insert_category_groups(cur, bid, cat_data["category_groups"]) - for bid, payee_data in zip(budget_ids, all_payee_data, strict=True): - insert_payees(cur, bid, payee_data["payees"]) - for bid, txn_data in zip(budget_ids, all_txn_data, strict=True): - insert_transactions(cur, bid, txn_data["transactions"]) - for bid, sched_txn_data in zip(budget_ids, all_sched_txn_data, strict=True): + print("Inserting plan data...") + insert_plans(cur, plans, new_lkos) + for plan_id, account_data in zip(plan_ids, all_account_data, strict=True): + insert_accounts(cur, plan_id, account_data["accounts"]) + for plan_id, cat_data in zip(plan_ids, all_cat_data, strict=True): + insert_category_groups(cur, plan_id, cat_data["category_groups"]) + for plan_id, payee_data in zip(plan_ids, all_payee_data, strict=True): + insert_payees(cur, plan_id, payee_data["payees"]) + for plan_id, txn_data in zip(plan_ids, all_txn_data, strict=True): + insert_transactions(cur, plan_id, txn_data["transactions"]) + for plan_id, sched_txn_data in zip( + plan_ids, all_sched_txn_data, strict=True + ): insert_scheduled_transactions( - cur, bid, sched_txn_data["scheduled_transactions"] + cur, plan_id, sched_txn_data["scheduled_transactions"] ) print("Done") @@ -202,17 +206,17 @@ def get_last_knowledge_of_server(cur: sqlite3.Cursor) -> dict[str, int]: return { r["id"]: r["last_knowledge_of_server"] for r in cur.execute( - "SELECT id, last_knowledge_of_server FROM budgets", + "SELECT id, last_knowledge_of_server FROM plans", ).fetchall() } -def insert_budgets( - cur: sqlite3.Cursor, budgets: list[dict[str, Any]], lkos: dict[str, int] +def insert_plans( + cur: sqlite3.Cursor, plans: list[dict[str, Any]], lkos: dict[str, int] ) -> None: cur.executemany( """ - INSERT OR REPLACE INTO budgets ( + INSERT OR REPLACE INTO plans ( id , name , currency_format_currency_symbol @@ -227,18 +231,18 @@ def insert_budgets( """, ( ( - bid := b["id"], - b["name"], - b["currency_format"]["currency_symbol"], - b["currency_format"]["decimal_digits"], - b["currency_format"]["decimal_separator"], - b["currency_format"]["display_symbol"], - b["currency_format"]["group_separator"], - b["currency_format"]["iso_code"], - b["currency_format"]["symbol_first"], - lkos[bid], + plan_id := plan["id"], + plan["name"], + plan["currency_format"]["currency_symbol"], + plan["currency_format"]["decimal_digits"], + plan["currency_format"]["decimal_separator"], + plan["currency_format"]["display_symbol"], + plan["currency_format"]["group_separator"], + plan["currency_format"]["iso_code"], + plan["currency_format"]["symbol_first"], + lkos[plan_id], ) - for b in budgets + for plan in plans ), ) @@ -249,7 +253,7 @@ def insert_budgets( def insert_accounts( - cur: sqlite3.Cursor, budget_id: str, accounts: list[dict[str, Any]] + cur: sqlite3.Cursor, plan_id: str, accounts: list[dict[str, Any]] ) -> None: # YNAB's LoanAccountPeriodValues are untyped dicts so we need to turn them into a more standard sub-entry view updated_accounts = [ @@ -271,7 +275,7 @@ def insert_accounts( return insert_nested_entries( cur, - budget_id, + plan_id, updated_accounts, "Accounts", "accounts", @@ -281,11 +285,11 @@ def insert_accounts( def insert_category_groups( - cur: sqlite3.Cursor, budget_id: str, category_groups: list[dict[str, Any]] + cur: sqlite3.Cursor, plan_id: str, category_groups: list[dict[str, Any]] ) -> None: return insert_nested_entries( cur, - budget_id, + plan_id, category_groups, "Categories", "category_groups", @@ -295,21 +299,21 @@ def insert_category_groups( def insert_payees( - cur: sqlite3.Cursor, budget_id: str, payees: list[dict[str, Any]] + cur: sqlite3.Cursor, plan_id: str, payees: list[dict[str, Any]] ) -> None: if not payees: return for payee in tldm(payees, desc="Payees"): - insert_entry(cur, "payees", budget_id, payee) + insert_entry(cur, "payees", plan_id, payee) def insert_transactions( - cur: sqlite3.Cursor, budget_id: str, transactions: list[dict[str, Any]] + cur: sqlite3.Cursor, plan_id: str, transactions: list[dict[str, Any]] ) -> None: return insert_nested_entries( cur, - budget_id, + plan_id, transactions, "Transactions", "transactions", @@ -319,11 +323,11 @@ def insert_transactions( def insert_scheduled_transactions( - cur: sqlite3.Cursor, budget_id: str, scheduled_transactions: list[dict[str, Any]] + cur: sqlite3.Cursor, plan_id: str, scheduled_transactions: list[dict[str, Any]] ) -> None: return insert_nested_entries( cur, - budget_id, + plan_id, scheduled_transactions, "Scheduled Transactions", "scheduled_transactions", @@ -335,7 +339,7 @@ def insert_scheduled_transactions( @overload def insert_nested_entries( cur: sqlite3.Cursor, - budget_id: str, + plan_id: str, entries: list[dict[str, Any]], desc: Literal["Accounts"], entries_name: Literal["accounts"], @@ -347,7 +351,7 @@ def insert_nested_entries( @overload def insert_nested_entries( cur: sqlite3.Cursor, - budget_id: str, + plan_id: str, entries: list[dict[str, Any]], desc: Literal["Categories"], entries_name: Literal["category_groups"], @@ -359,7 +363,7 @@ def insert_nested_entries( @overload def insert_nested_entries( cur: sqlite3.Cursor, - budget_id: str, + plan_id: str, entries: list[dict[str, Any]], desc: Literal["Transactions"], entries_name: Literal["transactions"], @@ -371,7 +375,7 @@ def insert_nested_entries( @overload def insert_nested_entries( cur: sqlite3.Cursor, - budget_id: str, + plan_id: str, entries: list[dict[str, Any]], desc: Literal["Scheduled Transactions"], entries_name: Literal["scheduled_transactions"], @@ -382,7 +386,7 @@ def insert_nested_entries( def insert_nested_entries( cur: sqlite3.Cursor, - budget_id: str, + plan_id: str, entries: list[dict[str, Any]], desc: ( Literal["Accounts"] @@ -419,24 +423,24 @@ def insert_nested_entries( insert_entry( cur, entries_name, - budget_id, + plan_id, {k: v for k, v in entry.items() if k != subentries_name}, ) pbar.update() for subentry in entry[subentries_name]: - insert_entry(cur, subentries_table_name, budget_id, subentry) + insert_entry(cur, subentries_table_name, plan_id, subentry) pbar.update() def insert_entry( cur: sqlite3.Cursor, table: _EntryTable, - budget_id: str, + plan_id: str, entry: dict[str, Any], ) -> None: ekeys, evalues = zip(*entry.items(), strict=True) - keys, values = ekeys + ("budget_id",), evalues + (budget_id,) + keys, values = ekeys + ("plan_id",), evalues + (plan_id,) cur.execute( f"INSERT OR REPLACE INTO {table} ({', '.join(keys)}) VALUES ({', '.join('?' * len(values))})", @@ -453,12 +457,12 @@ def jobs( | Literal["transactions"] | Literal["scheduled_transactions"] ), - budget_ids: list[str], + plan_ids: list[str], lkos: dict[str, int], ) -> list[Awaitable[dict[str, Any]]]: return [ - yc(f"budgets/{bid}/{endpoint}", last_knowledge_of_server=lkos.get(bid)) - for bid in budget_ids + yc(f"plans/{plan_id}/{endpoint}", last_knowledge_of_server=lkos.get(plan_id)) + for plan_id in plan_ids ] diff --git a/sqlite_export_for_ynab/ddl/create-relations.sql b/sqlite_export_for_ynab/ddl/create-relations.sql index d03ef18..9a5ec6f 100644 --- a/sqlite_export_for_ynab/ddl/create-relations.sql +++ b/sqlite_export_for_ynab/ddl/create-relations.sql @@ -1,4 +1,4 @@ -CREATE TABLE IF NOT EXISTS budgets ( +CREATE TABLE IF NOT EXISTS plans ( id TEXT PRIMARY KEY , name TEXT , currency_format_currency_symbol TEXT @@ -14,7 +14,7 @@ CREATE TABLE IF NOT EXISTS budgets ( CREATE TABLE IF NOT EXISTS accounts ( id TEXT PRIMARY KEY - , budget_id TEXT + , plan_id TEXT , balance INT , cleared_balance INT , closed BOOLEAN @@ -29,35 +29,35 @@ CREATE TABLE IF NOT EXISTS accounts ( , transfer_payee_id TEXT , type TEXT , uncleared_balance INT - , FOREIGN KEY (budget_id) REFERENCES budgets (id) + , FOREIGN KEY (plan_id) REFERENCES plans (id) ) ; CREATE TABLE IF NOT EXISTS account_periodic_values ( "date" TEXT , name TEXT - , budget_id TEXT + , plan_id TEXT , account_id TEXT , amount INT - , PRIMARY KEY (date, name, budget_id, account_id) - , FOREIGN KEY (budget_id) REFERENCES budgets (id) + , PRIMARY KEY (date, name, plan_id, account_id) + , FOREIGN KEY (plan_id) REFERENCES plans (id) , FOREIGN KEY (account_id) REFERENCES accounts (id) ) ; CREATE TABLE IF NOT EXISTS category_groups ( id TEXT PRIMARY KEY - , budget_id TEXT + , plan_id TEXT , name TEXT , hidden BOOLEAN , deleted BOOLEAN - , FOREIGN KEY (budget_id) REFERENCES budgets (id) + , FOREIGN KEY (plan_id) REFERENCES plans (id) ) ; CREATE TABLE IF NOT EXISTS categories ( id TEXT PRIMARY KEY - , budget_id TEXT + , plan_id TEXT , category_group_id TEXT , category_group_name TEXT , name TEXT @@ -83,24 +83,24 @@ CREATE TABLE IF NOT EXISTS categories ( , goal_overall_funded INT , goal_overall_left INT , deleted BOOLEAN - , FOREIGN KEY (budget_id) REFERENCES budgets (id) + , FOREIGN KEY (plan_id) REFERENCES plans (id) , FOREIGN KEY (category_group_id) REFERENCES category_groups (id) ) ; CREATE TABLE IF NOT EXISTS payees ( id TEXT PRIMARY KEY - , budget_id TEXT + , plan_id TEXT , name TEXT , transfer_account_id TEXT , deleted BOOLEAN - , FOREIGN KEY (budget_id) REFERENCES budgets (id) + , FOREIGN KEY (plan_id) REFERENCES plans (id) ) ; CREATE TABLE IF NOT EXISTS transactions ( id TEXT PRIMARY KEY - , budget_id TEXT + , plan_id TEXT , account_id TEXT , account_name TEXT , amount INT @@ -122,7 +122,7 @@ CREATE TABLE IF NOT EXISTS transactions ( , payee_name TEXT , transfer_account_id TEXT , transfer_transaction_id TEXT - , FOREIGN KEY (budget_id) REFERENCES budgets (id) + , FOREIGN KEY (plan_id) REFERENCES plans (id) , FOREIGN KEY (account_id) REFERENCES accounts (id) , FOREIGN KEY (category_id) REFERENCES categories (id) , FOREIGN KEY (payee_id) REFERENCES payees (id) @@ -131,7 +131,7 @@ CREATE TABLE IF NOT EXISTS transactions ( CREATE TABLE IF NOT EXISTS subtransactions ( id TEXT PRIMARY KEY - , budget_id TEXT + , plan_id TEXT , amount INT , category_id TEXT , category_name TEXT @@ -142,11 +142,11 @@ CREATE TABLE IF NOT EXISTS subtransactions ( , transaction_id TEXT , transfer_account_id TEXT , transfer_transaction_id TEXT - , FOREIGN KEY (budget_id) REFERENCES budget (id) + , FOREIGN KEY (plan_id) REFERENCES plans (id) , FOREIGN KEY (transfer_account_id) REFERENCES accounts (id) , FOREIGN KEY (category_id) REFERENCES categories (id) , FOREIGN KEY (payee_id) REFERENCES payees (id) - , FOREIGN KEY (transaction_id) REFERENCES transaction_id (id) + , FOREIGN KEY (transaction_id) REFERENCES transactions (id) ) ; @@ -154,7 +154,7 @@ CREATE VIEW IF NOT EXISTS flat_transactions AS SELECT t.id AS transaction_id , st.id AS subtransaction_id - , t.budget_id + , t.plan_id , t.account_id , t.account_name , t.approved @@ -187,12 +187,12 @@ FROM transactions AS t LEFT JOIN subtransactions AS st ON ( - t.budget_id = st.budget_id + t.plan_id = st.plan_id AND t.id = st.transaction_id ) INNER JOIN categories AS c ON ( - t.budget_id = c.budget_id + t.plan_id = c.plan_id AND c.id = CASE WHEN st.id IS NULL THEN t.category_id ELSE st.category_id END ) @@ -203,7 +203,7 @@ WHERE CREATE TABLE IF NOT EXISTS scheduled_transactions ( id TEXT PRIMARY KEY - , budget_id TEXT + , plan_id TEXT , account_id TEXT , account_name TEXT , amount INT @@ -219,7 +219,7 @@ CREATE TABLE IF NOT EXISTS scheduled_transactions ( , payee_id TEXT , payee_name TEXT , transfer_account_id TEXT - , FOREIGN KEY (budget_id) REFERENCES budgets (id) + , FOREIGN KEY (plan_id) REFERENCES plans (id) , FOREIGN KEY (account_id) REFERENCES accounts (id) , FOREIGN KEY (category_id) REFERENCES categories (id) , FOREIGN KEY (payee_id) REFERENCES payees (id) @@ -229,7 +229,7 @@ CREATE TABLE IF NOT EXISTS scheduled_transactions ( CREATE TABLE IF NOT EXISTS scheduled_subtransactions ( id TEXT PRIMARY KEY - , budget_id TEXT + , plan_id TEXT , scheduled_transaction_id TEXT , amount INT , memo TEXT @@ -239,11 +239,13 @@ CREATE TABLE IF NOT EXISTS scheduled_subtransactions ( , category_name TEXT , transfer_account_id TEXT , deleted BOOLEAN - , FOREIGN KEY (budget_id) REFERENCES budget (id) + , FOREIGN KEY (plan_id) REFERENCES plans (id) , FOREIGN KEY (transfer_account_id) REFERENCES accounts (id) , FOREIGN KEY (category_id) REFERENCES categories (id) , FOREIGN KEY (payee_id) REFERENCES payees (id) - , FOREIGN KEY (scheduled_transaction_id) REFERENCES transaction_id (id) + , FOREIGN KEY ( + scheduled_transaction_id + ) REFERENCES scheduled_transactions (id) ) ; @@ -251,7 +253,7 @@ CREATE VIEW IF NOT EXISTS scheduled_flat_transactions AS SELECT t.id AS transaction_id , st.id AS subtransaction_id - , t.budget_id + , t.plan_id , t.account_id , t.account_name , t.date_first @@ -277,12 +279,12 @@ FROM scheduled_transactions AS t LEFT JOIN scheduled_subtransactions AS st ON ( - t.budget_id = st.budget_id + t.plan_id = st.plan_id AND t.id = st.scheduled_transaction_id ) INNER JOIN categories AS c ON ( - t.budget_id = c.budget_id + t.plan_id = c.plan_id AND c.id = CASE WHEN st.id IS NULL THEN t.category_id ELSE st.category_id END ) diff --git a/sqlite_export_for_ynab/ddl/drop-relations.sql b/sqlite_export_for_ynab/ddl/drop-relations.sql index 3b6d657..a27d779 100644 --- a/sqlite_export_for_ynab/ddl/drop-relations.sql +++ b/sqlite_export_for_ynab/ddl/drop-relations.sql @@ -1,4 +1,4 @@ -DROP TABLE IF EXISTS budgets; +DROP TABLE IF EXISTS plans; DROP TABLE IF EXISTS accounts; diff --git a/testing/fixtures.py b/testing/fixtures.py index 8a796f6..aa28c6f 100644 --- a/testing/fixtures.py +++ b/testing/fixtures.py @@ -11,13 +11,13 @@ from sqlite_export_for_ynab._main import _row_factory from sqlite_export_for_ynab._main import contents -BUDGET_ID_1 = str(uuid4()) -BUDGET_ID_2 = str(uuid4()) +PLAN_ID_1 = str(uuid4()) +PLAN_ID_2 = str(uuid4()) -BUDGETS: list[dict[str, Any]] = [ +PLANS: list[dict[str, Any]] = [ { - "id": BUDGET_ID_1, - "name": "Budget 1", + "id": PLAN_ID_1, + "name": "Plan 1", "currency_format": { "currency_symbol": "$", "decimal_digits": 2, @@ -30,8 +30,8 @@ }, }, { - "id": BUDGET_ID_2, - "name": "Budget 2", + "id": PLAN_ID_2, + "name": "Plan 2", "currency_format": { "currency_symbol": "$", "decimal_digits": 2, @@ -49,8 +49,8 @@ SERVER_KNOWLEDGE_2 = 107668 LKOS = { - BUDGET_ID_1: SERVER_KNOWLEDGE_1, - BUDGET_ID_2: SERVER_KNOWLEDGE_2, + PLAN_ID_1: SERVER_KNOWLEDGE_1, + PLAN_ID_2: SERVER_KNOWLEDGE_2, } ACCOUNT_ID_1 = str(uuid4()) @@ -280,7 +280,7 @@ def strip_nones(d: dict[str, Any]) -> dict[str, Any]: TOKEN = f"token-{uuid4()}" EXAMPLE_ENDPOINT_RE = re.compile(".+/example$") -BUDGETS_ENDPOINT_RE = re.compile(".+/budgets$") +PLANS_ENDPOINT_RE = re.compile(".+/plans$") ACCOUNTS_ENDPOINT_RE = re.compile(".+/accounts$") CATEGORIES_ENDPOINT_RE = re.compile(".+/categories$") PAYEES_ENDPOINT_RE = re.compile(".+/payees$") diff --git a/tests/_main_test.py b/tests/_main_test.py index a84fd22..86e23d4 100644 --- a/tests/_main_test.py +++ b/tests/_main_test.py @@ -20,9 +20,9 @@ from sqlite_export_for_ynab._main import get_last_knowledge_of_server from sqlite_export_for_ynab._main import get_relations from sqlite_export_for_ynab._main import insert_accounts -from sqlite_export_for_ynab._main import insert_budgets from sqlite_export_for_ynab._main import insert_category_groups from sqlite_export_for_ynab._main import insert_payees +from sqlite_export_for_ynab._main import insert_plans from sqlite_export_for_ynab._main import insert_scheduled_transactions from sqlite_export_for_ynab._main import insert_transactions from sqlite_export_for_ynab._main import main @@ -33,10 +33,6 @@ from testing.fixtures import ACCOUNT_ID_2 from testing.fixtures import ACCOUNTS from testing.fixtures import ACCOUNTS_ENDPOINT_RE -from testing.fixtures import BUDGET_ID_1 -from testing.fixtures import BUDGET_ID_2 -from testing.fixtures import BUDGETS -from testing.fixtures import BUDGETS_ENDPOINT_RE from testing.fixtures import CATEGORIES_ENDPOINT_RE from testing.fixtures import CATEGORY_GOAL_TARGET_DATE_1 from testing.fixtures import CATEGORY_GROUP_ID_1 @@ -60,6 +56,10 @@ from testing.fixtures import PAYEE_ID_2 from testing.fixtures import PAYEES from testing.fixtures import PAYEES_ENDPOINT_RE +from testing.fixtures import PLAN_ID_1 +from testing.fixtures import PLAN_ID_2 +from testing.fixtures import PLANS +from testing.fixtures import PLANS_ENDPOINT_RE from testing.fixtures import SCHEDULED_SUBTRANSACTION_ID_1 from testing.fixtures import SCHEDULED_SUBTRANSACTION_ID_2 from testing.fixtures import SCHEDULED_TRANSACTION_ID_1 @@ -98,18 +98,18 @@ def test_get_relations(cur): @pytest.mark.usefixtures(cur.__name__) def test_get_last_knowledge_of_server(cur): - insert_budgets(cur, BUDGETS, LKOS) + insert_plans(cur, PLANS, LKOS) assert get_last_knowledge_of_server(cur) == LKOS @pytest.mark.usefixtures(cur.__name__) -def test_insert_budgets(cur): - insert_budgets(cur, BUDGETS, LKOS) - cur.execute("SELECT * FROM budgets ORDER BY name") +def test_insert_plans(cur): + insert_plans(cur, PLANS, LKOS) + cur.execute("SELECT * FROM plans ORDER BY name") assert cur.fetchall() == [ { - "id": BUDGET_ID_1, - "name": BUDGETS[0]["name"], + "id": PLAN_ID_1, + "name": PLANS[0]["name"], "currency_format_currency_symbol": "$", "currency_format_decimal_digits": 2, "currency_format_decimal_separator": ".", @@ -117,11 +117,11 @@ def test_insert_budgets(cur): "currency_format_group_separator": ",", "currency_format_iso_code": "USD", "currency_format_symbol_first": 1, - "last_knowledge_of_server": LKOS[BUDGET_ID_1], + "last_knowledge_of_server": LKOS[PLAN_ID_1], }, { - "id": BUDGET_ID_2, - "name": BUDGETS[1]["name"], + "id": PLAN_ID_2, + "name": PLANS[1]["name"], "currency_format_currency_symbol": "$", "currency_format_decimal_digits": 2, "currency_format_decimal_separator": ".", @@ -129,29 +129,29 @@ def test_insert_budgets(cur): "currency_format_group_separator": ",", "currency_format_iso_code": "USD", "currency_format_symbol_first": 1, - "last_knowledge_of_server": LKOS[BUDGET_ID_2], + "last_knowledge_of_server": LKOS[PLAN_ID_2], }, ] @pytest.mark.usefixtures(cur.__name__) def test_insert_accounts(cur): - insert_accounts(cur, BUDGET_ID_1, []) + insert_accounts(cur, PLAN_ID_1, []) assert not cur.execute("SELECT * FROM accounts").fetchall() assert not cur.execute("SELECT * FROM account_periodic_values").fetchall() - insert_accounts(cur, BUDGET_ID_1, ACCOUNTS) + insert_accounts(cur, PLAN_ID_1, ACCOUNTS) cur.execute("SELECT * FROM accounts ORDER BY name") assert [strip_nones(d) for d in cur.fetchall()] == [ { "id": ACCOUNT_ID_1, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "name": ACCOUNTS[0]["name"], "type": ACCOUNTS[0]["type"], }, { "id": ACCOUNT_ID_2, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "name": ACCOUNTS[1]["name"], "type": ACCOUNTS[1]["type"], }, @@ -161,14 +161,14 @@ def test_insert_accounts(cur): assert cur.fetchall() == [ { "account_id": ACCOUNT_ID_1, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "name": "debt_escrow_amounts", "date": "2024-01-01", "amount": 160000, }, { "account_id": ACCOUNT_ID_1, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "name": "debt_interest_rates", "date": "2024-02-01", "amount": 5000, @@ -178,22 +178,22 @@ def test_insert_accounts(cur): @pytest.mark.usefixtures(cur.__name__) def test_insert_category_groups(cur): - insert_category_groups(cur, BUDGET_ID_1, []) + insert_category_groups(cur, PLAN_ID_1, []) assert not cur.execute("SELECT * FROM category_groups").fetchall() assert not cur.execute("SELECT * FROM categories").fetchall() - insert_category_groups(cur, BUDGET_ID_1, CATEGORY_GROUPS) + insert_category_groups(cur, PLAN_ID_1, CATEGORY_GROUPS) cur.execute("SELECT * FROM category_groups ORDER BY name") assert [strip_nones(d) for d in cur.fetchall()] == [ { "id": CATEGORY_GROUP_ID_1, "name": CATEGORY_GROUP_NAME_1, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, }, { "id": CATEGORY_GROUP_ID_2, "name": CATEGORY_GROUP_NAME_2, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, }, ] @@ -203,7 +203,7 @@ def test_insert_category_groups(cur): "id": CATEGORY_ID_1, "category_group_id": CATEGORY_GROUP_ID_1, "category_group_name": CATEGORY_GROUP_NAME_1, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "name": CATEGORY_NAME_1, "goal_target_date": CATEGORY_GOAL_TARGET_DATE_1, }, @@ -211,21 +211,21 @@ def test_insert_category_groups(cur): "id": CATEGORY_ID_2, "category_group_id": CATEGORY_GROUP_ID_1, "category_group_name": CATEGORY_GROUP_NAME_1, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "name": CATEGORY_NAME_2, }, { "id": CATEGORY_ID_3, "category_group_id": CATEGORY_GROUP_ID_2, "category_group_name": CATEGORY_GROUP_NAME_2, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "name": CATEGORY_NAME_3, }, { "id": CATEGORY_ID_4, "category_group_id": CATEGORY_GROUP_ID_2, "category_group_name": CATEGORY_GROUP_NAME_2, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "name": CATEGORY_NAME_4, }, ] @@ -233,20 +233,20 @@ def test_insert_category_groups(cur): @pytest.mark.usefixtures(cur.__name__) def test_insert_payees(cur): - insert_payees(cur, BUDGET_ID_1, []) + insert_payees(cur, PLAN_ID_1, []) assert not cur.execute("SELECT * FROM payees").fetchall() - insert_payees(cur, BUDGET_ID_1, PAYEES) + insert_payees(cur, PLAN_ID_1, PAYEES) cur.execute("SELECT * FROM payees ORDER BY name") assert [strip_nones(d) for d in cur.fetchall()] == [ { "id": PAYEE_ID_1, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "name": PAYEES[0]["name"], }, { "id": PAYEE_ID_2, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "name": PAYEES[1]["name"], }, ] @@ -254,17 +254,17 @@ def test_insert_payees(cur): @pytest.mark.usefixtures(cur.__name__) def test_insert_transactions(cur): - insert_transactions(cur, BUDGET_ID_1, []) + insert_transactions(cur, PLAN_ID_1, []) assert not cur.execute("SELECT * FROM transactions").fetchall() assert not cur.execute("SELECT * FROM subtransactions").fetchall() - insert_category_groups(cur, BUDGET_ID_1, CATEGORY_GROUPS) - insert_transactions(cur, BUDGET_ID_1, TRANSACTIONS) + insert_category_groups(cur, PLAN_ID_1, CATEGORY_GROUPS) + insert_transactions(cur, PLAN_ID_1, TRANSACTIONS) cur.execute("SELECT * FROM transactions ORDER BY date") assert [strip_nones(d) for d in cur.fetchall()] == [ { "id": TRANSACTION_ID_1, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "date": "2024-01-01", "amount": -10000, "category_id": CATEGORY_ID_3, @@ -273,7 +273,7 @@ def test_insert_transactions(cur): }, { "id": TRANSACTION_ID_2, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "date": "2024-02-01", "amount": -15000, "category_id": CATEGORY_ID_2, @@ -282,7 +282,7 @@ def test_insert_transactions(cur): }, { "id": TRANSACTION_ID_3, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "date": "2024-03-01", "amount": -19000, "category_id": CATEGORY_ID_4, @@ -296,7 +296,7 @@ def test_insert_transactions(cur): { "id": SUBTRANSACTION_ID_1, "transaction_id": TRANSACTION_ID_1, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "amount": -7500, "category_id": CATEGORY_ID_1, "category_name": CATEGORY_NAME_1, @@ -305,7 +305,7 @@ def test_insert_transactions(cur): { "id": SUBTRANSACTION_ID_2, "transaction_id": TRANSACTION_ID_1, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "amount": -2500, "category_id": CATEGORY_ID_2, "category_name": CATEGORY_NAME_2, @@ -317,7 +317,7 @@ def test_insert_transactions(cur): assert [strip_nones(d) for d in cur.fetchall()] == [ { "transaction_id": TRANSACTION_ID_3, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "date": "2024-03-01", "id": TRANSACTION_ID_3, "amount": -19000, @@ -330,7 +330,7 @@ def test_insert_transactions(cur): { "transaction_id": TRANSACTION_ID_1, "subtransaction_id": SUBTRANSACTION_ID_1, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "date": "2024-01-01", "id": SUBTRANSACTION_ID_1, "amount": -7500, @@ -343,7 +343,7 @@ def test_insert_transactions(cur): { "transaction_id": TRANSACTION_ID_1, "subtransaction_id": SUBTRANSACTION_ID_2, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "date": "2024-01-01", "id": SUBTRANSACTION_ID_2, "amount": -2500, @@ -358,17 +358,17 @@ def test_insert_transactions(cur): @pytest.mark.usefixtures(cur.__name__) def test_insert_scheduled_transactions(cur): - insert_scheduled_transactions(cur, BUDGET_ID_1, []) + insert_scheduled_transactions(cur, PLAN_ID_1, []) assert not cur.execute("SELECT * FROM scheduled_transactions").fetchall() assert not cur.execute("SELECT * FROM scheduled_subtransactions").fetchall() - insert_category_groups(cur, BUDGET_ID_1, CATEGORY_GROUPS) - insert_scheduled_transactions(cur, BUDGET_ID_1, SCHEDULED_TRANSACTIONS) + insert_category_groups(cur, PLAN_ID_1, CATEGORY_GROUPS) + insert_scheduled_transactions(cur, PLAN_ID_1, SCHEDULED_TRANSACTIONS) cur.execute("SELECT * FROM scheduled_transactions ORDER BY amount") assert [strip_nones(d) for d in cur.fetchall()] == [ { "id": SCHEDULED_TRANSACTION_ID_1, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "frequency": "monthly", "amount": -12000, "category_id": CATEGORY_ID_1, @@ -377,7 +377,7 @@ def test_insert_scheduled_transactions(cur): }, { "id": SCHEDULED_TRANSACTION_ID_2, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "frequency": "yearly", "amount": -11000, "category_id": CATEGORY_ID_3, @@ -386,7 +386,7 @@ def test_insert_scheduled_transactions(cur): }, { "id": SCHEDULED_TRANSACTION_ID_3, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "frequency": "everyOtherMonth", "amount": -9000, "category_id": CATEGORY_ID_4, @@ -400,7 +400,7 @@ def test_insert_scheduled_transactions(cur): { "id": SCHEDULED_SUBTRANSACTION_ID_1, "scheduled_transaction_id": SCHEDULED_TRANSACTION_ID_1, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "amount": -8040, "category_id": CATEGORY_ID_2, "category_name": CATEGORY_NAME_2, @@ -409,7 +409,7 @@ def test_insert_scheduled_transactions(cur): { "id": SCHEDULED_SUBTRANSACTION_ID_2, "scheduled_transaction_id": SCHEDULED_TRANSACTION_ID_1, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "amount": -2960, "category_id": CATEGORY_ID_3, "category_name": CATEGORY_NAME_3, @@ -421,7 +421,7 @@ def test_insert_scheduled_transactions(cur): assert [strip_nones(d) for d in cur.fetchall()] == [ { "transaction_id": SCHEDULED_TRANSACTION_ID_3, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "id": SCHEDULED_TRANSACTION_ID_3, "frequency": "everyOtherMonth", "amount": -9000, @@ -434,7 +434,7 @@ def test_insert_scheduled_transactions(cur): { "transaction_id": SCHEDULED_TRANSACTION_ID_1, "subtransaction_id": SCHEDULED_SUBTRANSACTION_ID_1, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "id": SCHEDULED_SUBTRANSACTION_ID_1, "frequency": "monthly", "amount": -8040, @@ -447,7 +447,7 @@ def test_insert_scheduled_transactions(cur): { "transaction_id": SCHEDULED_TRANSACTION_ID_1, "subtransaction_id": SCHEDULED_SUBTRANSACTION_ID_2, - "budget_id": BUDGET_ID_1, + "plan_id": PLAN_ID_1, "id": SCHEDULED_SUBTRANSACTION_ID_2, "frequency": "monthly", "amount": -2960, @@ -520,7 +520,7 @@ def test_main_no_token(tmp_path, monkeypatch): @pytest.mark.usefixtures(mock_aioresponses.__name__) async def test_sync_no_data(tmp_path, mock_aioresponses): mock_aioresponses.get( - BUDGETS_ENDPOINT_RE, body=json.dumps({"data": {"budgets": BUDGETS}}) + PLANS_ENDPOINT_RE, body=json.dumps({"data": {"plans": PLANS}}) ) mock_aioresponses.get( ACCOUNTS_ENDPOINT_RE, @@ -565,7 +565,7 @@ async def test_sync_no_data(tmp_path, mock_aioresponses): @pytest.mark.usefixtures(mock_aioresponses.__name__) async def test_sync(tmp_path, mock_aioresponses): mock_aioresponses.get( - BUDGETS_ENDPOINT_RE, body=json.dumps({"data": {"budgets": BUDGETS}}) + PLANS_ENDPOINT_RE, body=json.dumps({"data": {"plans": PLANS}}) ) mock_aioresponses.get( ACCOUNTS_ENDPOINT_RE,