sqlite-export-for-ynab 2.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlite_export_for_ynab/__init__.py +6 -0
- sqlite_export_for_ynab/__main__.py +6 -0
- sqlite_export_for_ynab/_main.py +687 -0
- sqlite_export_for_ynab/ddl/__init__.py +0 -0
- sqlite_export_for_ynab/ddl/create-relations.sql +334 -0
- sqlite_export_for_ynab/ddl/drop-relations.sql +23 -0
- sqlite_export_for_ynab/py.typed +0 -0
- sqlite_export_for_ynab-2.9.1.dist-info/METADATA +594 -0
- sqlite_export_for_ynab-2.9.1.dist-info/RECORD +17 -0
- sqlite_export_for_ynab-2.9.1.dist-info/WHEEL +5 -0
- sqlite_export_for_ynab-2.9.1.dist-info/entry_points.txt +2 -0
- sqlite_export_for_ynab-2.9.1.dist-info/licenses/LICENSE +21 -0
- sqlite_export_for_ynab-2.9.1.dist-info/top_level.txt +3 -0
- testing/__init__.py +0 -0
- testing/fixtures.py +626 -0
- tests/__init__.py +0 -0
- tests/_main_test.py +1125 -0
tests/_main_test.py
ADDED
|
@@ -0,0 +1,1125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tomllib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import AsyncMock
|
|
6
|
+
from unittest.mock import Mock
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
import aiosqlite
|
|
10
|
+
import fasteners
|
|
11
|
+
import pytest
|
|
12
|
+
import pytest_asyncio
|
|
13
|
+
from rich.progress import Progress
|
|
14
|
+
|
|
15
|
+
from sqlite_export_for_ynab import default_db_path
|
|
16
|
+
from sqlite_export_for_ynab._main import _ALL_RELATIONS
|
|
17
|
+
from sqlite_export_for_ynab._main import _Context
|
|
18
|
+
from sqlite_export_for_ynab._main import _context
|
|
19
|
+
from sqlite_export_for_ynab._main import _ENV_TOKEN
|
|
20
|
+
from sqlite_export_for_ynab._main import _get_plan_summaries
|
|
21
|
+
from sqlite_export_for_ynab._main import _PACKAGE
|
|
22
|
+
from sqlite_export_for_ynab._main import _PROGRESS_COLUMNS
|
|
23
|
+
from sqlite_export_for_ynab._main import async_main
|
|
24
|
+
from sqlite_export_for_ynab._main import asyncio_for_ynab
|
|
25
|
+
from sqlite_export_for_ynab._main import contents
|
|
26
|
+
from sqlite_export_for_ynab._main import get_last_knowledge_of_server
|
|
27
|
+
from sqlite_export_for_ynab._main import get_relations
|
|
28
|
+
from sqlite_export_for_ynab._main import insert_accounts
|
|
29
|
+
from sqlite_export_for_ynab._main import insert_category_groups
|
|
30
|
+
from sqlite_export_for_ynab._main import insert_entries
|
|
31
|
+
from sqlite_export_for_ynab._main import insert_payees
|
|
32
|
+
from sqlite_export_for_ynab._main import insert_plans
|
|
33
|
+
from sqlite_export_for_ynab._main import insert_scheduled_transactions
|
|
34
|
+
from sqlite_export_for_ynab._main import insert_transactions
|
|
35
|
+
from sqlite_export_for_ynab._main import main
|
|
36
|
+
from sqlite_export_for_ynab._main import resolve_token
|
|
37
|
+
from sqlite_export_for_ynab._main import sync
|
|
38
|
+
from testing.fixtures import ACCOUNT_ID_1
|
|
39
|
+
from testing.fixtures import ACCOUNT_ID_2
|
|
40
|
+
from testing.fixtures import ACCOUNTS
|
|
41
|
+
from testing.fixtures import accounts_response
|
|
42
|
+
from testing.fixtures import categories_response
|
|
43
|
+
from testing.fixtures import CATEGORY_GOAL_TARGET_DATE_1
|
|
44
|
+
from testing.fixtures import CATEGORY_GROUP_ID_1
|
|
45
|
+
from testing.fixtures import CATEGORY_GROUP_ID_2
|
|
46
|
+
from testing.fixtures import CATEGORY_GROUP_NAME_1
|
|
47
|
+
from testing.fixtures import CATEGORY_GROUP_NAME_2
|
|
48
|
+
from testing.fixtures import CATEGORY_GROUPS
|
|
49
|
+
from testing.fixtures import CATEGORY_ID_1
|
|
50
|
+
from testing.fixtures import CATEGORY_ID_2
|
|
51
|
+
from testing.fixtures import CATEGORY_ID_3
|
|
52
|
+
from testing.fixtures import CATEGORY_ID_4
|
|
53
|
+
from testing.fixtures import CATEGORY_NAME_1
|
|
54
|
+
from testing.fixtures import CATEGORY_NAME_2
|
|
55
|
+
from testing.fixtures import CATEGORY_NAME_3
|
|
56
|
+
from testing.fixtures import CATEGORY_NAME_4
|
|
57
|
+
from testing.fixtures import LKOS
|
|
58
|
+
from testing.fixtures import PAYEE_ID_1
|
|
59
|
+
from testing.fixtures import PAYEE_ID_2
|
|
60
|
+
from testing.fixtures import PAYEES
|
|
61
|
+
from testing.fixtures import payees_response
|
|
62
|
+
from testing.fixtures import PLAN_ID_1
|
|
63
|
+
from testing.fixtures import PLAN_ID_2
|
|
64
|
+
from testing.fixtures import plan_response
|
|
65
|
+
from testing.fixtures import PLANS
|
|
66
|
+
from testing.fixtures import SCHEDULED_SUBTRANSACTION_ID_1
|
|
67
|
+
from testing.fixtures import SCHEDULED_SUBTRANSACTION_ID_2
|
|
68
|
+
from testing.fixtures import SCHEDULED_TRANSACTION_ID_1
|
|
69
|
+
from testing.fixtures import SCHEDULED_TRANSACTION_ID_2
|
|
70
|
+
from testing.fixtures import SCHEDULED_TRANSACTION_ID_3
|
|
71
|
+
from testing.fixtures import SCHEDULED_TRANSACTIONS
|
|
72
|
+
from testing.fixtures import scheduled_transactions_response
|
|
73
|
+
from testing.fixtures import SERVER_KNOWLEDGE_1
|
|
74
|
+
from testing.fixtures import SUBTRANSACTION_ID_1
|
|
75
|
+
from testing.fixtures import SUBTRANSACTION_ID_2
|
|
76
|
+
from testing.fixtures import TOKEN
|
|
77
|
+
from testing.fixtures import TRANSACTION_ID_1
|
|
78
|
+
from testing.fixtures import TRANSACTION_ID_2
|
|
79
|
+
from testing.fixtures import TRANSACTION_ID_3
|
|
80
|
+
from testing.fixtures import TRANSACTIONS
|
|
81
|
+
from testing.fixtures import transactions_response
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def fetchall(con, query):
|
|
85
|
+
async with con.cursor() as cur:
|
|
86
|
+
await cur.execute(query)
|
|
87
|
+
return await cur.fetchall()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def assert_rows(rows, expected_rows):
|
|
91
|
+
assert len(rows) == len(expected_rows)
|
|
92
|
+
for row, expected in zip(rows, expected_rows, strict=True):
|
|
93
|
+
actual = dict(row)
|
|
94
|
+
assert {k: actual[k] for k in expected} == expected
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest_asyncio.fixture
|
|
98
|
+
async def context(tmp_path):
|
|
99
|
+
with Progress(*_PROGRESS_COLUMNS, disable=True) as progress:
|
|
100
|
+
async with aiosqlite.connect(":memory:") as con:
|
|
101
|
+
con.row_factory = aiosqlite.Row
|
|
102
|
+
await con.executescript(await contents("create-relations.sql"))
|
|
103
|
+
lock = fasteners.InterProcessLock(
|
|
104
|
+
tmp_path / "sqlite-export-for-ynab-test.lock"
|
|
105
|
+
)
|
|
106
|
+
yield _Context(progress, con, lock, Mock(spec=asyncio_for_ynab.ApiClient))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.mark.parametrize(
|
|
110
|
+
("xdg_data_home", "expected_prefix"),
|
|
111
|
+
(
|
|
112
|
+
("/tmp", Path("/tmp")),
|
|
113
|
+
("", Path.home() / ".local" / "share"),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
def test_default_db_path(monkeypatch, xdg_data_home, expected_prefix):
|
|
117
|
+
monkeypatch.setenv("XDG_DATA_HOME", xdg_data_home)
|
|
118
|
+
assert default_db_path() == expected_prefix / "sqlite-export-for-ynab" / "db.sqlite"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@pytest.mark.asyncio
|
|
122
|
+
async def test_get_relations(context):
|
|
123
|
+
async with context.con.cursor() as cur:
|
|
124
|
+
assert await get_relations(cur) == _ALL_RELATIONS
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.mark.asyncio
|
|
128
|
+
async def test_get_last_knowledge_of_server(context):
|
|
129
|
+
await insert_plans(context, PLANS, LKOS)
|
|
130
|
+
async with context.con.cursor() as cur:
|
|
131
|
+
assert await get_last_knowledge_of_server(cur) == LKOS
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@pytest.mark.asyncio
|
|
135
|
+
async def test_insert_plans(context):
|
|
136
|
+
await insert_plans(context, PLANS, LKOS)
|
|
137
|
+
assert_rows(
|
|
138
|
+
await fetchall(context.con, "SELECT * FROM plans ORDER BY name"),
|
|
139
|
+
[
|
|
140
|
+
{
|
|
141
|
+
"id": PLAN_ID_1,
|
|
142
|
+
"name": PLANS[0].name,
|
|
143
|
+
"currency_format_currency_symbol": "$",
|
|
144
|
+
"currency_format_decimal_digits": 2,
|
|
145
|
+
"currency_format_decimal_separator": ".",
|
|
146
|
+
"currency_format_display_symbol": 1,
|
|
147
|
+
"currency_format_group_separator": ",",
|
|
148
|
+
"currency_format_iso_code": "USD",
|
|
149
|
+
"currency_format_symbol_first": 1,
|
|
150
|
+
"last_knowledge_of_server": LKOS[PLAN_ID_1],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"id": PLAN_ID_2,
|
|
154
|
+
"name": PLANS[1].name,
|
|
155
|
+
"currency_format_currency_symbol": "$",
|
|
156
|
+
"currency_format_decimal_digits": 2,
|
|
157
|
+
"currency_format_decimal_separator": ".",
|
|
158
|
+
"currency_format_display_symbol": 1,
|
|
159
|
+
"currency_format_group_separator": ",",
|
|
160
|
+
"currency_format_iso_code": "USD",
|
|
161
|
+
"currency_format_symbol_first": 1,
|
|
162
|
+
"last_knowledge_of_server": LKOS[PLAN_ID_2],
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@pytest.mark.asyncio
|
|
169
|
+
async def test_insert_accounts(context):
|
|
170
|
+
await insert_accounts(context, PLAN_ID_1, [])
|
|
171
|
+
assert not await fetchall(context.con, "SELECT * FROM accounts")
|
|
172
|
+
assert not await fetchall(context.con, "SELECT * FROM account_periodic_values")
|
|
173
|
+
|
|
174
|
+
await insert_accounts(context, PLAN_ID_1, ACCOUNTS)
|
|
175
|
+
assert_rows(
|
|
176
|
+
await fetchall(context.con, "SELECT * FROM accounts ORDER BY name"),
|
|
177
|
+
[
|
|
178
|
+
{
|
|
179
|
+
"id": ACCOUNT_ID_1,
|
|
180
|
+
"plan_id": PLAN_ID_1,
|
|
181
|
+
"name": ACCOUNTS[0].name,
|
|
182
|
+
"type": ACCOUNTS[0].type,
|
|
183
|
+
"balance": 160000,
|
|
184
|
+
"balance_formatted": "$160.00",
|
|
185
|
+
"balance_currency": 160.0,
|
|
186
|
+
"cleared_balance": 120000,
|
|
187
|
+
"cleared_balance_formatted": "$120.00",
|
|
188
|
+
"cleared_balance_currency": 120.0,
|
|
189
|
+
"closed": False,
|
|
190
|
+
"deleted": False,
|
|
191
|
+
"on_budget": True,
|
|
192
|
+
"transfer_payee_id": PAYEE_ID_1,
|
|
193
|
+
"uncleared_balance": 40000,
|
|
194
|
+
"uncleared_balance_formatted": "$40.00",
|
|
195
|
+
"uncleared_balance_currency": 40.0,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"id": ACCOUNT_ID_2,
|
|
199
|
+
"plan_id": PLAN_ID_1,
|
|
200
|
+
"name": ACCOUNTS[1].name,
|
|
201
|
+
"type": ACCOUNTS[1].type,
|
|
202
|
+
"balance": 25000,
|
|
203
|
+
"balance_formatted": "$25.00",
|
|
204
|
+
"balance_currency": 25.0,
|
|
205
|
+
"cleared_balance": 25000,
|
|
206
|
+
"cleared_balance_formatted": "$25.00",
|
|
207
|
+
"cleared_balance_currency": 25.0,
|
|
208
|
+
"closed": False,
|
|
209
|
+
"deleted": False,
|
|
210
|
+
"on_budget": True,
|
|
211
|
+
"transfer_payee_id": PAYEE_ID_2,
|
|
212
|
+
"uncleared_balance": 0,
|
|
213
|
+
"uncleared_balance_formatted": "$0.00",
|
|
214
|
+
"uncleared_balance_currency": 0.0,
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
assert_rows(
|
|
220
|
+
await fetchall(
|
|
221
|
+
context.con, "SELECT * FROM account_periodic_values ORDER BY name"
|
|
222
|
+
),
|
|
223
|
+
[
|
|
224
|
+
{
|
|
225
|
+
"account_id": ACCOUNT_ID_1,
|
|
226
|
+
"plan_id": PLAN_ID_1,
|
|
227
|
+
"name": "debt_escrow_amounts",
|
|
228
|
+
"date": "2024-01-01",
|
|
229
|
+
"amount": 160000,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"account_id": ACCOUNT_ID_1,
|
|
233
|
+
"plan_id": PLAN_ID_1,
|
|
234
|
+
"name": "debt_interest_rates",
|
|
235
|
+
"date": "2024-02-01",
|
|
236
|
+
"amount": 5000,
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@pytest.mark.asyncio
|
|
243
|
+
async def test_insert_category_groups(context):
|
|
244
|
+
await insert_category_groups(context, PLAN_ID_1, [])
|
|
245
|
+
assert not await fetchall(context.con, "SELECT * FROM category_groups")
|
|
246
|
+
assert not await fetchall(context.con, "SELECT * FROM categories")
|
|
247
|
+
|
|
248
|
+
await insert_category_groups(context, PLAN_ID_1, CATEGORY_GROUPS)
|
|
249
|
+
assert_rows(
|
|
250
|
+
await fetchall(context.con, "SELECT * FROM category_groups ORDER BY name"),
|
|
251
|
+
[
|
|
252
|
+
{
|
|
253
|
+
"id": CATEGORY_GROUP_ID_1,
|
|
254
|
+
"name": CATEGORY_GROUP_NAME_1,
|
|
255
|
+
"plan_id": PLAN_ID_1,
|
|
256
|
+
"hidden": False,
|
|
257
|
+
"internal": False,
|
|
258
|
+
"deleted": False,
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"id": CATEGORY_GROUP_ID_2,
|
|
262
|
+
"name": CATEGORY_GROUP_NAME_2,
|
|
263
|
+
"plan_id": PLAN_ID_1,
|
|
264
|
+
"hidden": False,
|
|
265
|
+
"internal": False,
|
|
266
|
+
"deleted": False,
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
assert_rows(
|
|
272
|
+
await fetchall(context.con, "SELECT * FROM categories ORDER BY name"),
|
|
273
|
+
[
|
|
274
|
+
{
|
|
275
|
+
"id": CATEGORY_ID_1,
|
|
276
|
+
"category_group_id": CATEGORY_GROUP_ID_1,
|
|
277
|
+
"category_group_name": CATEGORY_GROUP_NAME_1,
|
|
278
|
+
"plan_id": PLAN_ID_1,
|
|
279
|
+
"name": CATEGORY_NAME_1,
|
|
280
|
+
"hidden": False,
|
|
281
|
+
"internal": False,
|
|
282
|
+
"original_category_group_id": None,
|
|
283
|
+
"note": None,
|
|
284
|
+
"budgeted": 14500,
|
|
285
|
+
"balance_formatted": "$12.00",
|
|
286
|
+
"balance_currency": 12.0,
|
|
287
|
+
"activity": 2500,
|
|
288
|
+
"activity_formatted": "$2.50",
|
|
289
|
+
"activity_currency": 2.5,
|
|
290
|
+
"goal_type": None,
|
|
291
|
+
"goal_needs_whole_amount": None,
|
|
292
|
+
"goal_day": None,
|
|
293
|
+
"goal_cadence": None,
|
|
294
|
+
"goal_cadence_frequency": None,
|
|
295
|
+
"goal_creation_month": None,
|
|
296
|
+
"goal_snoozed_at": None,
|
|
297
|
+
"goal_target": 20000,
|
|
298
|
+
"budgeted_formatted": "$14.50",
|
|
299
|
+
"budgeted_currency": 14.5,
|
|
300
|
+
"goal_target_formatted": "$20.00",
|
|
301
|
+
"goal_target_currency": 20.0,
|
|
302
|
+
"goal_target_date": CATEGORY_GOAL_TARGET_DATE_1,
|
|
303
|
+
"goal_target_month": None,
|
|
304
|
+
"goal_percentage_complete": None,
|
|
305
|
+
"goal_months_to_budget": None,
|
|
306
|
+
"goal_under_funded": 8000,
|
|
307
|
+
"goal_under_funded_formatted": "$8.00",
|
|
308
|
+
"goal_under_funded_currency": 8.0,
|
|
309
|
+
"goal_overall_funded": 12000,
|
|
310
|
+
"goal_overall_funded_formatted": "$12.00",
|
|
311
|
+
"goal_overall_funded_currency": 12.0,
|
|
312
|
+
"goal_overall_left": 8000,
|
|
313
|
+
"goal_overall_left_formatted": "$8.00",
|
|
314
|
+
"goal_overall_left_currency": 8.0,
|
|
315
|
+
"deleted": False,
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
"id": CATEGORY_ID_2,
|
|
319
|
+
"category_group_id": CATEGORY_GROUP_ID_1,
|
|
320
|
+
"category_group_name": CATEGORY_GROUP_NAME_1,
|
|
321
|
+
"plan_id": PLAN_ID_1,
|
|
322
|
+
"name": CATEGORY_NAME_2,
|
|
323
|
+
"hidden": False,
|
|
324
|
+
"internal": False,
|
|
325
|
+
"original_category_group_id": None,
|
|
326
|
+
"note": None,
|
|
327
|
+
"budgeted": 10250,
|
|
328
|
+
"balance_formatted": "$9.25",
|
|
329
|
+
"balance_currency": 9.25,
|
|
330
|
+
"activity": 1000,
|
|
331
|
+
"activity_formatted": "$1.00",
|
|
332
|
+
"activity_currency": 1.0,
|
|
333
|
+
"goal_type": None,
|
|
334
|
+
"goal_needs_whole_amount": None,
|
|
335
|
+
"goal_day": None,
|
|
336
|
+
"goal_cadence": None,
|
|
337
|
+
"goal_cadence_frequency": None,
|
|
338
|
+
"goal_creation_month": None,
|
|
339
|
+
"goal_snoozed_at": None,
|
|
340
|
+
"goal_target": None,
|
|
341
|
+
"budgeted_formatted": "$10.25",
|
|
342
|
+
"budgeted_currency": 10.25,
|
|
343
|
+
"goal_target_formatted": None,
|
|
344
|
+
"goal_target_currency": None,
|
|
345
|
+
"goal_target_date": None,
|
|
346
|
+
"goal_target_month": None,
|
|
347
|
+
"goal_percentage_complete": None,
|
|
348
|
+
"goal_months_to_budget": None,
|
|
349
|
+
"goal_under_funded": None,
|
|
350
|
+
"goal_under_funded_formatted": None,
|
|
351
|
+
"goal_under_funded_currency": None,
|
|
352
|
+
"goal_overall_funded": None,
|
|
353
|
+
"goal_overall_funded_formatted": None,
|
|
354
|
+
"goal_overall_funded_currency": None,
|
|
355
|
+
"goal_overall_left": None,
|
|
356
|
+
"goal_overall_left_formatted": None,
|
|
357
|
+
"goal_overall_left_currency": None,
|
|
358
|
+
"deleted": False,
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
"id": CATEGORY_ID_3,
|
|
362
|
+
"category_group_id": CATEGORY_GROUP_ID_2,
|
|
363
|
+
"category_group_name": CATEGORY_GROUP_NAME_2,
|
|
364
|
+
"plan_id": PLAN_ID_1,
|
|
365
|
+
"name": CATEGORY_NAME_3,
|
|
366
|
+
"hidden": False,
|
|
367
|
+
"internal": False,
|
|
368
|
+
"original_category_group_id": None,
|
|
369
|
+
"note": None,
|
|
370
|
+
"budgeted": 15000,
|
|
371
|
+
"balance_formatted": "$7.50",
|
|
372
|
+
"balance_currency": 7.5,
|
|
373
|
+
"activity": 7500,
|
|
374
|
+
"activity_formatted": "$7.50",
|
|
375
|
+
"activity_currency": 7.5,
|
|
376
|
+
"goal_type": None,
|
|
377
|
+
"goal_needs_whole_amount": None,
|
|
378
|
+
"goal_day": None,
|
|
379
|
+
"goal_cadence": None,
|
|
380
|
+
"goal_cadence_frequency": None,
|
|
381
|
+
"goal_creation_month": None,
|
|
382
|
+
"goal_snoozed_at": None,
|
|
383
|
+
"goal_target": None,
|
|
384
|
+
"budgeted_formatted": "$15.00",
|
|
385
|
+
"budgeted_currency": 15.0,
|
|
386
|
+
"goal_target_formatted": None,
|
|
387
|
+
"goal_target_currency": None,
|
|
388
|
+
"goal_target_date": None,
|
|
389
|
+
"goal_target_month": None,
|
|
390
|
+
"goal_percentage_complete": None,
|
|
391
|
+
"goal_months_to_budget": None,
|
|
392
|
+
"goal_under_funded": None,
|
|
393
|
+
"goal_under_funded_formatted": None,
|
|
394
|
+
"goal_under_funded_currency": None,
|
|
395
|
+
"goal_overall_funded": None,
|
|
396
|
+
"goal_overall_funded_formatted": None,
|
|
397
|
+
"goal_overall_funded_currency": None,
|
|
398
|
+
"goal_overall_left": None,
|
|
399
|
+
"goal_overall_left_formatted": None,
|
|
400
|
+
"goal_overall_left_currency": None,
|
|
401
|
+
"deleted": False,
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
"id": CATEGORY_ID_4,
|
|
405
|
+
"category_group_id": CATEGORY_GROUP_ID_2,
|
|
406
|
+
"category_group_name": CATEGORY_GROUP_NAME_2,
|
|
407
|
+
"plan_id": PLAN_ID_1,
|
|
408
|
+
"name": CATEGORY_NAME_4,
|
|
409
|
+
"hidden": False,
|
|
410
|
+
"internal": False,
|
|
411
|
+
"original_category_group_id": None,
|
|
412
|
+
"note": None,
|
|
413
|
+
"budgeted": 20000,
|
|
414
|
+
"balance_formatted": "$19.00",
|
|
415
|
+
"balance_currency": 19.0,
|
|
416
|
+
"activity": 19000,
|
|
417
|
+
"activity_formatted": "$19.00",
|
|
418
|
+
"activity_currency": 19.0,
|
|
419
|
+
"goal_type": None,
|
|
420
|
+
"goal_needs_whole_amount": None,
|
|
421
|
+
"goal_day": None,
|
|
422
|
+
"goal_cadence": None,
|
|
423
|
+
"goal_cadence_frequency": None,
|
|
424
|
+
"goal_creation_month": None,
|
|
425
|
+
"goal_snoozed_at": None,
|
|
426
|
+
"goal_target": None,
|
|
427
|
+
"budgeted_formatted": "$20.00",
|
|
428
|
+
"budgeted_currency": 20.0,
|
|
429
|
+
"goal_target_formatted": None,
|
|
430
|
+
"goal_target_currency": None,
|
|
431
|
+
"goal_target_date": None,
|
|
432
|
+
"goal_target_month": None,
|
|
433
|
+
"goal_percentage_complete": None,
|
|
434
|
+
"goal_months_to_budget": None,
|
|
435
|
+
"goal_under_funded": None,
|
|
436
|
+
"goal_under_funded_formatted": None,
|
|
437
|
+
"goal_under_funded_currency": None,
|
|
438
|
+
"goal_overall_funded": None,
|
|
439
|
+
"goal_overall_funded_formatted": None,
|
|
440
|
+
"goal_overall_funded_currency": None,
|
|
441
|
+
"goal_overall_left": None,
|
|
442
|
+
"goal_overall_left_formatted": None,
|
|
443
|
+
"goal_overall_left_currency": None,
|
|
444
|
+
"deleted": False,
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
@pytest.mark.asyncio
|
|
451
|
+
async def test_insert_category_group_without_categories(context):
|
|
452
|
+
await insert_category_groups(
|
|
453
|
+
context,
|
|
454
|
+
PLAN_ID_1,
|
|
455
|
+
[CATEGORY_GROUPS[0].model_copy(update={"categories": []})],
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
assert_rows(
|
|
459
|
+
await fetchall(context.con, "SELECT * FROM category_groups ORDER BY name"),
|
|
460
|
+
[
|
|
461
|
+
{
|
|
462
|
+
"id": CATEGORY_GROUP_ID_1,
|
|
463
|
+
"name": CATEGORY_GROUP_NAME_1,
|
|
464
|
+
"plan_id": PLAN_ID_1,
|
|
465
|
+
"hidden": False,
|
|
466
|
+
"internal": False,
|
|
467
|
+
"deleted": False,
|
|
468
|
+
},
|
|
469
|
+
],
|
|
470
|
+
)
|
|
471
|
+
assert not await fetchall(context.con, "SELECT * FROM categories")
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@pytest.mark.asyncio
|
|
475
|
+
async def test_insert_payees(context):
|
|
476
|
+
await insert_payees(context, PLAN_ID_1, [])
|
|
477
|
+
assert not await fetchall(context.con, "SELECT * FROM payees")
|
|
478
|
+
|
|
479
|
+
await insert_payees(context, PLAN_ID_1, PAYEES)
|
|
480
|
+
assert_rows(
|
|
481
|
+
await fetchall(context.con, "SELECT * FROM payees ORDER BY name"),
|
|
482
|
+
[
|
|
483
|
+
{
|
|
484
|
+
"id": PAYEE_ID_1,
|
|
485
|
+
"plan_id": PLAN_ID_1,
|
|
486
|
+
"name": PAYEES[0].name,
|
|
487
|
+
"transfer_account_id": None,
|
|
488
|
+
"deleted": False,
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
"id": PAYEE_ID_2,
|
|
492
|
+
"plan_id": PLAN_ID_1,
|
|
493
|
+
"name": PAYEES[1].name,
|
|
494
|
+
"transfer_account_id": None,
|
|
495
|
+
"deleted": False,
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@pytest.mark.asyncio
|
|
502
|
+
async def test_insert_entries_ignores_unknown_keys(context):
|
|
503
|
+
task_id = context.progress.add_task("Payees", total=1)
|
|
504
|
+
entry = {
|
|
505
|
+
"id": PAYEE_ID_1,
|
|
506
|
+
"name": "Payee",
|
|
507
|
+
"transfer_account_id": None,
|
|
508
|
+
"deleted": False,
|
|
509
|
+
"brand_new_api_field": "surprise",
|
|
510
|
+
}
|
|
511
|
+
await insert_entries(context, "payees", PLAN_ID_1, [entry], task_id)
|
|
512
|
+
assert_rows(
|
|
513
|
+
await fetchall(context.con, "SELECT * FROM payees"),
|
|
514
|
+
[
|
|
515
|
+
{
|
|
516
|
+
"id": PAYEE_ID_1,
|
|
517
|
+
"plan_id": PLAN_ID_1,
|
|
518
|
+
"name": "Payee",
|
|
519
|
+
"transfer_account_id": None,
|
|
520
|
+
"deleted": False,
|
|
521
|
+
}
|
|
522
|
+
],
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
@pytest.mark.asyncio
|
|
527
|
+
async def test_insert_transactions(context):
|
|
528
|
+
await insert_transactions(context, PLAN_ID_1, [])
|
|
529
|
+
assert not await fetchall(context.con, "SELECT * FROM transactions")
|
|
530
|
+
assert not await fetchall(context.con, "SELECT * FROM subtransactions")
|
|
531
|
+
|
|
532
|
+
await insert_category_groups(context, PLAN_ID_1, CATEGORY_GROUPS)
|
|
533
|
+
await insert_transactions(context, PLAN_ID_1, TRANSACTIONS)
|
|
534
|
+
assert_rows(
|
|
535
|
+
await fetchall(context.con, "SELECT * FROM transactions ORDER BY date"),
|
|
536
|
+
[
|
|
537
|
+
{
|
|
538
|
+
"id": TRANSACTION_ID_1,
|
|
539
|
+
"plan_id": PLAN_ID_1,
|
|
540
|
+
"account_id": ACCOUNT_ID_1,
|
|
541
|
+
"account_name": "Account 1",
|
|
542
|
+
"date": "2024-01-01",
|
|
543
|
+
"amount": -10000,
|
|
544
|
+
"amount_formatted": "$10.00",
|
|
545
|
+
"amount_currency": 10.0,
|
|
546
|
+
"approved": 1,
|
|
547
|
+
"category_id": CATEGORY_ID_3,
|
|
548
|
+
"category_name": CATEGORY_NAME_3,
|
|
549
|
+
"cleared": "cleared",
|
|
550
|
+
"deleted": False,
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
"id": TRANSACTION_ID_2,
|
|
554
|
+
"plan_id": PLAN_ID_1,
|
|
555
|
+
"account_id": ACCOUNT_ID_1,
|
|
556
|
+
"account_name": "Account 1",
|
|
557
|
+
"date": "2024-02-01",
|
|
558
|
+
"amount": -15000,
|
|
559
|
+
"amount_formatted": "$15.00",
|
|
560
|
+
"amount_currency": 15.0,
|
|
561
|
+
"approved": 1,
|
|
562
|
+
"category_id": CATEGORY_ID_2,
|
|
563
|
+
"category_name": CATEGORY_NAME_2,
|
|
564
|
+
"cleared": "cleared",
|
|
565
|
+
"deleted": True,
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
"id": TRANSACTION_ID_3,
|
|
569
|
+
"plan_id": PLAN_ID_1,
|
|
570
|
+
"account_id": ACCOUNT_ID_1,
|
|
571
|
+
"account_name": "Account 1",
|
|
572
|
+
"date": "2024-03-01",
|
|
573
|
+
"amount": -19000,
|
|
574
|
+
"amount_formatted": "$19.00",
|
|
575
|
+
"amount_currency": 19.0,
|
|
576
|
+
"approved": 0,
|
|
577
|
+
"category_id": CATEGORY_ID_4,
|
|
578
|
+
"category_name": CATEGORY_NAME_4,
|
|
579
|
+
"cleared": "uncleared",
|
|
580
|
+
"deleted": False,
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
assert_rows(
|
|
586
|
+
await fetchall(context.con, "SELECT * FROM subtransactions ORDER BY amount"),
|
|
587
|
+
[
|
|
588
|
+
{
|
|
589
|
+
"id": SUBTRANSACTION_ID_1,
|
|
590
|
+
"transaction_id": TRANSACTION_ID_1,
|
|
591
|
+
"plan_id": PLAN_ID_1,
|
|
592
|
+
"amount": -7500,
|
|
593
|
+
"amount_formatted": "$7.50",
|
|
594
|
+
"amount_currency": 7.5,
|
|
595
|
+
"category_id": CATEGORY_ID_1,
|
|
596
|
+
"category_name": CATEGORY_NAME_1,
|
|
597
|
+
"deleted": False,
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
"id": SUBTRANSACTION_ID_2,
|
|
601
|
+
"transaction_id": TRANSACTION_ID_1,
|
|
602
|
+
"plan_id": PLAN_ID_1,
|
|
603
|
+
"amount": -2500,
|
|
604
|
+
"amount_formatted": "$2.50",
|
|
605
|
+
"amount_currency": 2.5,
|
|
606
|
+
"category_id": CATEGORY_ID_2,
|
|
607
|
+
"category_name": CATEGORY_NAME_2,
|
|
608
|
+
"deleted": False,
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
assert_rows(
|
|
614
|
+
await fetchall(context.con, "SELECT * FROM flat_transactions ORDER BY amount"),
|
|
615
|
+
[
|
|
616
|
+
{
|
|
617
|
+
"transaction_id": TRANSACTION_ID_1,
|
|
618
|
+
"subtransaction_id": SUBTRANSACTION_ID_1,
|
|
619
|
+
"plan_id": PLAN_ID_1,
|
|
620
|
+
"account_id": ACCOUNT_ID_1,
|
|
621
|
+
"account_name": "Account 1",
|
|
622
|
+
"cleared": "cleared",
|
|
623
|
+
"date": "2024-01-01",
|
|
624
|
+
"debt_transaction_type": None,
|
|
625
|
+
"id": SUBTRANSACTION_ID_1,
|
|
626
|
+
"amount": -7500,
|
|
627
|
+
"amount_formatted": "$7.50",
|
|
628
|
+
"amount_currency": 7.5,
|
|
629
|
+
"category_id": CATEGORY_ID_1,
|
|
630
|
+
"category_name": CATEGORY_NAME_1,
|
|
631
|
+
"category_group_id": CATEGORY_GROUP_ID_1,
|
|
632
|
+
"category_group_name": CATEGORY_GROUP_NAME_1,
|
|
633
|
+
"flag_color": None,
|
|
634
|
+
"flag_name": None,
|
|
635
|
+
"import_id": None,
|
|
636
|
+
"import_payee_name": None,
|
|
637
|
+
"import_payee_name_original": None,
|
|
638
|
+
"matched_transaction_id": None,
|
|
639
|
+
"memo": None,
|
|
640
|
+
"payee_id": None,
|
|
641
|
+
"payee_name": None,
|
|
642
|
+
"transfer_account_id": None,
|
|
643
|
+
"transfer_transaction_id": None,
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
"transaction_id": TRANSACTION_ID_1,
|
|
647
|
+
"subtransaction_id": SUBTRANSACTION_ID_2,
|
|
648
|
+
"plan_id": PLAN_ID_1,
|
|
649
|
+
"account_id": ACCOUNT_ID_1,
|
|
650
|
+
"account_name": "Account 1",
|
|
651
|
+
"cleared": "cleared",
|
|
652
|
+
"date": "2024-01-01",
|
|
653
|
+
"debt_transaction_type": None,
|
|
654
|
+
"id": SUBTRANSACTION_ID_2,
|
|
655
|
+
"amount": -2500,
|
|
656
|
+
"amount_formatted": "$2.50",
|
|
657
|
+
"amount_currency": 2.5,
|
|
658
|
+
"category_id": CATEGORY_ID_2,
|
|
659
|
+
"category_name": CATEGORY_NAME_2,
|
|
660
|
+
"category_group_id": CATEGORY_GROUP_ID_1,
|
|
661
|
+
"category_group_name": CATEGORY_GROUP_NAME_1,
|
|
662
|
+
"flag_color": None,
|
|
663
|
+
"flag_name": None,
|
|
664
|
+
"import_id": None,
|
|
665
|
+
"import_payee_name": None,
|
|
666
|
+
"import_payee_name_original": None,
|
|
667
|
+
"matched_transaction_id": None,
|
|
668
|
+
"memo": None,
|
|
669
|
+
"payee_id": None,
|
|
670
|
+
"payee_name": None,
|
|
671
|
+
"transfer_account_id": None,
|
|
672
|
+
"transfer_transaction_id": None,
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
@pytest.mark.asyncio
|
|
679
|
+
async def test_insert_scheduled_transactions(context):
|
|
680
|
+
await insert_scheduled_transactions(context, PLAN_ID_1, [])
|
|
681
|
+
assert not await fetchall(context.con, "SELECT * FROM scheduled_transactions")
|
|
682
|
+
assert not await fetchall(context.con, "SELECT * FROM scheduled_subtransactions")
|
|
683
|
+
|
|
684
|
+
await insert_category_groups(context, PLAN_ID_1, CATEGORY_GROUPS)
|
|
685
|
+
await insert_scheduled_transactions(context, PLAN_ID_1, SCHEDULED_TRANSACTIONS)
|
|
686
|
+
assert_rows(
|
|
687
|
+
await fetchall(
|
|
688
|
+
context.con, "SELECT * FROM scheduled_transactions ORDER BY amount"
|
|
689
|
+
),
|
|
690
|
+
[
|
|
691
|
+
{
|
|
692
|
+
"id": SCHEDULED_TRANSACTION_ID_1,
|
|
693
|
+
"plan_id": PLAN_ID_1,
|
|
694
|
+
"account_id": ACCOUNT_ID_1,
|
|
695
|
+
"account_name": "Account 1",
|
|
696
|
+
"date_first": "2024-01-01",
|
|
697
|
+
"date_next": "2024-01-01",
|
|
698
|
+
"frequency": "monthly",
|
|
699
|
+
"amount": -12000,
|
|
700
|
+
"amount_formatted": "$12.00",
|
|
701
|
+
"amount_currency": 12.0,
|
|
702
|
+
"category_id": CATEGORY_ID_1,
|
|
703
|
+
"category_name": CATEGORY_NAME_1,
|
|
704
|
+
"flag_color": None,
|
|
705
|
+
"flag_name": None,
|
|
706
|
+
"deleted": False,
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
"id": SCHEDULED_TRANSACTION_ID_2,
|
|
710
|
+
"plan_id": PLAN_ID_1,
|
|
711
|
+
"account_id": ACCOUNT_ID_1,
|
|
712
|
+
"account_name": "Account 1",
|
|
713
|
+
"date_first": "2024-02-01",
|
|
714
|
+
"date_next": "2024-02-01",
|
|
715
|
+
"frequency": "yearly",
|
|
716
|
+
"amount": -11000,
|
|
717
|
+
"amount_formatted": "$11.00",
|
|
718
|
+
"amount_currency": 11.0,
|
|
719
|
+
"category_id": CATEGORY_ID_3,
|
|
720
|
+
"category_name": CATEGORY_NAME_3,
|
|
721
|
+
"flag_color": None,
|
|
722
|
+
"flag_name": None,
|
|
723
|
+
"deleted": True,
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
"id": SCHEDULED_TRANSACTION_ID_3,
|
|
727
|
+
"plan_id": PLAN_ID_1,
|
|
728
|
+
"account_id": ACCOUNT_ID_1,
|
|
729
|
+
"account_name": "Account 1",
|
|
730
|
+
"date_first": "2024-03-01",
|
|
731
|
+
"date_next": "2024-03-01",
|
|
732
|
+
"frequency": "everyOtherMonth",
|
|
733
|
+
"amount": -9000,
|
|
734
|
+
"amount_formatted": "$9.00",
|
|
735
|
+
"amount_currency": 9.0,
|
|
736
|
+
"category_id": CATEGORY_ID_4,
|
|
737
|
+
"category_name": CATEGORY_NAME_4,
|
|
738
|
+
"flag_color": None,
|
|
739
|
+
"flag_name": None,
|
|
740
|
+
"deleted": False,
|
|
741
|
+
},
|
|
742
|
+
],
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
assert_rows(
|
|
746
|
+
await fetchall(
|
|
747
|
+
context.con, "SELECT * FROM scheduled_subtransactions ORDER BY amount"
|
|
748
|
+
),
|
|
749
|
+
[
|
|
750
|
+
{
|
|
751
|
+
"id": SCHEDULED_SUBTRANSACTION_ID_1,
|
|
752
|
+
"scheduled_transaction_id": SCHEDULED_TRANSACTION_ID_1,
|
|
753
|
+
"plan_id": PLAN_ID_1,
|
|
754
|
+
"amount": -8040,
|
|
755
|
+
"amount_formatted": "$8.04",
|
|
756
|
+
"amount_currency": 8.04,
|
|
757
|
+
"category_id": CATEGORY_ID_2,
|
|
758
|
+
"category_name": CATEGORY_NAME_2,
|
|
759
|
+
"deleted": False,
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
"id": SCHEDULED_SUBTRANSACTION_ID_2,
|
|
763
|
+
"scheduled_transaction_id": SCHEDULED_TRANSACTION_ID_1,
|
|
764
|
+
"plan_id": PLAN_ID_1,
|
|
765
|
+
"amount": -2960,
|
|
766
|
+
"amount_formatted": "$2.96",
|
|
767
|
+
"amount_currency": 2.96,
|
|
768
|
+
"category_id": CATEGORY_ID_3,
|
|
769
|
+
"category_name": CATEGORY_NAME_3,
|
|
770
|
+
"deleted": False,
|
|
771
|
+
},
|
|
772
|
+
],
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
assert_rows(
|
|
776
|
+
await fetchall(
|
|
777
|
+
context.con, "SELECT * FROM scheduled_flat_transactions ORDER BY amount"
|
|
778
|
+
),
|
|
779
|
+
[
|
|
780
|
+
{
|
|
781
|
+
"transaction_id": SCHEDULED_TRANSACTION_ID_3,
|
|
782
|
+
"plan_id": PLAN_ID_1,
|
|
783
|
+
"account_id": ACCOUNT_ID_1,
|
|
784
|
+
"account_name": "Account 1",
|
|
785
|
+
"date_first": "2024-03-01",
|
|
786
|
+
"date_next": "2024-03-01",
|
|
787
|
+
"id": SCHEDULED_TRANSACTION_ID_3,
|
|
788
|
+
"frequency": "everyOtherMonth",
|
|
789
|
+
"amount": -9000,
|
|
790
|
+
"amount_formatted": "$9.00",
|
|
791
|
+
"amount_currency": 9.0,
|
|
792
|
+
"category_id": CATEGORY_ID_4,
|
|
793
|
+
"category_name": CATEGORY_NAME_4,
|
|
794
|
+
"category_group_id": CATEGORY_GROUP_ID_2,
|
|
795
|
+
"category_group_name": CATEGORY_GROUP_NAME_2,
|
|
796
|
+
"flag_color": None,
|
|
797
|
+
"flag_name": None,
|
|
798
|
+
"memo": None,
|
|
799
|
+
"payee_name": None,
|
|
800
|
+
"payee_id": None,
|
|
801
|
+
"transfer_account_id": None,
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
"transaction_id": SCHEDULED_TRANSACTION_ID_1,
|
|
805
|
+
"subtransaction_id": SCHEDULED_SUBTRANSACTION_ID_1,
|
|
806
|
+
"plan_id": PLAN_ID_1,
|
|
807
|
+
"account_id": ACCOUNT_ID_1,
|
|
808
|
+
"account_name": "Account 1",
|
|
809
|
+
"date_first": "2024-01-01",
|
|
810
|
+
"date_next": "2024-01-01",
|
|
811
|
+
"id": SCHEDULED_SUBTRANSACTION_ID_1,
|
|
812
|
+
"frequency": "monthly",
|
|
813
|
+
"amount": -8040,
|
|
814
|
+
"amount_formatted": "$8.04",
|
|
815
|
+
"amount_currency": 8.04,
|
|
816
|
+
"category_id": CATEGORY_ID_2,
|
|
817
|
+
"category_name": CATEGORY_NAME_2,
|
|
818
|
+
"category_group_id": CATEGORY_GROUP_ID_1,
|
|
819
|
+
"category_group_name": CATEGORY_GROUP_NAME_1,
|
|
820
|
+
"flag_color": None,
|
|
821
|
+
"flag_name": None,
|
|
822
|
+
"memo": None,
|
|
823
|
+
"payee_name": None,
|
|
824
|
+
"payee_id": None,
|
|
825
|
+
"transfer_account_id": None,
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
"transaction_id": SCHEDULED_TRANSACTION_ID_1,
|
|
829
|
+
"subtransaction_id": SCHEDULED_SUBTRANSACTION_ID_2,
|
|
830
|
+
"plan_id": PLAN_ID_1,
|
|
831
|
+
"account_id": ACCOUNT_ID_1,
|
|
832
|
+
"account_name": "Account 1",
|
|
833
|
+
"date_first": "2024-01-01",
|
|
834
|
+
"date_next": "2024-01-01",
|
|
835
|
+
"id": SCHEDULED_SUBTRANSACTION_ID_2,
|
|
836
|
+
"frequency": "monthly",
|
|
837
|
+
"amount": -2960,
|
|
838
|
+
"amount_formatted": "$2.96",
|
|
839
|
+
"amount_currency": 2.96,
|
|
840
|
+
"category_id": CATEGORY_ID_3,
|
|
841
|
+
"category_name": CATEGORY_NAME_3,
|
|
842
|
+
"category_group_id": CATEGORY_GROUP_ID_2,
|
|
843
|
+
"category_group_name": CATEGORY_GROUP_NAME_2,
|
|
844
|
+
"flag_color": None,
|
|
845
|
+
"flag_name": None,
|
|
846
|
+
"memo": None,
|
|
847
|
+
"payee_name": None,
|
|
848
|
+
"payee_id": None,
|
|
849
|
+
"transfer_account_id": None,
|
|
850
|
+
},
|
|
851
|
+
],
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
@patch(
|
|
856
|
+
"sqlite_export_for_ynab._main.PlansApi.get_plans",
|
|
857
|
+
new=AsyncMock(side_effect=[RuntimeError("boom"), plan_response(PLANS)]),
|
|
858
|
+
)
|
|
859
|
+
@pytest.mark.asyncio
|
|
860
|
+
async def test_get_plan_summaries_retries():
|
|
861
|
+
assert await _get_plan_summaries(Mock(spec=asyncio_for_ynab.ApiClient)) == PLANS
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
@patch("sqlite_export_for_ynab._main.sync")
|
|
865
|
+
@pytest.mark.asyncio
|
|
866
|
+
async def test_async_main_parses_full_refresh_and_quiet(sync, tmp_path, monkeypatch):
|
|
867
|
+
monkeypatch.setenv(_ENV_TOKEN, TOKEN)
|
|
868
|
+
|
|
869
|
+
ret = await async_main(
|
|
870
|
+
("--db", str(tmp_path / "db.sqlite"), "--full-refresh", "--quiet")
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
sync.assert_called_once_with(TOKEN, tmp_path / "db.sqlite", True, quiet=True)
|
|
874
|
+
assert ret == 0
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def test_main_version(capsys):
|
|
878
|
+
with open(Path(__file__).parent.parent / "pyproject.toml", "rb") as f:
|
|
879
|
+
data = tomllib.load(f)
|
|
880
|
+
expected_version = data["project"]["version"]
|
|
881
|
+
|
|
882
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
883
|
+
main(("--version",))
|
|
884
|
+
assert excinfo.value.code == 0
|
|
885
|
+
|
|
886
|
+
out, _ = capsys.readouterr()
|
|
887
|
+
assert out == f"{_PACKAGE} {expected_version}\n"
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
@patch("sqlite_export_for_ynab._main.sync")
|
|
891
|
+
def test_main_ok(sync, tmp_path, monkeypatch):
|
|
892
|
+
monkeypatch.setenv(_ENV_TOKEN, TOKEN)
|
|
893
|
+
|
|
894
|
+
ret = main(("--db", str(tmp_path / "db.sqlite")))
|
|
895
|
+
sync.assert_called_once_with(TOKEN, tmp_path / "db.sqlite", False, quiet=False)
|
|
896
|
+
assert ret == 0
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def test_main_no_token(tmp_path, monkeypatch):
|
|
900
|
+
monkeypatch.setenv(_ENV_TOKEN, "")
|
|
901
|
+
|
|
902
|
+
with pytest.raises(ValueError):
|
|
903
|
+
main(("--db", str(tmp_path / "db.sqlite")))
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
@patch("sqlite_export_for_ynab._main.sync")
|
|
907
|
+
def test_main_uses_token_override(sync, tmp_path, monkeypatch):
|
|
908
|
+
monkeypatch.delenv(_ENV_TOKEN, raising=False)
|
|
909
|
+
|
|
910
|
+
ret = main(("--db", str(tmp_path / "db.sqlite")), token_override="override-token")
|
|
911
|
+
|
|
912
|
+
sync.assert_called_once_with(
|
|
913
|
+
"override-token", tmp_path / "db.sqlite", False, quiet=False
|
|
914
|
+
)
|
|
915
|
+
assert ret == 0
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
@patch("sqlite_export_for_ynab._main.sync")
|
|
919
|
+
def test_main_quiet(sync, tmp_path, monkeypatch):
|
|
920
|
+
monkeypatch.setenv(_ENV_TOKEN, TOKEN)
|
|
921
|
+
|
|
922
|
+
ret = main(("--db", str(tmp_path / "db.sqlite"), "--quiet"))
|
|
923
|
+
|
|
924
|
+
sync.assert_called_once_with(TOKEN, tmp_path / "db.sqlite", False, quiet=True)
|
|
925
|
+
assert ret == 0
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def test_resolve_token_override(monkeypatch):
|
|
929
|
+
monkeypatch.delenv(_ENV_TOKEN, raising=False)
|
|
930
|
+
|
|
931
|
+
assert resolve_token("override-token") == "override-token"
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def test_resolve_token_env(monkeypatch):
|
|
935
|
+
monkeypatch.setenv(_ENV_TOKEN, TOKEN)
|
|
936
|
+
|
|
937
|
+
assert resolve_token() == TOKEN
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
@patch(
|
|
941
|
+
"sqlite_export_for_ynab._main.fasteners.InterProcessLock.acquire",
|
|
942
|
+
autospec=True,
|
|
943
|
+
)
|
|
944
|
+
@patch("sqlite_export_for_ynab._main.asyncio.get_running_loop")
|
|
945
|
+
@pytest.mark.asyncio
|
|
946
|
+
async def test_sync_lock_times_out(mock_get_running_loop, mock_acquire, tmp_path):
|
|
947
|
+
class FakeLoop:
|
|
948
|
+
def __init__(self):
|
|
949
|
+
self._times = iter((0.0, 0.2))
|
|
950
|
+
|
|
951
|
+
def time(self):
|
|
952
|
+
return next(self._times)
|
|
953
|
+
|
|
954
|
+
mock_get_running_loop.return_value = FakeLoop()
|
|
955
|
+
mock_acquire.return_value = False
|
|
956
|
+
|
|
957
|
+
with pytest.raises(TimeoutError):
|
|
958
|
+
await _context(
|
|
959
|
+
tmp_path / "db.sqlite",
|
|
960
|
+
asyncio_for_ynab.Configuration(access_token=TOKEN),
|
|
961
|
+
quiet=True,
|
|
962
|
+
timeout=0.1,
|
|
963
|
+
).__aenter__()
|
|
964
|
+
|
|
965
|
+
assert mock_acquire.call_count == 1
|
|
966
|
+
assert mock_acquire.call_args.args[1] is False
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
@patch(
|
|
970
|
+
"sqlite_export_for_ynab._main.fasteners.InterProcessLock.acquire",
|
|
971
|
+
autospec=True,
|
|
972
|
+
)
|
|
973
|
+
@patch("sqlite_export_for_ynab._main.fasteners.InterProcessLock.release", autospec=True)
|
|
974
|
+
@patch("sqlite_export_for_ynab._main.asyncio.sleep")
|
|
975
|
+
@patch("sqlite_export_for_ynab._main.asyncio.get_running_loop")
|
|
976
|
+
@pytest.mark.asyncio
|
|
977
|
+
async def test_context_retries_after_sleep(
|
|
978
|
+
mock_get_running_loop,
|
|
979
|
+
mock_sleep,
|
|
980
|
+
mock_release,
|
|
981
|
+
mock_acquire,
|
|
982
|
+
tmp_path,
|
|
983
|
+
):
|
|
984
|
+
class FakeLoop:
|
|
985
|
+
def __init__(self):
|
|
986
|
+
self._times = iter((0.0, 0.0, 0.2))
|
|
987
|
+
|
|
988
|
+
def time(self):
|
|
989
|
+
return next(self._times)
|
|
990
|
+
|
|
991
|
+
mock_get_running_loop.return_value = FakeLoop()
|
|
992
|
+
mock_acquire.side_effect = [False, True]
|
|
993
|
+
|
|
994
|
+
async with _context(
|
|
995
|
+
tmp_path / "db.sqlite",
|
|
996
|
+
asyncio_for_ynab.Configuration(access_token=TOKEN),
|
|
997
|
+
quiet=True,
|
|
998
|
+
timeout=0.1,
|
|
999
|
+
):
|
|
1000
|
+
pass
|
|
1001
|
+
|
|
1002
|
+
assert mock_acquire.call_count == 2
|
|
1003
|
+
assert mock_acquire.call_args_list[0].args[1] is False
|
|
1004
|
+
assert mock_sleep.call_count == 1
|
|
1005
|
+
assert mock_release.call_count == 1
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
@pytest.mark.asyncio
|
|
1009
|
+
async def test_context_removes_lock_file(tmp_path):
|
|
1010
|
+
db = tmp_path / "db.sqlite"
|
|
1011
|
+
lock_path = tmp_path / "db.sqlite.lock"
|
|
1012
|
+
|
|
1013
|
+
async with _context(
|
|
1014
|
+
db, asyncio_for_ynab.Configuration(access_token=TOKEN), quiet=True
|
|
1015
|
+
):
|
|
1016
|
+
assert lock_path.exists()
|
|
1017
|
+
|
|
1018
|
+
assert not lock_path.exists()
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
@patch(
|
|
1022
|
+
"sqlite_export_for_ynab._main.PlansApi.get_plans",
|
|
1023
|
+
new=AsyncMock(return_value=plan_response(PLANS)),
|
|
1024
|
+
)
|
|
1025
|
+
@patch(
|
|
1026
|
+
"sqlite_export_for_ynab._main.AccountsApi.get_accounts",
|
|
1027
|
+
new=AsyncMock(return_value=accounts_response([], SERVER_KNOWLEDGE_1)),
|
|
1028
|
+
)
|
|
1029
|
+
@patch(
|
|
1030
|
+
"sqlite_export_for_ynab._main.CategoriesApi.get_categories",
|
|
1031
|
+
new=AsyncMock(return_value=categories_response([], SERVER_KNOWLEDGE_1)),
|
|
1032
|
+
)
|
|
1033
|
+
@patch(
|
|
1034
|
+
"sqlite_export_for_ynab._main.PayeesApi.get_payees",
|
|
1035
|
+
new=AsyncMock(return_value=payees_response([], SERVER_KNOWLEDGE_1)),
|
|
1036
|
+
)
|
|
1037
|
+
@patch(
|
|
1038
|
+
"sqlite_export_for_ynab._main.TransactionsApi.get_transactions",
|
|
1039
|
+
new=AsyncMock(return_value=transactions_response([], SERVER_KNOWLEDGE_1)),
|
|
1040
|
+
)
|
|
1041
|
+
@patch(
|
|
1042
|
+
"sqlite_export_for_ynab._main.ScheduledTransactionsApi.get_scheduled_transactions",
|
|
1043
|
+
new=AsyncMock(return_value=scheduled_transactions_response([], SERVER_KNOWLEDGE_1)),
|
|
1044
|
+
)
|
|
1045
|
+
@pytest.mark.asyncio
|
|
1046
|
+
async def test_sync_no_data(tmp_path):
|
|
1047
|
+
# create the db and tables to exercise all code branches
|
|
1048
|
+
db = tmp_path / "db.sqlite"
|
|
1049
|
+
async with aiosqlite.connect(db) as con:
|
|
1050
|
+
await con.executescript(await contents("create-relations.sql"))
|
|
1051
|
+
|
|
1052
|
+
await sync(TOKEN, db, False)
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
@patch(
|
|
1056
|
+
"sqlite_export_for_ynab._main.PlansApi.get_plans",
|
|
1057
|
+
new=AsyncMock(return_value=plan_response(PLANS)),
|
|
1058
|
+
)
|
|
1059
|
+
@patch(
|
|
1060
|
+
"sqlite_export_for_ynab._main.AccountsApi.get_accounts",
|
|
1061
|
+
new=AsyncMock(return_value=accounts_response([], SERVER_KNOWLEDGE_1)),
|
|
1062
|
+
)
|
|
1063
|
+
@patch(
|
|
1064
|
+
"sqlite_export_for_ynab._main.CategoriesApi.get_categories",
|
|
1065
|
+
new=AsyncMock(return_value=categories_response([], SERVER_KNOWLEDGE_1)),
|
|
1066
|
+
)
|
|
1067
|
+
@patch(
|
|
1068
|
+
"sqlite_export_for_ynab._main.PayeesApi.get_payees",
|
|
1069
|
+
new=AsyncMock(return_value=payees_response([], SERVER_KNOWLEDGE_1)),
|
|
1070
|
+
)
|
|
1071
|
+
@patch(
|
|
1072
|
+
"sqlite_export_for_ynab._main.TransactionsApi.get_transactions",
|
|
1073
|
+
new=AsyncMock(return_value=transactions_response([], SERVER_KNOWLEDGE_1)),
|
|
1074
|
+
)
|
|
1075
|
+
@patch(
|
|
1076
|
+
"sqlite_export_for_ynab._main.ScheduledTransactionsApi.get_scheduled_transactions",
|
|
1077
|
+
new=AsyncMock(return_value=scheduled_transactions_response([], SERVER_KNOWLEDGE_1)),
|
|
1078
|
+
)
|
|
1079
|
+
@pytest.mark.asyncio
|
|
1080
|
+
async def test_sync_no_data_quiet(tmp_path, capsys):
|
|
1081
|
+
db = tmp_path / "db.sqlite"
|
|
1082
|
+
async with aiosqlite.connect(db) as con:
|
|
1083
|
+
await con.executescript(await contents("create-relations.sql"))
|
|
1084
|
+
|
|
1085
|
+
await sync(TOKEN, db, False, quiet=True)
|
|
1086
|
+
|
|
1087
|
+
out, err = capsys.readouterr()
|
|
1088
|
+
assert out == ""
|
|
1089
|
+
assert err == ""
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
@patch(
|
|
1093
|
+
"sqlite_export_for_ynab._main.PlansApi.get_plans",
|
|
1094
|
+
new=AsyncMock(return_value=plan_response(PLANS)),
|
|
1095
|
+
)
|
|
1096
|
+
@patch(
|
|
1097
|
+
"sqlite_export_for_ynab._main.AccountsApi.get_accounts",
|
|
1098
|
+
new=AsyncMock(return_value=accounts_response(ACCOUNTS, SERVER_KNOWLEDGE_1)),
|
|
1099
|
+
)
|
|
1100
|
+
@patch(
|
|
1101
|
+
"sqlite_export_for_ynab._main.CategoriesApi.get_categories",
|
|
1102
|
+
new=AsyncMock(
|
|
1103
|
+
return_value=categories_response(CATEGORY_GROUPS, SERVER_KNOWLEDGE_1)
|
|
1104
|
+
),
|
|
1105
|
+
)
|
|
1106
|
+
@patch(
|
|
1107
|
+
"sqlite_export_for_ynab._main.PayeesApi.get_payees",
|
|
1108
|
+
new=AsyncMock(return_value=payees_response(PAYEES, SERVER_KNOWLEDGE_1)),
|
|
1109
|
+
)
|
|
1110
|
+
@patch(
|
|
1111
|
+
"sqlite_export_for_ynab._main.TransactionsApi.get_transactions",
|
|
1112
|
+
new=AsyncMock(return_value=transactions_response(TRANSACTIONS, SERVER_KNOWLEDGE_1)),
|
|
1113
|
+
)
|
|
1114
|
+
@patch(
|
|
1115
|
+
"sqlite_export_for_ynab._main.ScheduledTransactionsApi.get_scheduled_transactions",
|
|
1116
|
+
new=AsyncMock(
|
|
1117
|
+
return_value=scheduled_transactions_response(
|
|
1118
|
+
SCHEDULED_TRANSACTIONS,
|
|
1119
|
+
SERVER_KNOWLEDGE_1,
|
|
1120
|
+
)
|
|
1121
|
+
),
|
|
1122
|
+
)
|
|
1123
|
+
@pytest.mark.asyncio
|
|
1124
|
+
async def test_sync(tmp_path):
|
|
1125
|
+
await sync(TOKEN, tmp_path / "db.sqlite", True)
|