sqlite-export-for-ynab 2.7.2__tar.gz → 2.9.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.
Files changed (23) hide show
  1. {sqlite_export_for_ynab-2.7.2/sqlite_export_for_ynab.egg-info → sqlite_export_for_ynab-2.9.0}/PKG-INFO +4 -4
  2. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/README.md +2 -2
  3. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/setup.cfg +2 -2
  4. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/sqlite_export_for_ynab/_main.py +39 -86
  5. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/sqlite_export_for_ynab/ddl/create-relations.sql +2 -0
  6. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0/sqlite_export_for_ynab.egg-info}/PKG-INFO +4 -4
  7. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/sqlite_export_for_ynab.egg-info/requires.txt +1 -1
  8. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/testing/fixtures.py +6 -0
  9. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/tests/_main_test.py +33 -0
  10. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/LICENSE +0 -0
  11. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/pyproject.toml +0 -0
  12. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/setup.py +0 -0
  13. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/sqlite_export_for_ynab/__init__.py +0 -0
  14. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/sqlite_export_for_ynab/__main__.py +0 -0
  15. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/sqlite_export_for_ynab/ddl/__init__.py +0 -0
  16. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/sqlite_export_for_ynab/ddl/drop-relations.sql +0 -0
  17. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/sqlite_export_for_ynab/py.typed +0 -0
  18. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/sqlite_export_for_ynab.egg-info/SOURCES.txt +0 -0
  19. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/sqlite_export_for_ynab.egg-info/dependency_links.txt +0 -0
  20. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/sqlite_export_for_ynab.egg-info/entry_points.txt +0 -0
  21. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/sqlite_export_for_ynab.egg-info/top_level.txt +0 -0
  22. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.0}/testing/__init__.py +0 -0
  23. {sqlite_export_for_ynab-2.7.2 → sqlite_export_for_ynab-2.9.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.7.2
3
+ Version: 2.9.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
@@ -17,7 +17,7 @@ License-File: LICENSE
17
17
  Requires-Dist: aiohttp>=3
18
18
  Requires-Dist: aiopathlib
19
19
  Requires-Dist: aiosqlite
20
- Requires-Dist: asyncio-for-ynab
20
+ Requires-Dist: asyncio-for-ynab~=1.84.0
21
21
  Requires-Dist: fasteners
22
22
  Requires-Dist: rich>=10
23
23
  Requires-Dist: tenacity
@@ -272,7 +272,7 @@ WITH interest_by_account AS (
272
272
  SELECT
273
273
  plan_id
274
274
  , account_name
275
- , SUM(-amount_currency) AS total
275
+ , SUM(amount_currency) AS total
276
276
  FROM flat_transactions
277
277
  WHERE
278
278
  TRUE
@@ -536,7 +536,7 @@ WITH params AS (
536
536
  TRUE
537
537
  AND NOT c.deleted
538
538
  AND c.category_group_name != 'Credit Card Payments'
539
- AND c.category_group_name != 'Internal Master Category'
539
+ AND NOT c.internal
540
540
  AND (
541
541
  v.include_category_groups = ''
542
542
  OR EXISTS (
@@ -242,7 +242,7 @@ WITH interest_by_account AS (
242
242
  SELECT
243
243
  plan_id
244
244
  , account_name
245
- , SUM(-amount_currency) AS total
245
+ , SUM(amount_currency) AS total
246
246
  FROM flat_transactions
247
247
  WHERE
248
248
  TRUE
@@ -506,7 +506,7 @@ WITH params AS (
506
506
  TRUE
507
507
  AND NOT c.deleted
508
508
  AND c.category_group_name != 'Credit Card Payments'
509
- AND c.category_group_name != 'Internal Master Category'
509
+ AND NOT c.internal
510
510
  AND (
511
511
  v.include_category_groups = ''
512
512
  OR EXISTS (
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = sqlite_export_for_ynab
3
- version = 2.7.2
3
+ version = 2.9.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
@@ -22,7 +22,7 @@ install_requires =
22
22
  aiohttp>=3
23
23
  aiopathlib
24
24
  aiosqlite
25
- asyncio-for-ynab
25
+ asyncio-for-ynab~=1.84.0
26
26
  fasteners
27
27
  rich>=10
28
28
  tenacity
@@ -46,6 +46,8 @@ from sqlite_export_for_ynab import ddl
46
46
 
47
47
  if TYPE_CHECKING:
48
48
  from collections.abc import AsyncIterator
49
+ from collections.abc import Awaitable
50
+ from collections.abc import Callable
49
51
  from collections.abc import Iterator
50
52
  from collections.abc import Sequence
51
53
 
@@ -603,7 +605,13 @@ async def insert_entries(
603
605
  if not entries:
604
606
  return
605
607
 
606
- entry_keys = tuple(entries[0])
608
+ async with context.con.cursor() as cur:
609
+ await cur.execute(f"PRAGMA table_info({table})")
610
+ table_columns = {row["name"] async for row in cur}
611
+
612
+ # Ignore any keys the YNAB API returns that aren't columns in the DDL so
613
+ # newly-added API fields don't break the insert.
614
+ entry_keys = tuple(k for k in entries[0] if k in table_columns)
607
615
  sql = f"INSERT OR REPLACE INTO {table} ({', '.join(entry_keys + ('plan_id',))}) VALUES ({', '.join('?' * (len(entry_keys) + 1))})"
608
616
 
609
617
  async with context.con.cursor() as cur:
@@ -634,100 +642,45 @@ async def _get_all_ynab(
634
642
  async def _get_plan_data(
635
643
  context: _Context, plan_id: str, lkos: dict[str, int], task_id: TaskID
636
644
  ) -> tuple[str, _YnabPlanData]:
637
- (
638
- accounts,
639
- categories,
640
- payees,
641
- transactions_serverknowledge,
642
- scheduled_transactions,
643
- ) = await asyncio.gather(
644
- _get_accounts(context, plan_id, lkos, task_id),
645
- _get_categories(context, plan_id, lkos, task_id),
646
- _get_payees(context, plan_id, lkos, task_id),
647
- _get_transactions(context, plan_id, lkos, task_id),
648
- _get_scheduled_transactions(context, plan_id, lkos, task_id),
645
+ accounts, categories, payees, transactions, scheduled = await asyncio.gather(
646
+ *(
647
+ _get_ynab(context, endpoint, plan_id, lkos, task_id)
648
+ for endpoint in (
649
+ AccountsApi(context.api_client).get_accounts,
650
+ CategoriesApi(context.api_client).get_categories,
651
+ PayeesApi(context.api_client).get_payees,
652
+ TransactionsApi(context.api_client).get_transactions,
653
+ ScheduledTransactionsApi(context.api_client).get_scheduled_transactions,
654
+ )
655
+ )
649
656
  )
650
- transactions, server_knowledge = transactions_serverknowledge
651
657
  return (
652
658
  plan_id,
653
659
  _YnabPlanData(
654
- accounts=accounts,
655
- category_groups=categories,
656
- payees=payees,
657
- transactions=transactions,
658
- server_knowledge=server_knowledge,
659
- scheduled_transactions=scheduled_transactions,
660
+ accounts=accounts.data.accounts,
661
+ category_groups=categories.data.category_groups,
662
+ payees=payees.data.payees,
663
+ transactions=transactions.data.transactions,
664
+ server_knowledge=transactions.data.server_knowledge,
665
+ scheduled_transactions=scheduled.data.scheduled_transactions,
660
666
  ),
661
667
  )
662
668
 
663
669
 
664
670
  @retry(stop=stop_after_attempt(3))
665
- async def _get_accounts(
666
- context: _Context, plan_id: str, lkos: dict[str, int], task_id: TaskID
667
- ) -> list[Account]:
668
- resp = await AccountsApi(context.api_client).get_accounts(
669
- plan_id=plan_id, last_knowledge_of_server=lkos.get(plan_id)
670
- )
671
- context.progress.update(task_id, advance=1)
672
- return resp.data.accounts
673
-
674
-
675
- @retry(stop=stop_after_attempt(3))
676
- async def _get_categories(
677
- context: _Context, plan_id: str, lkos: dict[str, int], task_id: TaskID
678
- ) -> list[CategoryGroupWithCategories]:
679
- resp = await CategoriesApi(context.api_client).get_categories(
680
- plan_id=plan_id, last_knowledge_of_server=lkos.get(plan_id)
681
- )
682
- context.progress.update(task_id, advance=1)
683
- return resp.data.category_groups
684
-
685
-
686
- @retry(stop=stop_after_attempt(3))
687
- async def _get_payees(
688
- context: _Context, plan_id: str, lkos: dict[str, int], task_id: TaskID
689
- ) -> list[Payee]:
690
- resp = await PayeesApi(context.api_client).get_payees(
691
- plan_id=plan_id, last_knowledge_of_server=lkos.get(plan_id)
692
- )
693
- context.progress.update(task_id, advance=1)
694
- return resp.data.payees
695
-
696
-
697
- @retry(stop=stop_after_attempt(3))
698
- async def _get_transactions(
699
- context: _Context, plan_id: str, lkos: dict[str, int], task_id: TaskID
700
- ) -> tuple[list[TransactionDetail], int]:
701
- resp = await TransactionsApi(context.api_client).get_transactions(
702
- plan_id=plan_id, last_knowledge_of_server=lkos.get(plan_id)
703
- )
704
- context.progress.update(task_id, advance=1)
705
- return resp.data.transactions, resp.data.server_knowledge
706
-
707
-
708
- @retry(stop=stop_after_attempt(3))
709
- async def _get_scheduled_transactions(
710
- context: _Context, plan_id: str, lkos: dict[str, int], task_id: TaskID
711
- ) -> list[ScheduledTransactionDetail]:
712
- resp = await ScheduledTransactionsApi(
713
- context.api_client
714
- ).get_scheduled_transactions(
715
- plan_id=plan_id, last_knowledge_of_server=lkos.get(plan_id)
716
- )
717
- context.progress.update(task_id, advance=1)
718
- return resp.data.scheduled_transactions
719
-
720
-
721
- # @retry(stop=stop_after_attempt(3))
722
- # async def _get_ynab[T](
723
- # context: _Context,
724
- # getter: Callable[..., Awaitable[T]],
725
- # task_id: TaskID,
726
- # ) -> T:
727
- # try:
728
- # return await getter()
729
- # finally:
730
- # context.progress.update(task_id, advance=1)
671
+ async def _get_ynab[T](
672
+ context: _Context,
673
+ endpoint: Callable[..., Awaitable[T]],
674
+ plan_id: str,
675
+ lkos: dict[str, int],
676
+ task_id: TaskID,
677
+ ) -> T:
678
+ try:
679
+ return await endpoint(
680
+ plan_id=plan_id, last_knowledge_of_server=lkos.get(plan_id)
681
+ )
682
+ finally:
683
+ context.progress.update(task_id, advance=1)
731
684
 
732
685
 
733
686
  def main(
@@ -56,6 +56,7 @@ CREATE TABLE IF NOT EXISTS category_groups (
56
56
  , plan_id TEXT
57
57
  , name TEXT
58
58
  , hidden BOOLEAN
59
+ , internal BOOLEAN
59
60
  , deleted BOOLEAN
60
61
  , FOREIGN KEY (plan_id) REFERENCES plans (id)
61
62
  )
@@ -68,6 +69,7 @@ CREATE TABLE IF NOT EXISTS categories (
68
69
  , category_group_name TEXT
69
70
  , name TEXT
70
71
  , hidden BOOLEAN
72
+ , internal BOOLEAN
71
73
  , original_category_group_id TEXT
72
74
  , note TEXT
73
75
  , budgeted INT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlite_export_for_ynab
3
- Version: 2.7.2
3
+ Version: 2.9.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
@@ -17,7 +17,7 @@ License-File: LICENSE
17
17
  Requires-Dist: aiohttp>=3
18
18
  Requires-Dist: aiopathlib
19
19
  Requires-Dist: aiosqlite
20
- Requires-Dist: asyncio-for-ynab
20
+ Requires-Dist: asyncio-for-ynab~=1.84.0
21
21
  Requires-Dist: fasteners
22
22
  Requires-Dist: rich>=10
23
23
  Requires-Dist: tenacity
@@ -272,7 +272,7 @@ WITH interest_by_account AS (
272
272
  SELECT
273
273
  plan_id
274
274
  , account_name
275
- , SUM(-amount_currency) AS total
275
+ , SUM(amount_currency) AS total
276
276
  FROM flat_transactions
277
277
  WHERE
278
278
  TRUE
@@ -536,7 +536,7 @@ WITH params AS (
536
536
  TRUE
537
537
  AND NOT c.deleted
538
538
  AND c.category_group_name != 'Credit Card Payments'
539
- AND c.category_group_name != 'Internal Master Category'
539
+ AND NOT c.internal
540
540
  AND (
541
541
  v.include_category_groups = ''
542
542
  OR EXISTS (
@@ -1,7 +1,7 @@
1
1
  aiohttp>=3
2
2
  aiopathlib
3
3
  aiosqlite
4
- asyncio-for-ynab
4
+ asyncio-for-ynab~=1.84.0
5
5
  fasteners
6
6
  rich>=10
7
7
  tenacity
@@ -156,6 +156,7 @@ CATEGORY_GROUPS: list[CategoryGroupWithCategories] = [
156
156
  id=UUID(CATEGORY_GROUP_ID_1),
157
157
  name=CATEGORY_GROUP_NAME_1,
158
158
  hidden=False,
159
+ internal=False,
159
160
  deleted=False,
160
161
  categories=[
161
162
  Category(
@@ -164,6 +165,7 @@ CATEGORY_GROUPS: list[CategoryGroupWithCategories] = [
164
165
  category_group_name=CATEGORY_GROUP_NAME_1,
165
166
  name=CATEGORY_NAME_1,
166
167
  hidden=False,
168
+ internal=False,
167
169
  original_category_group_id=None,
168
170
  note=None,
169
171
  budgeted=14500,
@@ -206,6 +208,7 @@ CATEGORY_GROUPS: list[CategoryGroupWithCategories] = [
206
208
  category_group_name=CATEGORY_GROUP_NAME_1,
207
209
  name=CATEGORY_NAME_2,
208
210
  hidden=False,
211
+ internal=False,
209
212
  original_category_group_id=None,
210
213
  note=None,
211
214
  budgeted=10250,
@@ -248,6 +251,7 @@ CATEGORY_GROUPS: list[CategoryGroupWithCategories] = [
248
251
  id=UUID(CATEGORY_GROUP_ID_2),
249
252
  name=CATEGORY_GROUP_NAME_2,
250
253
  hidden=False,
254
+ internal=False,
251
255
  deleted=False,
252
256
  categories=[
253
257
  Category(
@@ -256,6 +260,7 @@ CATEGORY_GROUPS: list[CategoryGroupWithCategories] = [
256
260
  category_group_name=CATEGORY_GROUP_NAME_2,
257
261
  name=CATEGORY_NAME_3,
258
262
  hidden=False,
263
+ internal=False,
259
264
  original_category_group_id=None,
260
265
  note=None,
261
266
  budgeted=15000,
@@ -298,6 +303,7 @@ CATEGORY_GROUPS: list[CategoryGroupWithCategories] = [
298
303
  category_group_name=CATEGORY_GROUP_NAME_2,
299
304
  name=CATEGORY_NAME_4,
300
305
  hidden=False,
306
+ internal=False,
301
307
  original_category_group_id=None,
302
308
  note=None,
303
309
  budgeted=20000,
@@ -29,6 +29,7 @@ from sqlite_export_for_ynab._main import get_last_knowledge_of_server
29
29
  from sqlite_export_for_ynab._main import get_relations
30
30
  from sqlite_export_for_ynab._main import insert_accounts
31
31
  from sqlite_export_for_ynab._main import insert_category_groups
32
+ from sqlite_export_for_ynab._main import insert_entries
32
33
  from sqlite_export_for_ynab._main import insert_payees
33
34
  from sqlite_export_for_ynab._main import insert_plans
34
35
  from sqlite_export_for_ynab._main import insert_scheduled_transactions
@@ -255,6 +256,7 @@ async def test_insert_category_groups(context):
255
256
  "name": CATEGORY_GROUP_NAME_1,
256
257
  "plan_id": PLAN_ID_1,
257
258
  "hidden": False,
259
+ "internal": False,
258
260
  "deleted": False,
259
261
  },
260
262
  {
@@ -262,6 +264,7 @@ async def test_insert_category_groups(context):
262
264
  "name": CATEGORY_GROUP_NAME_2,
263
265
  "plan_id": PLAN_ID_1,
264
266
  "hidden": False,
267
+ "internal": False,
265
268
  "deleted": False,
266
269
  },
267
270
  ],
@@ -277,6 +280,7 @@ async def test_insert_category_groups(context):
277
280
  "plan_id": PLAN_ID_1,
278
281
  "name": CATEGORY_NAME_1,
279
282
  "hidden": False,
283
+ "internal": False,
280
284
  "original_category_group_id": None,
281
285
  "note": None,
282
286
  "budgeted": 14500,
@@ -319,6 +323,7 @@ async def test_insert_category_groups(context):
319
323
  "plan_id": PLAN_ID_1,
320
324
  "name": CATEGORY_NAME_2,
321
325
  "hidden": False,
326
+ "internal": False,
322
327
  "original_category_group_id": None,
323
328
  "note": None,
324
329
  "budgeted": 10250,
@@ -361,6 +366,7 @@ async def test_insert_category_groups(context):
361
366
  "plan_id": PLAN_ID_1,
362
367
  "name": CATEGORY_NAME_3,
363
368
  "hidden": False,
369
+ "internal": False,
364
370
  "original_category_group_id": None,
365
371
  "note": None,
366
372
  "budgeted": 15000,
@@ -403,6 +409,7 @@ async def test_insert_category_groups(context):
403
409
  "plan_id": PLAN_ID_1,
404
410
  "name": CATEGORY_NAME_4,
405
411
  "hidden": False,
412
+ "internal": False,
406
413
  "original_category_group_id": None,
407
414
  "note": None,
408
415
  "budgeted": 20000,
@@ -458,6 +465,7 @@ async def test_insert_category_group_without_categories(context):
458
465
  "name": CATEGORY_GROUP_NAME_1,
459
466
  "plan_id": PLAN_ID_1,
460
467
  "hidden": False,
468
+ "internal": False,
461
469
  "deleted": False,
462
470
  },
463
471
  ],
@@ -492,6 +500,31 @@ async def test_insert_payees(context):
492
500
  )
493
501
 
494
502
 
503
+ @pytest.mark.asyncio
504
+ async def test_insert_entries_ignores_unknown_keys(context):
505
+ task_id = context.progress.add_task("Payees", total=1)
506
+ entry = {
507
+ "id": PAYEE_ID_1,
508
+ "name": "Payee",
509
+ "transfer_account_id": None,
510
+ "deleted": False,
511
+ "brand_new_api_field": "surprise",
512
+ }
513
+ await insert_entries(context, "payees", PLAN_ID_1, [entry], task_id)
514
+ assert_rows(
515
+ await fetchall(context.con, "SELECT * FROM payees"),
516
+ [
517
+ {
518
+ "id": PAYEE_ID_1,
519
+ "plan_id": PLAN_ID_1,
520
+ "name": "Payee",
521
+ "transfer_account_id": None,
522
+ "deleted": False,
523
+ }
524
+ ],
525
+ )
526
+
527
+
495
528
  @pytest.mark.asyncio
496
529
  async def test_insert_transactions(context):
497
530
  await insert_transactions(context, PLAN_ID_1, [])