sqlite-export-for-ynab 1.6.1__tar.gz → 2.0.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-1.6.1/sqlite_export_for_ynab.egg-info → sqlite_export_for_ynab-2.0.0}/PKG-INFO +33 -30
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/README.md +30 -27
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/setup.cfg +3 -3
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab/_main.py +66 -62
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab/ddl/create-relations.sql +31 -28
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab/ddl/drop-relations.sql +1 -1
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0/sqlite_export_for_ynab.egg-info}/PKG-INFO +33 -30
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/testing/fixtures.py +12 -10
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/tests/_main_test.py +59 -57
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/LICENSE +0 -0
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/pyproject.toml +0 -0
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/setup.py +0 -0
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab/__init__.py +0 -0
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab/__main__.py +0 -0
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab/ddl/__init__.py +0 -0
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab/py.typed +0 -0
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab.egg-info/SOURCES.txt +0 -0
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab.egg-info/dependency_links.txt +0 -0
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab.egg-info/entry_points.txt +0 -0
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab.egg-info/requires.txt +0 -0
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab.egg-info/top_level.txt +0 -0
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/testing/__init__.py +0 -0
- {sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/tests/__init__.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlite_export_for_ynab
|
|
3
|
-
Version:
|
|
4
|
-
Summary: SQLite Export for YNAB - Export YNAB
|
|
3
|
+
Version: 2.0.0
|
|
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
|
|
7
7
|
Author-email: maxr@outlook.com
|
|
8
8
|
License: MIT
|
|
9
|
-
Keywords: ynab,sqlite,sql,budget,cli
|
|
9
|
+
Keywords: ynab,sqlite,sql,budget,plan,cli
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
12
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
@@ -26,7 +26,7 @@ SQLite Export for YNAB - Export YNAB Budget Data to SQLite
|
|
|
26
26
|
|
|
27
27
|
## What This Does
|
|
28
28
|
|
|
29
|
-
Export your [YNAB](https://ynab.com/)
|
|
29
|
+
Export all your [YNAB](https://ynab.com/) plans to a local [SQLite](https://www.sqlite.org/) DB. Then you can query your data with any tools compatible with SQLite.
|
|
30
30
|
|
|
31
31
|
## Installation
|
|
32
32
|
|
|
@@ -44,7 +44,7 @@ Provision a [YNAB Personal Access Token](https://api.ynab.com/#personal-access-t
|
|
|
44
44
|
$ export YNAB_PERSONAL_ACCESS_TOKEN="..."
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Run the tool from the terminal to download your
|
|
47
|
+
Run the tool from the terminal to download your plans:
|
|
48
48
|
|
|
49
49
|
```console
|
|
50
50
|
$ sqlite-export-for-ynab
|
|
@@ -80,7 +80,7 @@ asyncio.run(sync(token, db, full_refresh))
|
|
|
80
80
|
The relations are defined in [create-relations.sql](sqlite_export_for_ynab/ddl/create-relations.sql). They are 1:1 with [YNAB's OpenAPI Spec](https://api.ynab.com/papi/open_api_spec.yaml) (ex: transactions, accounts, etc) with some additions:
|
|
81
81
|
|
|
82
82
|
1. Some objects are pulled out into their own tables so they can be more cleanly modeled in SQLite (ex: subtransactions, loan account periodic values).
|
|
83
|
-
1. Foreign keys are added as needed (ex:
|
|
83
|
+
1. Foreign keys are added as needed (ex: plan ID, transaction ID) so data across plans remains separate.
|
|
84
84
|
1. Two new views called `flat_transactions` and `scheduled_flat_transactions`. These allow you to query split and non-split transactions easily, without needing to also query `subtransactions` and `scheduled_subtransactions` respectively. They also include fields to improve quality of life (ex: `amount_major` to convert from [YNAB's milliunits](https://api.ynab.com/#formats) to [major units](https://en.wikipedia.org/wiki/ISO_4217) i.e. dollars) and filter out deleted transactions/subtransactions.
|
|
85
85
|
|
|
86
86
|
## Querying
|
|
@@ -89,35 +89,35 @@ You can issue queries with typical SQLite tools. *`sqlite-export-for-ynab` delib
|
|
|
89
89
|
|
|
90
90
|
### Sample Queries
|
|
91
91
|
|
|
92
|
-
To get the top 5 payees by spending per
|
|
92
|
+
To get the top 5 payees by spending per plan, you could do:
|
|
93
93
|
|
|
94
94
|
```sql
|
|
95
95
|
WITH
|
|
96
96
|
ranked_payees AS (
|
|
97
97
|
SELECT
|
|
98
|
-
|
|
98
|
+
pl."name" AS plan_name
|
|
99
99
|
, t.payee_name AS payee
|
|
100
100
|
, SUM(t.amount_major) AS net_spent
|
|
101
101
|
, ROW_NUMBER() OVER (
|
|
102
102
|
PARTITION BY
|
|
103
|
-
|
|
103
|
+
pl.id
|
|
104
104
|
ORDER BY
|
|
105
105
|
SUM(t.amount) ASC
|
|
106
106
|
) AS rnk
|
|
107
107
|
FROM
|
|
108
108
|
flat_transactions AS t
|
|
109
|
-
INNER JOIN
|
|
110
|
-
ON t.
|
|
109
|
+
INNER JOIN plans AS pl
|
|
110
|
+
ON t.plan_id = pl.id
|
|
111
111
|
WHERE
|
|
112
112
|
t.payee_name != 'Starting Balance'
|
|
113
113
|
AND t.transfer_account_id IS NULL
|
|
114
114
|
GROUP BY
|
|
115
|
-
|
|
115
|
+
pl.id
|
|
116
116
|
, t.payee_id
|
|
117
117
|
)
|
|
118
118
|
|
|
119
119
|
SELECT
|
|
120
|
-
|
|
120
|
+
plan_name
|
|
121
121
|
, payee
|
|
122
122
|
, net_spent
|
|
123
123
|
FROM
|
|
@@ -125,7 +125,7 @@ FROM
|
|
|
125
125
|
WHERE
|
|
126
126
|
rnk <= 5
|
|
127
127
|
ORDER BY
|
|
128
|
-
|
|
128
|
+
plan_name ASC
|
|
129
129
|
, net_spent DESC
|
|
130
130
|
;
|
|
131
131
|
```
|
|
@@ -134,41 +134,44 @@ To get duplicate payees, or payees with no transactions:
|
|
|
134
134
|
|
|
135
135
|
```sql
|
|
136
136
|
SELECT DISTINCT
|
|
137
|
-
|
|
138
|
-
, dupes.name AS payee
|
|
137
|
+
pl."name" AS "plan"
|
|
138
|
+
, dupes."name" AS payee
|
|
139
139
|
FROM (
|
|
140
140
|
SELECT DISTINCT
|
|
141
|
-
p.
|
|
142
|
-
, p.name
|
|
141
|
+
p.plan_id
|
|
142
|
+
, p."name"
|
|
143
143
|
FROM payees AS p
|
|
144
144
|
LEFT JOIN flat_transactions AS ft
|
|
145
145
|
ON
|
|
146
|
-
p.
|
|
146
|
+
p.plan_id = ft.plan_id
|
|
147
147
|
AND p.id = ft.payee_id
|
|
148
148
|
LEFT JOIN scheduled_flat_transactions AS sft
|
|
149
149
|
ON
|
|
150
|
-
p.
|
|
150
|
+
p.plan_id = sft.plan_id
|
|
151
151
|
AND p.id = sft.payee_id
|
|
152
152
|
WHERE
|
|
153
153
|
TRUE
|
|
154
154
|
AND ft.payee_id IS NULL
|
|
155
155
|
AND sft.payee_id IS NULL
|
|
156
156
|
AND p.transfer_account_id IS NULL
|
|
157
|
-
AND p.name != 'Reconciliation Balance Adjustment'
|
|
157
|
+
AND p."name" != 'Reconciliation Balance Adjustment'
|
|
158
|
+
AND p."name" != 'Manual Balance Adjustment'
|
|
159
|
+
AND NOT p.deleted
|
|
158
160
|
|
|
159
161
|
UNION ALL
|
|
160
162
|
|
|
161
163
|
SELECT
|
|
162
|
-
|
|
163
|
-
, name
|
|
164
|
+
plan_id
|
|
165
|
+
, "name"
|
|
164
166
|
FROM payees
|
|
165
|
-
|
|
167
|
+
WHERE NOT deleted
|
|
168
|
+
GROUP BY plan_id, "name"
|
|
166
169
|
HAVING COUNT(*) > 1
|
|
167
170
|
|
|
168
171
|
) AS dupes
|
|
169
|
-
INNER JOIN
|
|
170
|
-
ON dupes.
|
|
171
|
-
ORDER BY
|
|
172
|
+
INNER JOIN plans AS pl
|
|
173
|
+
ON dupes.plan_id = pl.id
|
|
174
|
+
ORDER BY "plan", payee
|
|
172
175
|
;
|
|
173
176
|
```
|
|
174
177
|
|
|
@@ -176,11 +179,11 @@ To count the spend for a category (ex: "Apps") between this month and the next 1
|
|
|
176
179
|
|
|
177
180
|
```sql
|
|
178
181
|
SELECT
|
|
179
|
-
|
|
182
|
+
plan_id
|
|
180
183
|
, SUM(amount_major) AS amount_major
|
|
181
184
|
FROM (
|
|
182
185
|
SELECT
|
|
183
|
-
|
|
186
|
+
plan_id
|
|
184
187
|
, amount_major
|
|
185
188
|
FROM flat_transactions
|
|
186
189
|
WHERE
|
|
@@ -188,7 +191,7 @@ FROM (
|
|
|
188
191
|
AND SUBSTR(`date`, 1, 7) = SUBSTR(DATE(), 1, 7)
|
|
189
192
|
UNION ALL
|
|
190
193
|
SELECT
|
|
191
|
-
|
|
194
|
+
plan_id
|
|
192
195
|
, amount_major * (
|
|
193
196
|
CASE
|
|
194
197
|
WHEN frequency = 'monthly' THEN 11
|
|
@@ -6,7 +6,7 @@ SQLite Export for YNAB - Export YNAB Budget Data to SQLite
|
|
|
6
6
|
|
|
7
7
|
## What This Does
|
|
8
8
|
|
|
9
|
-
Export your [YNAB](https://ynab.com/)
|
|
9
|
+
Export all your [YNAB](https://ynab.com/) plans to a local [SQLite](https://www.sqlite.org/) DB. Then you can query your data with any tools compatible with SQLite.
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
@@ -24,7 +24,7 @@ Provision a [YNAB Personal Access Token](https://api.ynab.com/#personal-access-t
|
|
|
24
24
|
$ export YNAB_PERSONAL_ACCESS_TOKEN="..."
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Run the tool from the terminal to download your
|
|
27
|
+
Run the tool from the terminal to download your plans:
|
|
28
28
|
|
|
29
29
|
```console
|
|
30
30
|
$ sqlite-export-for-ynab
|
|
@@ -60,7 +60,7 @@ asyncio.run(sync(token, db, full_refresh))
|
|
|
60
60
|
The relations are defined in [create-relations.sql](sqlite_export_for_ynab/ddl/create-relations.sql). They are 1:1 with [YNAB's OpenAPI Spec](https://api.ynab.com/papi/open_api_spec.yaml) (ex: transactions, accounts, etc) with some additions:
|
|
61
61
|
|
|
62
62
|
1. Some objects are pulled out into their own tables so they can be more cleanly modeled in SQLite (ex: subtransactions, loan account periodic values).
|
|
63
|
-
1. Foreign keys are added as needed (ex:
|
|
63
|
+
1. Foreign keys are added as needed (ex: plan ID, transaction ID) so data across plans remains separate.
|
|
64
64
|
1. Two new views called `flat_transactions` and `scheduled_flat_transactions`. These allow you to query split and non-split transactions easily, without needing to also query `subtransactions` and `scheduled_subtransactions` respectively. They also include fields to improve quality of life (ex: `amount_major` to convert from [YNAB's milliunits](https://api.ynab.com/#formats) to [major units](https://en.wikipedia.org/wiki/ISO_4217) i.e. dollars) and filter out deleted transactions/subtransactions.
|
|
65
65
|
|
|
66
66
|
## Querying
|
|
@@ -69,35 +69,35 @@ You can issue queries with typical SQLite tools. *`sqlite-export-for-ynab` delib
|
|
|
69
69
|
|
|
70
70
|
### Sample Queries
|
|
71
71
|
|
|
72
|
-
To get the top 5 payees by spending per
|
|
72
|
+
To get the top 5 payees by spending per plan, you could do:
|
|
73
73
|
|
|
74
74
|
```sql
|
|
75
75
|
WITH
|
|
76
76
|
ranked_payees AS (
|
|
77
77
|
SELECT
|
|
78
|
-
|
|
78
|
+
pl."name" AS plan_name
|
|
79
79
|
, t.payee_name AS payee
|
|
80
80
|
, SUM(t.amount_major) AS net_spent
|
|
81
81
|
, ROW_NUMBER() OVER (
|
|
82
82
|
PARTITION BY
|
|
83
|
-
|
|
83
|
+
pl.id
|
|
84
84
|
ORDER BY
|
|
85
85
|
SUM(t.amount) ASC
|
|
86
86
|
) AS rnk
|
|
87
87
|
FROM
|
|
88
88
|
flat_transactions AS t
|
|
89
|
-
INNER JOIN
|
|
90
|
-
ON t.
|
|
89
|
+
INNER JOIN plans AS pl
|
|
90
|
+
ON t.plan_id = pl.id
|
|
91
91
|
WHERE
|
|
92
92
|
t.payee_name != 'Starting Balance'
|
|
93
93
|
AND t.transfer_account_id IS NULL
|
|
94
94
|
GROUP BY
|
|
95
|
-
|
|
95
|
+
pl.id
|
|
96
96
|
, t.payee_id
|
|
97
97
|
)
|
|
98
98
|
|
|
99
99
|
SELECT
|
|
100
|
-
|
|
100
|
+
plan_name
|
|
101
101
|
, payee
|
|
102
102
|
, net_spent
|
|
103
103
|
FROM
|
|
@@ -105,7 +105,7 @@ FROM
|
|
|
105
105
|
WHERE
|
|
106
106
|
rnk <= 5
|
|
107
107
|
ORDER BY
|
|
108
|
-
|
|
108
|
+
plan_name ASC
|
|
109
109
|
, net_spent DESC
|
|
110
110
|
;
|
|
111
111
|
```
|
|
@@ -114,41 +114,44 @@ To get duplicate payees, or payees with no transactions:
|
|
|
114
114
|
|
|
115
115
|
```sql
|
|
116
116
|
SELECT DISTINCT
|
|
117
|
-
|
|
118
|
-
, dupes.name AS payee
|
|
117
|
+
pl."name" AS "plan"
|
|
118
|
+
, dupes."name" AS payee
|
|
119
119
|
FROM (
|
|
120
120
|
SELECT DISTINCT
|
|
121
|
-
p.
|
|
122
|
-
, p.name
|
|
121
|
+
p.plan_id
|
|
122
|
+
, p."name"
|
|
123
123
|
FROM payees AS p
|
|
124
124
|
LEFT JOIN flat_transactions AS ft
|
|
125
125
|
ON
|
|
126
|
-
p.
|
|
126
|
+
p.plan_id = ft.plan_id
|
|
127
127
|
AND p.id = ft.payee_id
|
|
128
128
|
LEFT JOIN scheduled_flat_transactions AS sft
|
|
129
129
|
ON
|
|
130
|
-
p.
|
|
130
|
+
p.plan_id = sft.plan_id
|
|
131
131
|
AND p.id = sft.payee_id
|
|
132
132
|
WHERE
|
|
133
133
|
TRUE
|
|
134
134
|
AND ft.payee_id IS NULL
|
|
135
135
|
AND sft.payee_id IS NULL
|
|
136
136
|
AND p.transfer_account_id IS NULL
|
|
137
|
-
AND p.name != 'Reconciliation Balance Adjustment'
|
|
137
|
+
AND p."name" != 'Reconciliation Balance Adjustment'
|
|
138
|
+
AND p."name" != 'Manual Balance Adjustment'
|
|
139
|
+
AND NOT p.deleted
|
|
138
140
|
|
|
139
141
|
UNION ALL
|
|
140
142
|
|
|
141
143
|
SELECT
|
|
142
|
-
|
|
143
|
-
, name
|
|
144
|
+
plan_id
|
|
145
|
+
, "name"
|
|
144
146
|
FROM payees
|
|
145
|
-
|
|
147
|
+
WHERE NOT deleted
|
|
148
|
+
GROUP BY plan_id, "name"
|
|
146
149
|
HAVING COUNT(*) > 1
|
|
147
150
|
|
|
148
151
|
) AS dupes
|
|
149
|
-
INNER JOIN
|
|
150
|
-
ON dupes.
|
|
151
|
-
ORDER BY
|
|
152
|
+
INNER JOIN plans AS pl
|
|
153
|
+
ON dupes.plan_id = pl.id
|
|
154
|
+
ORDER BY "plan", payee
|
|
152
155
|
;
|
|
153
156
|
```
|
|
154
157
|
|
|
@@ -156,11 +159,11 @@ To count the spend for a category (ex: "Apps") between this month and the next 1
|
|
|
156
159
|
|
|
157
160
|
```sql
|
|
158
161
|
SELECT
|
|
159
|
-
|
|
162
|
+
plan_id
|
|
160
163
|
, SUM(amount_major) AS amount_major
|
|
161
164
|
FROM (
|
|
162
165
|
SELECT
|
|
163
|
-
|
|
166
|
+
plan_id
|
|
164
167
|
, amount_major
|
|
165
168
|
FROM flat_transactions
|
|
166
169
|
WHERE
|
|
@@ -168,7 +171,7 @@ FROM (
|
|
|
168
171
|
AND SUBSTR(`date`, 1, 7) = SUBSTR(DATE(), 1, 7)
|
|
169
172
|
UNION ALL
|
|
170
173
|
SELECT
|
|
171
|
-
|
|
174
|
+
plan_id
|
|
172
175
|
, amount_major * (
|
|
173
176
|
CASE
|
|
174
177
|
WHEN frequency = 'monthly' THEN 11
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = sqlite_export_for_ynab
|
|
3
|
-
version =
|
|
4
|
-
description = SQLite Export for YNAB - Export YNAB
|
|
3
|
+
version = 2.0.0
|
|
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
|
|
7
7
|
url = https://github.com/mxr/sqlite-export-for-ynab
|
|
@@ -14,7 +14,7 @@ classifiers =
|
|
|
14
14
|
Programming Language :: Python :: 3 :: Only
|
|
15
15
|
Programming Language :: Python :: Implementation :: CPython
|
|
16
16
|
Programming Language :: Python :: Implementation :: PyPy
|
|
17
|
-
keywords = ynab, sqlite, sql, budget, cli
|
|
17
|
+
keywords = ynab, sqlite, sql, budget, plan, cli
|
|
18
18
|
|
|
19
19
|
[options]
|
|
20
20
|
packages = find:
|
{sqlite_export_for_ynab-1.6.1 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab/_main.py
RENAMED
|
@@ -41,7 +41,7 @@ _EntryTable = (
|
|
|
41
41
|
| Literal["scheduled_subtransactions"]
|
|
42
42
|
)
|
|
43
43
|
_ALL_RELATIONS = frozenset(
|
|
44
|
-
("
|
|
44
|
+
("plans", "flat_transactions", "scheduled_flat_transactions")
|
|
45
45
|
+ tuple(lit.__args__[0] for lit in _EntryTable.__args__)
|
|
46
46
|
)
|
|
47
47
|
|
|
@@ -61,7 +61,7 @@ async def async_main(argv: Sequence[str] | None = None) -> int:
|
|
|
61
61
|
parser.add_argument(
|
|
62
62
|
"--full-refresh",
|
|
63
63
|
action="store_true",
|
|
64
|
-
help="**DROP ALL TABLES** and fetch all
|
|
64
|
+
help="**DROP ALL TABLES** and fetch all data again.",
|
|
65
65
|
)
|
|
66
66
|
parser.add_argument(
|
|
67
67
|
"--version", action="version", version=f"%(prog)s {version(_PACKAGE)}"
|
|
@@ -98,9 +98,9 @@ def default_db_path() -> Path:
|
|
|
98
98
|
|
|
99
99
|
async def sync(token: str, db: Path, full_refresh: bool) -> None:
|
|
100
100
|
async with aiohttp.ClientSession() as session:
|
|
101
|
-
|
|
101
|
+
plans = (await YnabClient(token, session)("plans"))["plans"]
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
plan_ids = [plan["id"] for plan in plans]
|
|
104
104
|
|
|
105
105
|
if not db.exists():
|
|
106
106
|
db.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -122,17 +122,17 @@ async def sync(token: str, db: Path, full_refresh: bool) -> None:
|
|
|
122
122
|
con.commit()
|
|
123
123
|
print("Done")
|
|
124
124
|
|
|
125
|
-
print("Fetching
|
|
125
|
+
print("Fetching plan data...")
|
|
126
126
|
lkos = get_last_knowledge_of_server(cur)
|
|
127
127
|
async with aiohttp.ClientSession() as session:
|
|
128
|
-
with tldm(desc="
|
|
128
|
+
with tldm(desc="Plan Data", total=len(plans) * 5) as pbar:
|
|
129
129
|
yc = ProgressYnabClient(YnabClient(token, session), pbar)
|
|
130
130
|
|
|
131
|
-
account_jobs = jobs(yc, "accounts",
|
|
132
|
-
cat_jobs = jobs(yc, "categories",
|
|
133
|
-
payee_jobs = jobs(yc, "payees",
|
|
134
|
-
txn_jobs = jobs(yc, "transactions",
|
|
135
|
-
sched_txn_jobs = jobs(yc, "scheduled_transactions",
|
|
131
|
+
account_jobs = jobs(yc, "accounts", plan_ids, lkos)
|
|
132
|
+
cat_jobs = jobs(yc, "categories", plan_ids, lkos)
|
|
133
|
+
payee_jobs = jobs(yc, "payees", plan_ids, lkos)
|
|
134
|
+
txn_jobs = jobs(yc, "transactions", plan_ids, lkos)
|
|
135
|
+
sched_txn_jobs = jobs(yc, "scheduled_transactions", plan_ids, lkos)
|
|
136
136
|
|
|
137
137
|
data = await asyncio.gather(
|
|
138
138
|
*account_jobs, *cat_jobs, *payee_jobs, *txn_jobs, *sched_txn_jobs
|
|
@@ -150,8 +150,10 @@ async def sync(token: str, db: Path, full_refresh: bool) -> None:
|
|
|
150
150
|
all_sched_txn_data = data[la + lc + lp + lt :]
|
|
151
151
|
|
|
152
152
|
new_lkos = {
|
|
153
|
-
|
|
154
|
-
for
|
|
153
|
+
plan_id: transaction_data["server_knowledge"]
|
|
154
|
+
for plan_id, transaction_data in zip(
|
|
155
|
+
plan_ids, all_txn_data, strict=True
|
|
156
|
+
)
|
|
155
157
|
}
|
|
156
158
|
print("Done")
|
|
157
159
|
|
|
@@ -164,19 +166,21 @@ async def sync(token: str, db: Path, full_refresh: bool) -> None:
|
|
|
164
166
|
):
|
|
165
167
|
print("No new data fetched")
|
|
166
168
|
else:
|
|
167
|
-
print("Inserting
|
|
168
|
-
|
|
169
|
-
for
|
|
170
|
-
insert_accounts(cur,
|
|
171
|
-
for
|
|
172
|
-
insert_category_groups(cur,
|
|
173
|
-
for
|
|
174
|
-
insert_payees(cur,
|
|
175
|
-
for
|
|
176
|
-
insert_transactions(cur,
|
|
177
|
-
for
|
|
169
|
+
print("Inserting plan data...")
|
|
170
|
+
insert_plans(cur, plans, new_lkos)
|
|
171
|
+
for plan_id, account_data in zip(plan_ids, all_account_data, strict=True):
|
|
172
|
+
insert_accounts(cur, plan_id, account_data["accounts"])
|
|
173
|
+
for plan_id, cat_data in zip(plan_ids, all_cat_data, strict=True):
|
|
174
|
+
insert_category_groups(cur, plan_id, cat_data["category_groups"])
|
|
175
|
+
for plan_id, payee_data in zip(plan_ids, all_payee_data, strict=True):
|
|
176
|
+
insert_payees(cur, plan_id, payee_data["payees"])
|
|
177
|
+
for plan_id, txn_data in zip(plan_ids, all_txn_data, strict=True):
|
|
178
|
+
insert_transactions(cur, plan_id, txn_data["transactions"])
|
|
179
|
+
for plan_id, sched_txn_data in zip(
|
|
180
|
+
plan_ids, all_sched_txn_data, strict=True
|
|
181
|
+
):
|
|
178
182
|
insert_scheduled_transactions(
|
|
179
|
-
cur,
|
|
183
|
+
cur, plan_id, sched_txn_data["scheduled_transactions"]
|
|
180
184
|
)
|
|
181
185
|
print("Done")
|
|
182
186
|
|
|
@@ -202,17 +206,17 @@ def get_last_knowledge_of_server(cur: sqlite3.Cursor) -> dict[str, int]:
|
|
|
202
206
|
return {
|
|
203
207
|
r["id"]: r["last_knowledge_of_server"]
|
|
204
208
|
for r in cur.execute(
|
|
205
|
-
"SELECT id, last_knowledge_of_server FROM
|
|
209
|
+
"SELECT id, last_knowledge_of_server FROM plans",
|
|
206
210
|
).fetchall()
|
|
207
211
|
}
|
|
208
212
|
|
|
209
213
|
|
|
210
|
-
def
|
|
211
|
-
cur: sqlite3.Cursor,
|
|
214
|
+
def insert_plans(
|
|
215
|
+
cur: sqlite3.Cursor, plans: list[dict[str, Any]], lkos: dict[str, int]
|
|
212
216
|
) -> None:
|
|
213
217
|
cur.executemany(
|
|
214
218
|
"""
|
|
215
|
-
INSERT OR REPLACE INTO
|
|
219
|
+
INSERT OR REPLACE INTO plans (
|
|
216
220
|
id
|
|
217
221
|
, name
|
|
218
222
|
, currency_format_currency_symbol
|
|
@@ -227,18 +231,18 @@ def insert_budgets(
|
|
|
227
231
|
""",
|
|
228
232
|
(
|
|
229
233
|
(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
lkos[
|
|
234
|
+
plan_id := plan["id"],
|
|
235
|
+
plan["name"],
|
|
236
|
+
plan["currency_format"]["currency_symbol"],
|
|
237
|
+
plan["currency_format"]["decimal_digits"],
|
|
238
|
+
plan["currency_format"]["decimal_separator"],
|
|
239
|
+
plan["currency_format"]["display_symbol"],
|
|
240
|
+
plan["currency_format"]["group_separator"],
|
|
241
|
+
plan["currency_format"]["iso_code"],
|
|
242
|
+
plan["currency_format"]["symbol_first"],
|
|
243
|
+
lkos[plan_id],
|
|
240
244
|
)
|
|
241
|
-
for
|
|
245
|
+
for plan in plans
|
|
242
246
|
),
|
|
243
247
|
)
|
|
244
248
|
|
|
@@ -249,7 +253,7 @@ _LOAN_ACCOUNT_PERIODIC_VALUES = frozenset(
|
|
|
249
253
|
|
|
250
254
|
|
|
251
255
|
def insert_accounts(
|
|
252
|
-
cur: sqlite3.Cursor,
|
|
256
|
+
cur: sqlite3.Cursor, plan_id: str, accounts: list[dict[str, Any]]
|
|
253
257
|
) -> None:
|
|
254
258
|
# YNAB's LoanAccountPeriodValues are untyped dicts so we need to turn them into a more standard sub-entry view
|
|
255
259
|
updated_accounts = [
|
|
@@ -271,7 +275,7 @@ def insert_accounts(
|
|
|
271
275
|
|
|
272
276
|
return insert_nested_entries(
|
|
273
277
|
cur,
|
|
274
|
-
|
|
278
|
+
plan_id,
|
|
275
279
|
updated_accounts,
|
|
276
280
|
"Accounts",
|
|
277
281
|
"accounts",
|
|
@@ -281,11 +285,11 @@ def insert_accounts(
|
|
|
281
285
|
|
|
282
286
|
|
|
283
287
|
def insert_category_groups(
|
|
284
|
-
cur: sqlite3.Cursor,
|
|
288
|
+
cur: sqlite3.Cursor, plan_id: str, category_groups: list[dict[str, Any]]
|
|
285
289
|
) -> None:
|
|
286
290
|
return insert_nested_entries(
|
|
287
291
|
cur,
|
|
288
|
-
|
|
292
|
+
plan_id,
|
|
289
293
|
category_groups,
|
|
290
294
|
"Categories",
|
|
291
295
|
"category_groups",
|
|
@@ -295,21 +299,21 @@ def insert_category_groups(
|
|
|
295
299
|
|
|
296
300
|
|
|
297
301
|
def insert_payees(
|
|
298
|
-
cur: sqlite3.Cursor,
|
|
302
|
+
cur: sqlite3.Cursor, plan_id: str, payees: list[dict[str, Any]]
|
|
299
303
|
) -> None:
|
|
300
304
|
if not payees:
|
|
301
305
|
return
|
|
302
306
|
|
|
303
307
|
for payee in tldm(payees, desc="Payees"):
|
|
304
|
-
insert_entry(cur, "payees",
|
|
308
|
+
insert_entry(cur, "payees", plan_id, payee)
|
|
305
309
|
|
|
306
310
|
|
|
307
311
|
def insert_transactions(
|
|
308
|
-
cur: sqlite3.Cursor,
|
|
312
|
+
cur: sqlite3.Cursor, plan_id: str, transactions: list[dict[str, Any]]
|
|
309
313
|
) -> None:
|
|
310
314
|
return insert_nested_entries(
|
|
311
315
|
cur,
|
|
312
|
-
|
|
316
|
+
plan_id,
|
|
313
317
|
transactions,
|
|
314
318
|
"Transactions",
|
|
315
319
|
"transactions",
|
|
@@ -319,11 +323,11 @@ def insert_transactions(
|
|
|
319
323
|
|
|
320
324
|
|
|
321
325
|
def insert_scheduled_transactions(
|
|
322
|
-
cur: sqlite3.Cursor,
|
|
326
|
+
cur: sqlite3.Cursor, plan_id: str, scheduled_transactions: list[dict[str, Any]]
|
|
323
327
|
) -> None:
|
|
324
328
|
return insert_nested_entries(
|
|
325
329
|
cur,
|
|
326
|
-
|
|
330
|
+
plan_id,
|
|
327
331
|
scheduled_transactions,
|
|
328
332
|
"Scheduled Transactions",
|
|
329
333
|
"scheduled_transactions",
|
|
@@ -335,7 +339,7 @@ def insert_scheduled_transactions(
|
|
|
335
339
|
@overload
|
|
336
340
|
def insert_nested_entries(
|
|
337
341
|
cur: sqlite3.Cursor,
|
|
338
|
-
|
|
342
|
+
plan_id: str,
|
|
339
343
|
entries: list[dict[str, Any]],
|
|
340
344
|
desc: Literal["Accounts"],
|
|
341
345
|
entries_name: Literal["accounts"],
|
|
@@ -347,7 +351,7 @@ def insert_nested_entries(
|
|
|
347
351
|
@overload
|
|
348
352
|
def insert_nested_entries(
|
|
349
353
|
cur: sqlite3.Cursor,
|
|
350
|
-
|
|
354
|
+
plan_id: str,
|
|
351
355
|
entries: list[dict[str, Any]],
|
|
352
356
|
desc: Literal["Categories"],
|
|
353
357
|
entries_name: Literal["category_groups"],
|
|
@@ -359,7 +363,7 @@ def insert_nested_entries(
|
|
|
359
363
|
@overload
|
|
360
364
|
def insert_nested_entries(
|
|
361
365
|
cur: sqlite3.Cursor,
|
|
362
|
-
|
|
366
|
+
plan_id: str,
|
|
363
367
|
entries: list[dict[str, Any]],
|
|
364
368
|
desc: Literal["Transactions"],
|
|
365
369
|
entries_name: Literal["transactions"],
|
|
@@ -371,7 +375,7 @@ def insert_nested_entries(
|
|
|
371
375
|
@overload
|
|
372
376
|
def insert_nested_entries(
|
|
373
377
|
cur: sqlite3.Cursor,
|
|
374
|
-
|
|
378
|
+
plan_id: str,
|
|
375
379
|
entries: list[dict[str, Any]],
|
|
376
380
|
desc: Literal["Scheduled Transactions"],
|
|
377
381
|
entries_name: Literal["scheduled_transactions"],
|
|
@@ -382,7 +386,7 @@ def insert_nested_entries(
|
|
|
382
386
|
|
|
383
387
|
def insert_nested_entries(
|
|
384
388
|
cur: sqlite3.Cursor,
|
|
385
|
-
|
|
389
|
+
plan_id: str,
|
|
386
390
|
entries: list[dict[str, Any]],
|
|
387
391
|
desc: (
|
|
388
392
|
Literal["Accounts"]
|
|
@@ -419,24 +423,24 @@ def insert_nested_entries(
|
|
|
419
423
|
insert_entry(
|
|
420
424
|
cur,
|
|
421
425
|
entries_name,
|
|
422
|
-
|
|
426
|
+
plan_id,
|
|
423
427
|
{k: v for k, v in entry.items() if k != subentries_name},
|
|
424
428
|
)
|
|
425
429
|
pbar.update()
|
|
426
430
|
|
|
427
431
|
for subentry in entry[subentries_name]:
|
|
428
|
-
insert_entry(cur, subentries_table_name,
|
|
432
|
+
insert_entry(cur, subentries_table_name, plan_id, subentry)
|
|
429
433
|
pbar.update()
|
|
430
434
|
|
|
431
435
|
|
|
432
436
|
def insert_entry(
|
|
433
437
|
cur: sqlite3.Cursor,
|
|
434
438
|
table: _EntryTable,
|
|
435
|
-
|
|
439
|
+
plan_id: str,
|
|
436
440
|
entry: dict[str, Any],
|
|
437
441
|
) -> None:
|
|
438
442
|
ekeys, evalues = zip(*entry.items(), strict=True)
|
|
439
|
-
keys, values = ekeys + ("
|
|
443
|
+
keys, values = ekeys + ("plan_id",), evalues + (plan_id,)
|
|
440
444
|
|
|
441
445
|
cur.execute(
|
|
442
446
|
f"INSERT OR REPLACE INTO {table} ({', '.join(keys)}) VALUES ({', '.join('?' * len(values))})",
|
|
@@ -453,12 +457,12 @@ def jobs(
|
|
|
453
457
|
| Literal["transactions"]
|
|
454
458
|
| Literal["scheduled_transactions"]
|
|
455
459
|
),
|
|
456
|
-
|
|
460
|
+
plan_ids: list[str],
|
|
457
461
|
lkos: dict[str, int],
|
|
458
462
|
) -> list[Awaitable[dict[str, Any]]]:
|
|
459
463
|
return [
|
|
460
|
-
yc(f"
|
|
461
|
-
for
|
|
464
|
+
yc(f"plans/{plan_id}/{endpoint}", last_knowledge_of_server=lkos.get(plan_id))
|
|
465
|
+
for plan_id in plan_ids
|
|
462
466
|
]
|
|
463
467
|
|
|
464
468
|
|