sqlite-export-for-ynab 2.3.0__tar.gz → 2.4.0__tar.gz
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-2.3.0/sqlite_export_for_ynab.egg-info → sqlite_export_for_ynab-2.4.0}/PKG-INFO +2 -1
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/setup.cfg +2 -1
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab/_main.py +206 -153
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0/sqlite_export_for_ynab.egg-info}/PKG-INFO +2 -1
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab.egg-info/requires.txt +1 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/testing/fixtures.py +41 -8
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/tests/_main_test.py +144 -73
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/LICENSE +0 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/README.md +0 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/pyproject.toml +0 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/setup.py +0 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab/__init__.py +0 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab/__main__.py +0 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab/ddl/__init__.py +0 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab/ddl/create-relations.sql +0 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab/ddl/drop-relations.sql +0 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab/py.typed +0 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab.egg-info/SOURCES.txt +0 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab.egg-info/dependency_links.txt +0 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab.egg-info/entry_points.txt +0 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab.egg-info/top_level.txt +0 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/testing/__init__.py +0 -0
- {sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlite_export_for_ynab
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: SQLite Export for YNAB - Export YNAB Data to SQLite
|
|
5
5
|
Home-page: https://github.com/mxr/sqlite-export-for-ynab
|
|
6
6
|
Author: Max R
|
|
@@ -15,6 +15,7 @@ Requires-Python: >=3.12
|
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Requires-Dist: aiohttp>=3
|
|
18
|
+
Requires-Dist: aiosqlite
|
|
18
19
|
Requires-Dist: tldm
|
|
19
20
|
Dynamic: license-file
|
|
20
21
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = sqlite_export_for_ynab
|
|
3
|
-
version = 2.
|
|
3
|
+
version = 2.4.0
|
|
4
4
|
description = SQLite Export for YNAB - Export YNAB Data to SQLite
|
|
5
5
|
long_description = file: README.md
|
|
6
6
|
long_description_content_type = text/markdown
|
|
@@ -20,6 +20,7 @@ keywords = ynab, sqlite, sql, budget, plan, cli
|
|
|
20
20
|
packages = find:
|
|
21
21
|
install_requires =
|
|
22
22
|
aiohttp>=3
|
|
23
|
+
aiosqlite
|
|
23
24
|
tldm
|
|
24
25
|
python_requires = >=3.12
|
|
25
26
|
|
{sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab/_main.py
RENAMED
|
@@ -4,10 +4,10 @@ import argparse
|
|
|
4
4
|
import asyncio
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
-
import sqlite3
|
|
8
7
|
from dataclasses import dataclass
|
|
9
8
|
from importlib import resources
|
|
10
9
|
from importlib.metadata import version
|
|
10
|
+
from itertools import batched
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any
|
|
13
13
|
from typing import ClassVar
|
|
@@ -20,6 +20,7 @@ from urllib.parse import urljoin
|
|
|
20
20
|
from urllib.parse import urlunparse
|
|
21
21
|
|
|
22
22
|
import aiohttp
|
|
23
|
+
import aiosqlite
|
|
23
24
|
from tldm import tldm
|
|
24
25
|
|
|
25
26
|
from sqlite_export_for_ynab import ddl
|
|
@@ -40,6 +41,14 @@ _EntryTable = (
|
|
|
40
41
|
| Literal["scheduled_transactions"]
|
|
41
42
|
| Literal["scheduled_subtransactions"]
|
|
42
43
|
)
|
|
44
|
+
_Endpoint = (
|
|
45
|
+
Literal["accounts"]
|
|
46
|
+
| Literal["categories"]
|
|
47
|
+
| Literal["payees"]
|
|
48
|
+
| Literal["transactions"]
|
|
49
|
+
| Literal["scheduled_transactions"]
|
|
50
|
+
)
|
|
51
|
+
_ENDPOINTS = tuple(lit.__args__[0] for lit in _Endpoint.__args__)
|
|
43
52
|
_ALL_RELATIONS = frozenset(
|
|
44
53
|
("plans", "flat_transactions", "scheduled_flat_transactions")
|
|
45
54
|
+ tuple(lit.__args__[0] for lit in _EntryTable.__args__)
|
|
@@ -49,6 +58,8 @@ _ENV_TOKEN = "YNAB_PERSONAL_ACCESS_TOKEN"
|
|
|
49
58
|
|
|
50
59
|
_PACKAGE = "sqlite-export-for-ynab"
|
|
51
60
|
|
|
61
|
+
_BATCH_SIZE = 100
|
|
62
|
+
|
|
52
63
|
|
|
53
64
|
def resolve_token(token_override: str | None = None) -> str:
|
|
54
65
|
token = token_override or os.environ.get(_ENV_TOKEN)
|
|
@@ -124,49 +135,51 @@ async def sync(
|
|
|
124
135
|
if not db.exists():
|
|
125
136
|
db.parent.mkdir(parents=True, exist_ok=True)
|
|
126
137
|
|
|
127
|
-
with
|
|
128
|
-
con.row_factory =
|
|
129
|
-
cur = con.cursor()
|
|
138
|
+
async with aiosqlite.connect(db) as con:
|
|
139
|
+
con.row_factory = aiosqlite.Row
|
|
130
140
|
|
|
131
141
|
if full_refresh:
|
|
132
142
|
_print("Dropping relations...", quiet=quiet)
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
async with con.cursor() as cur:
|
|
144
|
+
await cur.executescript(contents("drop-relations.sql"))
|
|
145
|
+
await con.commit()
|
|
135
146
|
_print("Done", quiet=quiet)
|
|
136
147
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
async with con.cursor() as cur:
|
|
149
|
+
relations = await get_relations(cur)
|
|
150
|
+
if relations != _ALL_RELATIONS:
|
|
151
|
+
_print("Recreating relations...", quiet=quiet)
|
|
152
|
+
await cur.executescript(contents("create-relations.sql"))
|
|
153
|
+
await con.commit()
|
|
154
|
+
_print("Done", quiet=quiet)
|
|
143
155
|
|
|
144
156
|
_print("Fetching plan data...", quiet=quiet)
|
|
145
|
-
|
|
157
|
+
async with con.cursor() as cur:
|
|
158
|
+
lkos = await get_last_knowledge_of_server(cur)
|
|
146
159
|
async with aiohttp.ClientSession() as session:
|
|
147
|
-
with tldm(
|
|
160
|
+
with tldm(
|
|
161
|
+
desc="Plan Data", total=len(plans) * len(_ENDPOINTS), disable=quiet
|
|
162
|
+
) as pbar:
|
|
148
163
|
yc = ProgressYnabClient(YnabClient(token, session), pbar)
|
|
149
164
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
endpoint_data = dict(
|
|
166
|
+
zip(
|
|
167
|
+
_ENDPOINTS,
|
|
168
|
+
await asyncio.gather(
|
|
169
|
+
*(
|
|
170
|
+
asyncio.gather(*jobs(yc, endpoint, plan_ids, lkos))
|
|
171
|
+
for endpoint in _ENDPOINTS
|
|
172
|
+
)
|
|
173
|
+
),
|
|
174
|
+
strict=True,
|
|
175
|
+
)
|
|
158
176
|
)
|
|
159
177
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
all_account_data = data[:la]
|
|
166
|
-
all_cat_data = data[la : la + lc]
|
|
167
|
-
all_payee_data = data[la + lc : la + lc + lp]
|
|
168
|
-
all_txn_data = data[la + lc + lp : la + lc + lp + lt]
|
|
169
|
-
all_sched_txn_data = data[la + lc + lp + lt :]
|
|
178
|
+
all_account_data = endpoint_data["accounts"]
|
|
179
|
+
all_cat_data = endpoint_data["categories"]
|
|
180
|
+
all_payee_data = endpoint_data["payees"]
|
|
181
|
+
all_txn_data = endpoint_data["transactions"]
|
|
182
|
+
all_sched_txn_data = endpoint_data["scheduled_transactions"]
|
|
170
183
|
|
|
171
184
|
new_lkos = {
|
|
172
185
|
plan_id: transaction_data["server_knowledge"]
|
|
@@ -186,23 +199,58 @@ async def sync(
|
|
|
186
199
|
_print("No new data fetched", quiet=quiet)
|
|
187
200
|
else:
|
|
188
201
|
_print("Inserting plan data...", quiet=quiet)
|
|
189
|
-
insert_plans(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
await insert_plans(con, plans, new_lkos)
|
|
203
|
+
await asyncio.gather(
|
|
204
|
+
*(
|
|
205
|
+
insert_accounts(
|
|
206
|
+
con,
|
|
207
|
+
plan_id,
|
|
208
|
+
account_data["accounts"],
|
|
209
|
+
quiet=quiet,
|
|
210
|
+
)
|
|
211
|
+
for plan_id, account_data in zip(
|
|
212
|
+
plan_ids, all_account_data, strict=True
|
|
213
|
+
)
|
|
214
|
+
),
|
|
215
|
+
*(
|
|
216
|
+
insert_category_groups(
|
|
217
|
+
con,
|
|
218
|
+
plan_id,
|
|
219
|
+
cat_data["category_groups"],
|
|
220
|
+
quiet=quiet,
|
|
221
|
+
)
|
|
222
|
+
for plan_id, cat_data in zip(plan_ids, all_cat_data, strict=True)
|
|
223
|
+
),
|
|
224
|
+
*(
|
|
225
|
+
insert_payees(con, plan_id, payee_data["payees"], quiet=quiet)
|
|
226
|
+
for plan_id, payee_data in zip(
|
|
227
|
+
plan_ids, all_payee_data, strict=True
|
|
228
|
+
)
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
await asyncio.gather(
|
|
232
|
+
*(
|
|
233
|
+
insert_transactions(
|
|
234
|
+
con,
|
|
235
|
+
plan_id,
|
|
236
|
+
txn_data["transactions"],
|
|
237
|
+
quiet=quiet,
|
|
238
|
+
)
|
|
239
|
+
for plan_id, txn_data in zip(plan_ids, all_txn_data, strict=True)
|
|
240
|
+
),
|
|
241
|
+
*(
|
|
242
|
+
insert_scheduled_transactions(
|
|
243
|
+
con,
|
|
244
|
+
plan_id,
|
|
245
|
+
sched_txn_data["scheduled_transactions"],
|
|
246
|
+
quiet=quiet,
|
|
247
|
+
)
|
|
248
|
+
for plan_id, sched_txn_data in zip(
|
|
249
|
+
plan_ids, all_sched_txn_data, strict=True
|
|
250
|
+
)
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
await con.commit()
|
|
206
254
|
_print("Done", quiet=quiet)
|
|
207
255
|
|
|
208
256
|
|
|
@@ -210,58 +258,56 @@ def contents(filename: str) -> str:
|
|
|
210
258
|
return (resources.files(ddl) / filename).read_text()
|
|
211
259
|
|
|
212
260
|
|
|
213
|
-
def get_relations(cur:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
).fetchall()
|
|
219
|
-
}
|
|
261
|
+
async def get_relations(cur: aiosqlite.Cursor) -> set[str]:
|
|
262
|
+
await cur.execute(
|
|
263
|
+
"SELECT name FROM sqlite_master WHERE type='table' OR type='view'"
|
|
264
|
+
)
|
|
265
|
+
return {t["name"] for t in await cur.fetchall()}
|
|
220
266
|
|
|
221
267
|
|
|
222
|
-
def get_last_knowledge_of_server(cur:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
).fetchall()
|
|
228
|
-
}
|
|
268
|
+
async def get_last_knowledge_of_server(cur: aiosqlite.Cursor) -> dict[str, int]:
|
|
269
|
+
await cur.execute(
|
|
270
|
+
"SELECT id, last_knowledge_of_server FROM plans",
|
|
271
|
+
)
|
|
272
|
+
return {r["id"]: r["last_knowledge_of_server"] for r in await cur.fetchall()}
|
|
229
273
|
|
|
230
274
|
|
|
231
|
-
def insert_plans(
|
|
232
|
-
|
|
275
|
+
async def insert_plans(
|
|
276
|
+
con: aiosqlite.Connection, plans: list[dict[str, Any]], lkos: dict[str, int]
|
|
233
277
|
) -> None:
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
278
|
+
async with con.cursor() as cur:
|
|
279
|
+
for plan_batch in batched(plans, _BATCH_SIZE):
|
|
280
|
+
await cur.executemany(
|
|
281
|
+
"""
|
|
282
|
+
INSERT OR REPLACE INTO plans (
|
|
283
|
+
id
|
|
284
|
+
, name
|
|
285
|
+
, currency_format_currency_symbol
|
|
286
|
+
, currency_format_decimal_digits
|
|
287
|
+
, currency_format_decimal_separator
|
|
288
|
+
, currency_format_display_symbol
|
|
289
|
+
, currency_format_group_separator
|
|
290
|
+
, currency_format_iso_code
|
|
291
|
+
, currency_format_symbol_first
|
|
292
|
+
, last_knowledge_of_server
|
|
293
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
294
|
+
""",
|
|
295
|
+
(
|
|
296
|
+
(
|
|
297
|
+
plan_id := plan["id"],
|
|
298
|
+
plan["name"],
|
|
299
|
+
plan["currency_format"]["currency_symbol"],
|
|
300
|
+
plan["currency_format"]["decimal_digits"],
|
|
301
|
+
plan["currency_format"]["decimal_separator"],
|
|
302
|
+
plan["currency_format"]["display_symbol"],
|
|
303
|
+
plan["currency_format"]["group_separator"],
|
|
304
|
+
plan["currency_format"]["iso_code"],
|
|
305
|
+
plan["currency_format"]["symbol_first"],
|
|
306
|
+
lkos[plan_id],
|
|
307
|
+
)
|
|
308
|
+
for plan in plan_batch
|
|
309
|
+
),
|
|
261
310
|
)
|
|
262
|
-
for plan in plans
|
|
263
|
-
),
|
|
264
|
-
)
|
|
265
311
|
|
|
266
312
|
|
|
267
313
|
_LOAN_ACCOUNT_PERIODIC_VALUES = frozenset(
|
|
@@ -269,8 +315,8 @@ _LOAN_ACCOUNT_PERIODIC_VALUES = frozenset(
|
|
|
269
315
|
)
|
|
270
316
|
|
|
271
317
|
|
|
272
|
-
def insert_accounts(
|
|
273
|
-
|
|
318
|
+
async def insert_accounts(
|
|
319
|
+
con: aiosqlite.Connection,
|
|
274
320
|
plan_id: str,
|
|
275
321
|
accounts: list[dict[str, Any]],
|
|
276
322
|
*,
|
|
@@ -294,8 +340,8 @@ def insert_accounts(
|
|
|
294
340
|
for account in accounts
|
|
295
341
|
]
|
|
296
342
|
|
|
297
|
-
|
|
298
|
-
|
|
343
|
+
await insert_nested_entries(
|
|
344
|
+
con,
|
|
299
345
|
plan_id,
|
|
300
346
|
updated_accounts,
|
|
301
347
|
"Accounts",
|
|
@@ -306,15 +352,15 @@ def insert_accounts(
|
|
|
306
352
|
)
|
|
307
353
|
|
|
308
354
|
|
|
309
|
-
def insert_category_groups(
|
|
310
|
-
|
|
355
|
+
async def insert_category_groups(
|
|
356
|
+
con: aiosqlite.Connection,
|
|
311
357
|
plan_id: str,
|
|
312
358
|
category_groups: list[dict[str, Any]],
|
|
313
359
|
*,
|
|
314
360
|
quiet: bool = False,
|
|
315
361
|
) -> None:
|
|
316
|
-
|
|
317
|
-
|
|
362
|
+
await insert_nested_entries(
|
|
363
|
+
con,
|
|
318
364
|
plan_id,
|
|
319
365
|
category_groups,
|
|
320
366
|
"Categories",
|
|
@@ -325,8 +371,8 @@ def insert_category_groups(
|
|
|
325
371
|
)
|
|
326
372
|
|
|
327
373
|
|
|
328
|
-
def insert_payees(
|
|
329
|
-
|
|
374
|
+
async def insert_payees(
|
|
375
|
+
con: aiosqlite.Connection,
|
|
330
376
|
plan_id: str,
|
|
331
377
|
payees: list[dict[str, Any]],
|
|
332
378
|
*,
|
|
@@ -335,19 +381,19 @@ def insert_payees(
|
|
|
335
381
|
if not payees:
|
|
336
382
|
return
|
|
337
383
|
|
|
338
|
-
|
|
339
|
-
|
|
384
|
+
with tldm(total=len(payees), desc="Payees", disable=quiet) as pbar:
|
|
385
|
+
await insert_entries(con, "payees", plan_id, payees, pbar)
|
|
340
386
|
|
|
341
387
|
|
|
342
|
-
def insert_transactions(
|
|
343
|
-
|
|
388
|
+
async def insert_transactions(
|
|
389
|
+
con: aiosqlite.Connection,
|
|
344
390
|
plan_id: str,
|
|
345
391
|
transactions: list[dict[str, Any]],
|
|
346
392
|
*,
|
|
347
393
|
quiet: bool = False,
|
|
348
394
|
) -> None:
|
|
349
|
-
|
|
350
|
-
|
|
395
|
+
await insert_nested_entries(
|
|
396
|
+
con,
|
|
351
397
|
plan_id,
|
|
352
398
|
transactions,
|
|
353
399
|
"Transactions",
|
|
@@ -358,15 +404,15 @@ def insert_transactions(
|
|
|
358
404
|
)
|
|
359
405
|
|
|
360
406
|
|
|
361
|
-
def insert_scheduled_transactions(
|
|
362
|
-
|
|
407
|
+
async def insert_scheduled_transactions(
|
|
408
|
+
con: aiosqlite.Connection,
|
|
363
409
|
plan_id: str,
|
|
364
410
|
scheduled_transactions: list[dict[str, Any]],
|
|
365
411
|
*,
|
|
366
412
|
quiet: bool = False,
|
|
367
413
|
) -> None:
|
|
368
|
-
|
|
369
|
-
|
|
414
|
+
await insert_nested_entries(
|
|
415
|
+
con,
|
|
370
416
|
plan_id,
|
|
371
417
|
scheduled_transactions,
|
|
372
418
|
"Scheduled Transactions",
|
|
@@ -378,8 +424,8 @@ def insert_scheduled_transactions(
|
|
|
378
424
|
|
|
379
425
|
|
|
380
426
|
@overload
|
|
381
|
-
def insert_nested_entries(
|
|
382
|
-
|
|
427
|
+
async def insert_nested_entries(
|
|
428
|
+
con: aiosqlite.Connection,
|
|
383
429
|
plan_id: str,
|
|
384
430
|
entries: list[dict[str, Any]],
|
|
385
431
|
desc: Literal["Accounts"],
|
|
@@ -392,8 +438,8 @@ def insert_nested_entries(
|
|
|
392
438
|
|
|
393
439
|
|
|
394
440
|
@overload
|
|
395
|
-
def insert_nested_entries(
|
|
396
|
-
|
|
441
|
+
async def insert_nested_entries(
|
|
442
|
+
con: aiosqlite.Connection,
|
|
397
443
|
plan_id: str,
|
|
398
444
|
entries: list[dict[str, Any]],
|
|
399
445
|
desc: Literal["Categories"],
|
|
@@ -406,8 +452,8 @@ def insert_nested_entries(
|
|
|
406
452
|
|
|
407
453
|
|
|
408
454
|
@overload
|
|
409
|
-
def insert_nested_entries(
|
|
410
|
-
|
|
455
|
+
async def insert_nested_entries(
|
|
456
|
+
con: aiosqlite.Connection,
|
|
411
457
|
plan_id: str,
|
|
412
458
|
entries: list[dict[str, Any]],
|
|
413
459
|
desc: Literal["Transactions"],
|
|
@@ -420,8 +466,8 @@ def insert_nested_entries(
|
|
|
420
466
|
|
|
421
467
|
|
|
422
468
|
@overload
|
|
423
|
-
def insert_nested_entries(
|
|
424
|
-
|
|
469
|
+
async def insert_nested_entries(
|
|
470
|
+
con: aiosqlite.Connection,
|
|
425
471
|
plan_id: str,
|
|
426
472
|
entries: list[dict[str, Any]],
|
|
427
473
|
desc: Literal["Scheduled Transactions"],
|
|
@@ -433,8 +479,8 @@ def insert_nested_entries(
|
|
|
433
479
|
) -> None: ...
|
|
434
480
|
|
|
435
481
|
|
|
436
|
-
def insert_nested_entries(
|
|
437
|
-
|
|
482
|
+
async def insert_nested_entries(
|
|
483
|
+
con: aiosqlite.Connection,
|
|
438
484
|
plan_id: str,
|
|
439
485
|
entries: list[dict[str, Any]],
|
|
440
486
|
desc: (
|
|
@@ -471,44 +517,51 @@ def insert_nested_entries(
|
|
|
471
517
|
desc=desc,
|
|
472
518
|
disable=quiet,
|
|
473
519
|
) as pbar:
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
{k: v for k, v in entry.items() if k != subentries_name}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
520
|
+
await insert_entries(
|
|
521
|
+
con,
|
|
522
|
+
entries_name,
|
|
523
|
+
plan_id,
|
|
524
|
+
[
|
|
525
|
+
{k: v for k, v in entry.items() if k != subentries_name}
|
|
526
|
+
for entry in entries
|
|
527
|
+
],
|
|
528
|
+
pbar,
|
|
529
|
+
)
|
|
530
|
+
await insert_entries(
|
|
531
|
+
con,
|
|
532
|
+
subentries_table_name,
|
|
533
|
+
plan_id,
|
|
534
|
+
[subentry for entry in entries for subentry in entry[subentries_name]],
|
|
535
|
+
pbar,
|
|
536
|
+
)
|
|
486
537
|
|
|
487
538
|
|
|
488
|
-
def
|
|
489
|
-
|
|
539
|
+
async def insert_entries(
|
|
540
|
+
con: aiosqlite.Connection,
|
|
490
541
|
table: _EntryTable,
|
|
491
542
|
plan_id: str,
|
|
492
|
-
|
|
543
|
+
entries: list[dict[str, Any]],
|
|
544
|
+
pbar: tldm[Never],
|
|
493
545
|
) -> None:
|
|
494
|
-
|
|
495
|
-
|
|
546
|
+
if not entries:
|
|
547
|
+
return
|
|
496
548
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
)
|
|
549
|
+
entry_keys = tuple(entries[0])
|
|
550
|
+
sql = f"INSERT OR REPLACE INTO {table} ({', '.join(entry_keys + ('plan_id',))}) VALUES ({', '.join('?' * (len(entry_keys) + 1))})"
|
|
551
|
+
|
|
552
|
+
async with con.cursor() as cur:
|
|
553
|
+
for entry_batch in batched(entries, _BATCH_SIZE):
|
|
554
|
+
values_batch = [
|
|
555
|
+
tuple(entry[key] for key in entry_keys) + (plan_id,)
|
|
556
|
+
for entry in entry_batch
|
|
557
|
+
]
|
|
558
|
+
await cur.executemany(sql, values_batch)
|
|
559
|
+
pbar.update(len(values_batch))
|
|
501
560
|
|
|
502
561
|
|
|
503
562
|
def jobs(
|
|
504
563
|
yc: SupportsYnabClient,
|
|
505
|
-
endpoint:
|
|
506
|
-
Literal["accounts"]
|
|
507
|
-
| Literal["categories"]
|
|
508
|
-
| Literal["payees"]
|
|
509
|
-
| Literal["transactions"]
|
|
510
|
-
| Literal["scheduled_transactions"]
|
|
511
|
-
),
|
|
564
|
+
endpoint: _Endpoint,
|
|
512
565
|
plan_ids: list[str],
|
|
513
566
|
lkos: dict[str, int],
|
|
514
567
|
) -> list[Awaitable[dict[str, Any]]]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlite_export_for_ynab
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: SQLite Export for YNAB - Export YNAB Data to SQLite
|
|
5
5
|
Home-page: https://github.com/mxr/sqlite-export-for-ynab
|
|
6
6
|
Author: Max R
|
|
@@ -15,6 +15,7 @@ Requires-Python: >=3.12
|
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Requires-Dist: aiohttp>=3
|
|
18
|
+
Requires-Dist: aiosqlite
|
|
18
19
|
Requires-Dist: tldm
|
|
19
20
|
Dynamic: license-file
|
|
20
21
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
-
import sqlite3
|
|
5
4
|
from typing import Any
|
|
6
5
|
from uuid import uuid4
|
|
7
6
|
|
|
7
|
+
import aiosqlite
|
|
8
8
|
import pytest
|
|
9
|
+
import pytest_asyncio
|
|
9
10
|
from aioresponses import aioresponses
|
|
10
11
|
|
|
11
12
|
from sqlite_export_for_ynab._main import contents
|
|
@@ -144,6 +145,15 @@ CATEGORY_GROUPS: list[dict[str, Any]] = [
|
|
|
144
145
|
"activity_currency": 1.0,
|
|
145
146
|
"budgeted_formatted": "$10.25",
|
|
146
147
|
"budgeted_currency": 10.25,
|
|
148
|
+
"goal_target_formatted": None,
|
|
149
|
+
"goal_target_currency": None,
|
|
150
|
+
"goal_under_funded_formatted": None,
|
|
151
|
+
"goal_under_funded_currency": None,
|
|
152
|
+
"goal_overall_funded_formatted": None,
|
|
153
|
+
"goal_overall_funded_currency": None,
|
|
154
|
+
"goal_overall_left_formatted": None,
|
|
155
|
+
"goal_overall_left_currency": None,
|
|
156
|
+
"goal_target_date": None,
|
|
147
157
|
},
|
|
148
158
|
],
|
|
149
159
|
},
|
|
@@ -162,6 +172,15 @@ CATEGORY_GROUPS: list[dict[str, Any]] = [
|
|
|
162
172
|
"activity_currency": 7.5,
|
|
163
173
|
"budgeted_formatted": "$15.00",
|
|
164
174
|
"budgeted_currency": 15.0,
|
|
175
|
+
"goal_target_formatted": None,
|
|
176
|
+
"goal_target_currency": None,
|
|
177
|
+
"goal_under_funded_formatted": None,
|
|
178
|
+
"goal_under_funded_currency": None,
|
|
179
|
+
"goal_overall_funded_formatted": None,
|
|
180
|
+
"goal_overall_funded_currency": None,
|
|
181
|
+
"goal_overall_left_formatted": None,
|
|
182
|
+
"goal_overall_left_currency": None,
|
|
183
|
+
"goal_target_date": None,
|
|
165
184
|
},
|
|
166
185
|
{
|
|
167
186
|
"id": CATEGORY_ID_4,
|
|
@@ -174,6 +193,15 @@ CATEGORY_GROUPS: list[dict[str, Any]] = [
|
|
|
174
193
|
"activity_currency": 19.0,
|
|
175
194
|
"budgeted_formatted": "$20.00",
|
|
176
195
|
"budgeted_currency": 20.0,
|
|
196
|
+
"goal_target_formatted": None,
|
|
197
|
+
"goal_target_currency": None,
|
|
198
|
+
"goal_under_funded_formatted": None,
|
|
199
|
+
"goal_under_funded_currency": None,
|
|
200
|
+
"goal_overall_funded_formatted": None,
|
|
201
|
+
"goal_overall_funded_currency": None,
|
|
202
|
+
"goal_overall_left_formatted": None,
|
|
203
|
+
"goal_overall_left_currency": None,
|
|
204
|
+
"goal_target_date": None,
|
|
177
205
|
},
|
|
178
206
|
],
|
|
179
207
|
},
|
|
@@ -325,12 +353,17 @@ SCHEDULED_TRANSACTIONS: list[dict[str, Any]] = [
|
|
|
325
353
|
]
|
|
326
354
|
|
|
327
355
|
|
|
328
|
-
@
|
|
329
|
-
def
|
|
330
|
-
with
|
|
331
|
-
con.row_factory =
|
|
332
|
-
|
|
333
|
-
|
|
356
|
+
@pytest_asyncio.fixture
|
|
357
|
+
async def con():
|
|
358
|
+
async with aiosqlite.connect(":memory:") as con:
|
|
359
|
+
con.row_factory = aiosqlite.Row
|
|
360
|
+
await con.executescript(contents("create-relations.sql"))
|
|
361
|
+
yield con
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@pytest_asyncio.fixture
|
|
365
|
+
async def cur(con):
|
|
366
|
+
async with con.cursor() as cursor:
|
|
334
367
|
yield cursor
|
|
335
368
|
|
|
336
369
|
|
|
@@ -340,7 +373,7 @@ def mock_aioresponses():
|
|
|
340
373
|
yield m
|
|
341
374
|
|
|
342
375
|
|
|
343
|
-
def strip_nones(d:
|
|
376
|
+
def strip_nones(d: aiosqlite.Row) -> dict[str, Any]:
|
|
344
377
|
return {k: v for k, v in dict(d).items() if v is not None}
|
|
345
378
|
|
|
346
379
|
|
|
@@ -2,12 +2,12 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
|
-
import sqlite3
|
|
6
5
|
from configparser import ConfigParser
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
from unittest.mock import patch
|
|
9
8
|
|
|
10
9
|
import aiohttp
|
|
10
|
+
import aiosqlite
|
|
11
11
|
import pytest
|
|
12
12
|
from aiohttp.http_exceptions import HttpProcessingError
|
|
13
13
|
from tldm import tldm
|
|
@@ -49,6 +49,7 @@ from testing.fixtures import CATEGORY_NAME_1
|
|
|
49
49
|
from testing.fixtures import CATEGORY_NAME_2
|
|
50
50
|
from testing.fixtures import CATEGORY_NAME_3
|
|
51
51
|
from testing.fixtures import CATEGORY_NAME_4
|
|
52
|
+
from testing.fixtures import con
|
|
52
53
|
from testing.fixtures import cur
|
|
53
54
|
from testing.fixtures import EXAMPLE_ENDPOINT_RE
|
|
54
55
|
from testing.fixtures import LKOS
|
|
@@ -80,6 +81,12 @@ from testing.fixtures import TRANSACTIONS
|
|
|
80
81
|
from testing.fixtures import TRANSACTIONS_ENDPOINT_RE
|
|
81
82
|
|
|
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
|
+
|
|
83
90
|
@pytest.mark.parametrize(
|
|
84
91
|
("xdg_data_home", "expected_prefix"),
|
|
85
92
|
(
|
|
@@ -93,21 +100,26 @@ def test_default_db_path(monkeypatch, xdg_data_home, expected_prefix):
|
|
|
93
100
|
|
|
94
101
|
|
|
95
102
|
@pytest.mark.usefixtures(cur.__name__)
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
@pytest.mark.asyncio
|
|
104
|
+
async def test_get_relations(cur):
|
|
105
|
+
assert await get_relations(cur) == _ALL_RELATIONS
|
|
98
106
|
|
|
99
107
|
|
|
100
|
-
@pytest.mark.usefixtures(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
@pytest.mark.usefixtures(con.__name__)
|
|
109
|
+
@pytest.mark.asyncio
|
|
110
|
+
async def test_get_last_knowledge_of_server(con):
|
|
111
|
+
await insert_plans(con, PLANS, LKOS)
|
|
112
|
+
async with con.cursor() as cur:
|
|
113
|
+
assert await get_last_knowledge_of_server(cur) == LKOS
|
|
104
114
|
|
|
105
115
|
|
|
106
|
-
@pytest.mark.usefixtures(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
assert [
|
|
116
|
+
@pytest.mark.usefixtures(con.__name__)
|
|
117
|
+
@pytest.mark.asyncio
|
|
118
|
+
async def test_insert_plans(con):
|
|
119
|
+
await insert_plans(con, PLANS, LKOS)
|
|
120
|
+
assert [
|
|
121
|
+
strip_nones(d) for d in await fetchall(con, "SELECT * FROM plans ORDER BY name")
|
|
122
|
+
] == [
|
|
111
123
|
{
|
|
112
124
|
"id": PLAN_ID_1,
|
|
113
125
|
"name": PLANS[0]["name"],
|
|
@@ -135,15 +147,18 @@ def test_insert_plans(cur):
|
|
|
135
147
|
]
|
|
136
148
|
|
|
137
149
|
|
|
138
|
-
@pytest.mark.usefixtures(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
assert not
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
assert [
|
|
150
|
+
@pytest.mark.usefixtures(con.__name__)
|
|
151
|
+
@pytest.mark.asyncio
|
|
152
|
+
async def test_insert_accounts(con):
|
|
153
|
+
await insert_accounts(con, PLAN_ID_1, [])
|
|
154
|
+
assert not await fetchall(con, "SELECT * FROM accounts")
|
|
155
|
+
assert not await fetchall(con, "SELECT * FROM account_periodic_values")
|
|
156
|
+
|
|
157
|
+
await insert_accounts(con, PLAN_ID_1, ACCOUNTS)
|
|
158
|
+
assert [
|
|
159
|
+
strip_nones(d)
|
|
160
|
+
for d in await fetchall(con, "SELECT * FROM accounts ORDER BY name")
|
|
161
|
+
] == [
|
|
147
162
|
{
|
|
148
163
|
"id": ACCOUNT_ID_1,
|
|
149
164
|
"plan_id": PLAN_ID_1,
|
|
@@ -170,8 +185,12 @@ def test_insert_accounts(cur):
|
|
|
170
185
|
},
|
|
171
186
|
]
|
|
172
187
|
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
assert [
|
|
189
|
+
strip_nones(d)
|
|
190
|
+
for d in await fetchall(
|
|
191
|
+
con, "SELECT * FROM account_periodic_values ORDER BY name"
|
|
192
|
+
)
|
|
193
|
+
] == [
|
|
175
194
|
{
|
|
176
195
|
"account_id": ACCOUNT_ID_1,
|
|
177
196
|
"plan_id": PLAN_ID_1,
|
|
@@ -189,15 +208,18 @@ def test_insert_accounts(cur):
|
|
|
189
208
|
]
|
|
190
209
|
|
|
191
210
|
|
|
192
|
-
@pytest.mark.usefixtures(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
assert not
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
assert [
|
|
211
|
+
@pytest.mark.usefixtures(con.__name__)
|
|
212
|
+
@pytest.mark.asyncio
|
|
213
|
+
async def test_insert_category_groups(con):
|
|
214
|
+
await insert_category_groups(con, PLAN_ID_1, [])
|
|
215
|
+
assert not await fetchall(con, "SELECT * FROM category_groups")
|
|
216
|
+
assert not await fetchall(con, "SELECT * FROM categories")
|
|
217
|
+
|
|
218
|
+
await insert_category_groups(con, PLAN_ID_1, CATEGORY_GROUPS)
|
|
219
|
+
assert [
|
|
220
|
+
strip_nones(d)
|
|
221
|
+
for d in await fetchall(con, "SELECT * FROM category_groups ORDER BY name")
|
|
222
|
+
] == [
|
|
201
223
|
{
|
|
202
224
|
"id": CATEGORY_GROUP_ID_1,
|
|
203
225
|
"name": CATEGORY_GROUP_NAME_1,
|
|
@@ -210,8 +232,10 @@ def test_insert_category_groups(cur):
|
|
|
210
232
|
},
|
|
211
233
|
]
|
|
212
234
|
|
|
213
|
-
|
|
214
|
-
|
|
235
|
+
assert [
|
|
236
|
+
strip_nones(d)
|
|
237
|
+
for d in await fetchall(con, "SELECT * FROM categories ORDER BY name")
|
|
238
|
+
] == [
|
|
215
239
|
{
|
|
216
240
|
"id": CATEGORY_ID_1,
|
|
217
241
|
"category_group_id": CATEGORY_GROUP_ID_1,
|
|
@@ -276,14 +300,41 @@ def test_insert_category_groups(cur):
|
|
|
276
300
|
]
|
|
277
301
|
|
|
278
302
|
|
|
279
|
-
@pytest.mark.usefixtures(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
303
|
+
@pytest.mark.usefixtures(con.__name__)
|
|
304
|
+
@pytest.mark.asyncio
|
|
305
|
+
async def test_insert_category_group_without_categories(con):
|
|
306
|
+
category_group = {
|
|
307
|
+
"id": CATEGORY_GROUP_ID_1,
|
|
308
|
+
"name": CATEGORY_GROUP_NAME_1,
|
|
309
|
+
"categories": [],
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
await insert_category_groups(con, PLAN_ID_1, [category_group])
|
|
313
|
+
|
|
314
|
+
assert [
|
|
315
|
+
strip_nones(d)
|
|
316
|
+
for d in await fetchall(con, "SELECT * FROM category_groups ORDER BY name")
|
|
317
|
+
] == [
|
|
318
|
+
{
|
|
319
|
+
"id": CATEGORY_GROUP_ID_1,
|
|
320
|
+
"name": CATEGORY_GROUP_NAME_1,
|
|
321
|
+
"plan_id": PLAN_ID_1,
|
|
322
|
+
},
|
|
323
|
+
]
|
|
324
|
+
assert not await fetchall(con, "SELECT * FROM categories")
|
|
283
325
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
326
|
+
|
|
327
|
+
@pytest.mark.usefixtures(con.__name__)
|
|
328
|
+
@pytest.mark.asyncio
|
|
329
|
+
async def test_insert_payees(con):
|
|
330
|
+
await insert_payees(con, PLAN_ID_1, [])
|
|
331
|
+
assert not await fetchall(con, "SELECT * FROM payees")
|
|
332
|
+
|
|
333
|
+
await insert_payees(con, PLAN_ID_1, PAYEES)
|
|
334
|
+
assert [
|
|
335
|
+
strip_nones(d)
|
|
336
|
+
for d in await fetchall(con, "SELECT * FROM payees ORDER BY name")
|
|
337
|
+
] == [
|
|
287
338
|
{
|
|
288
339
|
"id": PAYEE_ID_1,
|
|
289
340
|
"plan_id": PLAN_ID_1,
|
|
@@ -297,16 +348,19 @@ def test_insert_payees(cur):
|
|
|
297
348
|
]
|
|
298
349
|
|
|
299
350
|
|
|
300
|
-
@pytest.mark.usefixtures(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
assert not
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
assert [
|
|
351
|
+
@pytest.mark.usefixtures(con.__name__)
|
|
352
|
+
@pytest.mark.asyncio
|
|
353
|
+
async def test_insert_transactions(con):
|
|
354
|
+
await insert_transactions(con, PLAN_ID_1, [])
|
|
355
|
+
assert not await fetchall(con, "SELECT * FROM transactions")
|
|
356
|
+
assert not await fetchall(con, "SELECT * FROM subtransactions")
|
|
357
|
+
|
|
358
|
+
await insert_category_groups(con, PLAN_ID_1, CATEGORY_GROUPS)
|
|
359
|
+
await insert_transactions(con, PLAN_ID_1, TRANSACTIONS)
|
|
360
|
+
assert [
|
|
361
|
+
strip_nones(d)
|
|
362
|
+
for d in await fetchall(con, "SELECT * FROM transactions ORDER BY date")
|
|
363
|
+
] == [
|
|
310
364
|
{
|
|
311
365
|
"id": TRANSACTION_ID_1,
|
|
312
366
|
"plan_id": PLAN_ID_1,
|
|
@@ -345,8 +399,10 @@ def test_insert_transactions(cur):
|
|
|
345
399
|
},
|
|
346
400
|
]
|
|
347
401
|
|
|
348
|
-
|
|
349
|
-
|
|
402
|
+
assert [
|
|
403
|
+
strip_nones(d)
|
|
404
|
+
for d in await fetchall(con, "SELECT * FROM subtransactions ORDER BY amount")
|
|
405
|
+
] == [
|
|
350
406
|
{
|
|
351
407
|
"id": SUBTRANSACTION_ID_1,
|
|
352
408
|
"transaction_id": TRANSACTION_ID_1,
|
|
@@ -371,8 +427,10 @@ def test_insert_transactions(cur):
|
|
|
371
427
|
},
|
|
372
428
|
]
|
|
373
429
|
|
|
374
|
-
|
|
375
|
-
|
|
430
|
+
assert [
|
|
431
|
+
strip_nones(d)
|
|
432
|
+
for d in await fetchall(con, "SELECT * FROM flat_transactions ORDER BY amount")
|
|
433
|
+
] == [
|
|
376
434
|
{
|
|
377
435
|
"transaction_id": TRANSACTION_ID_1,
|
|
378
436
|
"subtransaction_id": SUBTRANSACTION_ID_1,
|
|
@@ -404,16 +462,21 @@ def test_insert_transactions(cur):
|
|
|
404
462
|
]
|
|
405
463
|
|
|
406
464
|
|
|
407
|
-
@pytest.mark.usefixtures(
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
assert not
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
assert [
|
|
465
|
+
@pytest.mark.usefixtures(con.__name__)
|
|
466
|
+
@pytest.mark.asyncio
|
|
467
|
+
async def test_insert_scheduled_transactions(con):
|
|
468
|
+
await insert_scheduled_transactions(con, PLAN_ID_1, [])
|
|
469
|
+
assert not await fetchall(con, "SELECT * FROM scheduled_transactions")
|
|
470
|
+
assert not await fetchall(con, "SELECT * FROM scheduled_subtransactions")
|
|
471
|
+
|
|
472
|
+
await insert_category_groups(con, PLAN_ID_1, CATEGORY_GROUPS)
|
|
473
|
+
await insert_scheduled_transactions(con, PLAN_ID_1, SCHEDULED_TRANSACTIONS)
|
|
474
|
+
assert [
|
|
475
|
+
strip_nones(d)
|
|
476
|
+
for d in await fetchall(
|
|
477
|
+
con, "SELECT * FROM scheduled_transactions ORDER BY amount"
|
|
478
|
+
)
|
|
479
|
+
] == [
|
|
417
480
|
{
|
|
418
481
|
"id": SCHEDULED_TRANSACTION_ID_1,
|
|
419
482
|
"plan_id": PLAN_ID_1,
|
|
@@ -449,8 +512,12 @@ def test_insert_scheduled_transactions(cur):
|
|
|
449
512
|
},
|
|
450
513
|
]
|
|
451
514
|
|
|
452
|
-
|
|
453
|
-
|
|
515
|
+
assert [
|
|
516
|
+
strip_nones(d)
|
|
517
|
+
for d in await fetchall(
|
|
518
|
+
con, "SELECT * FROM scheduled_subtransactions ORDER BY amount"
|
|
519
|
+
)
|
|
520
|
+
] == [
|
|
454
521
|
{
|
|
455
522
|
"id": SCHEDULED_SUBTRANSACTION_ID_1,
|
|
456
523
|
"scheduled_transaction_id": SCHEDULED_TRANSACTION_ID_1,
|
|
@@ -475,8 +542,12 @@ def test_insert_scheduled_transactions(cur):
|
|
|
475
542
|
},
|
|
476
543
|
]
|
|
477
544
|
|
|
478
|
-
|
|
479
|
-
|
|
545
|
+
assert [
|
|
546
|
+
strip_nones(d)
|
|
547
|
+
for d in await fetchall(
|
|
548
|
+
con, "SELECT * FROM scheduled_flat_transactions ORDER BY amount"
|
|
549
|
+
)
|
|
550
|
+
] == [
|
|
480
551
|
{
|
|
481
552
|
"transaction_id": SCHEDULED_TRANSACTION_ID_3,
|
|
482
553
|
"plan_id": PLAN_ID_1,
|
|
@@ -650,8 +721,8 @@ async def test_sync_no_data(tmp_path, mock_aioresponses):
|
|
|
650
721
|
|
|
651
722
|
# create the db and tables to exercise all code branches
|
|
652
723
|
db = tmp_path / "db.sqlite"
|
|
653
|
-
with
|
|
654
|
-
con.executescript(contents("create-relations.sql"))
|
|
724
|
+
async with aiosqlite.connect(db) as con:
|
|
725
|
+
await con.executescript(contents("create-relations.sql"))
|
|
655
726
|
|
|
656
727
|
await sync(TOKEN, db, False)
|
|
657
728
|
|
|
@@ -694,8 +765,8 @@ async def test_sync_no_data_quiet(tmp_path, mock_aioresponses, capsys):
|
|
|
694
765
|
)
|
|
695
766
|
|
|
696
767
|
db = tmp_path / "db.sqlite"
|
|
697
|
-
with
|
|
698
|
-
con.executescript(contents("create-relations.sql"))
|
|
768
|
+
async with aiosqlite.connect(db) as con:
|
|
769
|
+
await con.executescript(contents("create-relations.sql"))
|
|
699
770
|
|
|
700
771
|
await sync(TOKEN, db, False, quiet=True)
|
|
701
772
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab/__init__.py
RENAMED
|
File without changes
|
{sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab/__main__.py
RENAMED
|
File without changes
|
{sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab/ddl/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlite_export_for_ynab-2.3.0 → sqlite_export_for_ynab-2.4.0}/sqlite_export_for_ynab/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|