sqlite-export-for-ynab 1.6.2__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.2/sqlite_export_for_ynab.egg-info → sqlite_export_for_ynab-2.0.0}/PKG-INFO +31 -30
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/README.md +28 -27
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/setup.cfg +3 -3
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab/_main.py +66 -62
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab/ddl/create-relations.sql +30 -28
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab/ddl/drop-relations.sql +1 -1
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0/sqlite_export_for_ynab.egg-info}/PKG-INFO +31 -30
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/testing/fixtures.py +10 -10
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/tests/_main_test.py +57 -57
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/LICENSE +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/pyproject.toml +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/setup.py +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab/__init__.py +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab/__main__.py +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab/ddl/__init__.py +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab/py.typed +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab.egg-info/SOURCES.txt +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab.egg-info/dependency_links.txt +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab.egg-info/entry_points.txt +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab.egg-info/requires.txt +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/sqlite_export_for_ynab.egg-info/top_level.txt +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.0.0}/testing/__init__.py +0 -0
- {sqlite_export_for_ynab-1.6.2 → 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,43 +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'
|
|
158
159
|
AND NOT p.deleted
|
|
159
160
|
|
|
160
161
|
UNION ALL
|
|
161
162
|
|
|
162
163
|
SELECT
|
|
163
|
-
|
|
164
|
-
, name
|
|
164
|
+
plan_id
|
|
165
|
+
, "name"
|
|
165
166
|
FROM payees
|
|
166
167
|
WHERE NOT deleted
|
|
167
|
-
GROUP BY
|
|
168
|
+
GROUP BY plan_id, "name"
|
|
168
169
|
HAVING COUNT(*) > 1
|
|
169
170
|
|
|
170
171
|
) AS dupes
|
|
171
|
-
INNER JOIN
|
|
172
|
-
ON dupes.
|
|
173
|
-
ORDER BY
|
|
172
|
+
INNER JOIN plans AS pl
|
|
173
|
+
ON dupes.plan_id = pl.id
|
|
174
|
+
ORDER BY "plan", payee
|
|
174
175
|
;
|
|
175
176
|
```
|
|
176
177
|
|
|
@@ -178,11 +179,11 @@ To count the spend for a category (ex: "Apps") between this month and the next 1
|
|
|
178
179
|
|
|
179
180
|
```sql
|
|
180
181
|
SELECT
|
|
181
|
-
|
|
182
|
+
plan_id
|
|
182
183
|
, SUM(amount_major) AS amount_major
|
|
183
184
|
FROM (
|
|
184
185
|
SELECT
|
|
185
|
-
|
|
186
|
+
plan_id
|
|
186
187
|
, amount_major
|
|
187
188
|
FROM flat_transactions
|
|
188
189
|
WHERE
|
|
@@ -190,7 +191,7 @@ FROM (
|
|
|
190
191
|
AND SUBSTR(`date`, 1, 7) = SUBSTR(DATE(), 1, 7)
|
|
191
192
|
UNION ALL
|
|
192
193
|
SELECT
|
|
193
|
-
|
|
194
|
+
plan_id
|
|
194
195
|
, amount_major * (
|
|
195
196
|
CASE
|
|
196
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,43 +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'
|
|
138
139
|
AND NOT p.deleted
|
|
139
140
|
|
|
140
141
|
UNION ALL
|
|
141
142
|
|
|
142
143
|
SELECT
|
|
143
|
-
|
|
144
|
-
, name
|
|
144
|
+
plan_id
|
|
145
|
+
, "name"
|
|
145
146
|
FROM payees
|
|
146
147
|
WHERE NOT deleted
|
|
147
|
-
GROUP BY
|
|
148
|
+
GROUP BY plan_id, "name"
|
|
148
149
|
HAVING COUNT(*) > 1
|
|
149
150
|
|
|
150
151
|
) AS dupes
|
|
151
|
-
INNER JOIN
|
|
152
|
-
ON dupes.
|
|
153
|
-
ORDER BY
|
|
152
|
+
INNER JOIN plans AS pl
|
|
153
|
+
ON dupes.plan_id = pl.id
|
|
154
|
+
ORDER BY "plan", payee
|
|
154
155
|
;
|
|
155
156
|
```
|
|
156
157
|
|
|
@@ -158,11 +159,11 @@ To count the spend for a category (ex: "Apps") between this month and the next 1
|
|
|
158
159
|
|
|
159
160
|
```sql
|
|
160
161
|
SELECT
|
|
161
|
-
|
|
162
|
+
plan_id
|
|
162
163
|
, SUM(amount_major) AS amount_major
|
|
163
164
|
FROM (
|
|
164
165
|
SELECT
|
|
165
|
-
|
|
166
|
+
plan_id
|
|
166
167
|
, amount_major
|
|
167
168
|
FROM flat_transactions
|
|
168
169
|
WHERE
|
|
@@ -170,7 +171,7 @@ FROM (
|
|
|
170
171
|
AND SUBSTR(`date`, 1, 7) = SUBSTR(DATE(), 1, 7)
|
|
171
172
|
UNION ALL
|
|
172
173
|
SELECT
|
|
173
|
-
|
|
174
|
+
plan_id
|
|
174
175
|
, amount_major * (
|
|
175
176
|
CASE
|
|
176
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.2 → 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
|
|