sqlite-export-for-ynab 0.0.1__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-0.0.1/PKG-INFO +18 -0
- sqlite_export_for_ynab-0.0.1/pyproject.toml +20 -0
- sqlite_export_for_ynab-0.0.1/setup.cfg +40 -0
- sqlite_export_for_ynab-0.0.1/setup.py +5 -0
- sqlite_export_for_ynab-0.0.1/sqlite_export_for_ynab/__init__.py +6 -0
- sqlite_export_for_ynab-0.0.1/sqlite_export_for_ynab/__main__.py +6 -0
- sqlite_export_for_ynab-0.0.1/sqlite_export_for_ynab/_main.py +328 -0
- sqlite_export_for_ynab-0.0.1/sqlite_export_for_ynab/ddl/__init__.py +0 -0
- sqlite_export_for_ynab-0.0.1/sqlite_export_for_ynab/ddl/create-tables.sqlite +98 -0
- sqlite_export_for_ynab-0.0.1/sqlite_export_for_ynab/ddl/drop-tables.sqlite +17 -0
- sqlite_export_for_ynab-0.0.1/sqlite_export_for_ynab.egg-info/PKG-INFO +18 -0
- sqlite_export_for_ynab-0.0.1/sqlite_export_for_ynab.egg-info/SOURCES.txt +15 -0
- sqlite_export_for_ynab-0.0.1/sqlite_export_for_ynab.egg-info/dependency_links.txt +1 -0
- sqlite_export_for_ynab-0.0.1/sqlite_export_for_ynab.egg-info/entry_points.txt +2 -0
- sqlite_export_for_ynab-0.0.1/sqlite_export_for_ynab.egg-info/requires.txt +2 -0
- sqlite_export_for_ynab-0.0.1/sqlite_export_for_ynab.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sqlite_export_for_ynab
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: SQLite Export for YNAB - Export YNAB Budget data to SQLite
|
|
5
|
+
Home-page: https://github.com/mxr/sqlite-export-for-ynab
|
|
6
|
+
Author: Max R
|
|
7
|
+
Author-email: maxr@outlook.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: ynab,sqlite,sql,budget,cli
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
14
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: aiohttp
|
|
18
|
+
Requires-Dist: tqdm
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[tool.mypy]
|
|
2
|
+
check_untyped_defs = true
|
|
3
|
+
disallow_any_generics = true
|
|
4
|
+
disallow_incomplete_defs = true
|
|
5
|
+
disallow_untyped_defs = true
|
|
6
|
+
warn_redundant_casts = true
|
|
7
|
+
warn_unused_ignores = true
|
|
8
|
+
|
|
9
|
+
[tool.ruff]
|
|
10
|
+
target-version = "py312"
|
|
11
|
+
|
|
12
|
+
[tool.ruff.lint]
|
|
13
|
+
extend-select = [
|
|
14
|
+
"UP", # see pyupgrade
|
|
15
|
+
"B", # see flake8-bugbear
|
|
16
|
+
"A", # see flake8-builtins
|
|
17
|
+
"C4", # see flake8-comprehension
|
|
18
|
+
"SIM", # see flake8-simplify
|
|
19
|
+
"TCH" # see flake8-type-checking
|
|
20
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = sqlite_export_for_ynab
|
|
3
|
+
version = 0.0.1
|
|
4
|
+
description = SQLite Export for YNAB - Export YNAB Budget data to SQLite
|
|
5
|
+
long_description = file: README.md
|
|
6
|
+
long_description_content_type = text/markdown
|
|
7
|
+
url = https://github.com/mxr/sqlite-export-for-ynab
|
|
8
|
+
author = Max R
|
|
9
|
+
author_email = maxr@outlook.com
|
|
10
|
+
license = MIT
|
|
11
|
+
license_files = LICENSE
|
|
12
|
+
classifiers =
|
|
13
|
+
License :: OSI Approved :: MIT License
|
|
14
|
+
Programming Language :: Python :: 3
|
|
15
|
+
Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Programming Language :: Python :: Implementation :: CPython
|
|
17
|
+
Programming Language :: Python :: Implementation :: PyPy
|
|
18
|
+
keywords = ynab, sqlite, sql, budget, cli
|
|
19
|
+
|
|
20
|
+
[options]
|
|
21
|
+
packages = find:
|
|
22
|
+
install_requires =
|
|
23
|
+
aiohttp
|
|
24
|
+
tqdm
|
|
25
|
+
python_requires = >=3.12
|
|
26
|
+
|
|
27
|
+
[options.entry_points]
|
|
28
|
+
console_scripts =
|
|
29
|
+
sqlite-export-for-ynab = sqlite_export_for_ynab._main:main
|
|
30
|
+
|
|
31
|
+
[options.package_data]
|
|
32
|
+
* = *.sqlite
|
|
33
|
+
|
|
34
|
+
[bdist_wheel]
|
|
35
|
+
universal = True
|
|
36
|
+
|
|
37
|
+
[egg_info]
|
|
38
|
+
tag_build =
|
|
39
|
+
tag_date = 0
|
|
40
|
+
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sqlite3
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from importlib import resources
|
|
10
|
+
from typing import Any
|
|
11
|
+
from typing import ClassVar
|
|
12
|
+
from typing import Literal
|
|
13
|
+
from typing import overload
|
|
14
|
+
from typing import Protocol
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
from urllib.parse import urlencode
|
|
17
|
+
from urllib.parse import urljoin
|
|
18
|
+
from urllib.parse import urlunparse
|
|
19
|
+
|
|
20
|
+
import aiohttp
|
|
21
|
+
from tqdm import tqdm
|
|
22
|
+
|
|
23
|
+
from sqlite_export_for_ynab import ddl
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Awaitable
|
|
27
|
+
from typing import Never
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_EntryTable = (
|
|
31
|
+
Literal["payees"]
|
|
32
|
+
| Literal["transactions"]
|
|
33
|
+
| Literal["subtransactions"]
|
|
34
|
+
| Literal["category_groups"]
|
|
35
|
+
| Literal["categories"]
|
|
36
|
+
)
|
|
37
|
+
_ALL_TABLES = frozenset(
|
|
38
|
+
("budgets",) + tuple(lit.__args__[0] for lit in _EntryTable.__args__)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
_ENV_TOKEN = "YNAB_PERSONAL_ACCESS_TOKEN"
|
|
42
|
+
|
|
43
|
+
DEFAULT_DB = "sqlite-export-for-ynab.db"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def async_main() -> int:
|
|
47
|
+
parser = argparse.ArgumentParser()
|
|
48
|
+
parser.add_argument("--db", help="The SQLite database file.", default=DEFAULT_DB)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--full-refresh",
|
|
51
|
+
action="store_true",
|
|
52
|
+
help="Drop all tables and fetch the budget.",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
args = parser.parse_args()
|
|
56
|
+
db: str = args.db
|
|
57
|
+
full_refresh: bool = args.full_refresh
|
|
58
|
+
|
|
59
|
+
token = os.environ.get(_ENV_TOKEN)
|
|
60
|
+
if not token:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Must set YNAB access token as {_ENV_TOKEN!r} "
|
|
63
|
+
"environment variable. See "
|
|
64
|
+
"https://api.ynab.com/#personal-access-tokens"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
await sync(db, full_refresh, token)
|
|
68
|
+
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def sync(db: str, full_refresh: bool, token: str) -> None:
|
|
73
|
+
async with aiohttp.ClientSession() as session:
|
|
74
|
+
budgets = (await YnabClient(token, session)("budgets"))["budgets"]
|
|
75
|
+
|
|
76
|
+
budget_ids = [b["id"] for b in budgets]
|
|
77
|
+
|
|
78
|
+
with sqlite3.connect(db) as con:
|
|
79
|
+
con.row_factory = lambda c, row: dict(
|
|
80
|
+
zip([name for name, *_ in c.description], row, strict=True)
|
|
81
|
+
)
|
|
82
|
+
cur = con.cursor()
|
|
83
|
+
|
|
84
|
+
if full_refresh:
|
|
85
|
+
print("Dropping tables...")
|
|
86
|
+
cur.executescript(contents("drop-tables.sqlite"))
|
|
87
|
+
con.commit()
|
|
88
|
+
print("Done")
|
|
89
|
+
|
|
90
|
+
tables = get_tables(cur)
|
|
91
|
+
if tables != _ALL_TABLES:
|
|
92
|
+
print("Recreating tables...")
|
|
93
|
+
cur.executescript(contents("create-tables.sqlite"))
|
|
94
|
+
con.commit()
|
|
95
|
+
print("Done")
|
|
96
|
+
|
|
97
|
+
print("Fetching budget data...")
|
|
98
|
+
lkos = get_last_knowledge_of_server(cur)
|
|
99
|
+
async with aiohttp.ClientSession() as session:
|
|
100
|
+
with tqdm(desc="Budget Data", total=len(budgets) * 3) as pbar:
|
|
101
|
+
yc = ProgressYnabClient(
|
|
102
|
+
YnabClient(os.environ["YNAB_PERSONAL_ACCESS_TOKEN"], session), pbar
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
txn_jobs = jobs(yc, "transactions", budget_ids, lkos)
|
|
106
|
+
payee_jobs = jobs(yc, "payees", budget_ids, lkos)
|
|
107
|
+
cat_jobs = jobs(yc, "categories", budget_ids, lkos)
|
|
108
|
+
|
|
109
|
+
data = await asyncio.gather(*txn_jobs, *payee_jobs, *cat_jobs)
|
|
110
|
+
|
|
111
|
+
all_txn_data = data[: len(txn_jobs)]
|
|
112
|
+
all_payee_data = data[len(txn_jobs) : len(txn_jobs) + len(payee_jobs)]
|
|
113
|
+
all_cat_data = data[len(txn_jobs) + len(payee_jobs) :]
|
|
114
|
+
|
|
115
|
+
new_lkos = {
|
|
116
|
+
bid: t["server_knowledge"]
|
|
117
|
+
for bid, t in zip(budget_ids, all_txn_data, strict=True)
|
|
118
|
+
}
|
|
119
|
+
print("Done")
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
not any(t["transactions"] for t in all_txn_data)
|
|
123
|
+
and not any(p["payees"] for p in all_payee_data)
|
|
124
|
+
and not any(c["category_groups"] for c in all_cat_data)
|
|
125
|
+
):
|
|
126
|
+
print("No data fetched")
|
|
127
|
+
else:
|
|
128
|
+
print("Inserting budget data...")
|
|
129
|
+
insert_budgets(cur, budgets, new_lkos)
|
|
130
|
+
for bid, txn_data in zip(budget_ids, all_txn_data, strict=True):
|
|
131
|
+
insert_transactions(cur, bid, txn_data["transactions"])
|
|
132
|
+
for bid, payee_data in zip(budget_ids, all_payee_data, strict=True):
|
|
133
|
+
insert_payees(cur, bid, payee_data["payees"])
|
|
134
|
+
for bid, cat_data in zip(budget_ids, all_cat_data, strict=True):
|
|
135
|
+
insert_category_groups(cur, bid, cat_data["category_groups"])
|
|
136
|
+
print("Done")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def contents(filename: str) -> str:
|
|
140
|
+
return (resources.files(ddl) / filename).read_text()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def jobs(
|
|
144
|
+
yc: SupportsYnabClient,
|
|
145
|
+
endpoint: Literal["transactions"] | Literal["categories"] | Literal["payees"],
|
|
146
|
+
budget_ids: list[str],
|
|
147
|
+
lkos: dict[str, int],
|
|
148
|
+
) -> list[Awaitable[dict[str, Any]]]:
|
|
149
|
+
return [
|
|
150
|
+
yc(f"budgets/{bid}/{endpoint}", last_knowledge_of_server=lkos.get(bid))
|
|
151
|
+
for bid in budget_ids
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_tables(cur: sqlite3.Cursor) -> set[str]:
|
|
156
|
+
return {
|
|
157
|
+
t["name"]
|
|
158
|
+
for t in cur.execute(
|
|
159
|
+
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
160
|
+
).fetchall()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_last_knowledge_of_server(cur: sqlite3.Cursor) -> dict[str, int]:
|
|
165
|
+
return {
|
|
166
|
+
r["id"]: r["last_knowledge_of_server"]
|
|
167
|
+
for r in cur.execute(
|
|
168
|
+
"SELECT id, last_knowledge_of_server FROM budgets",
|
|
169
|
+
).fetchall()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def insert_budgets(
|
|
174
|
+
cur: sqlite3.Cursor, budgets: list[dict[str, Any]], lkos: dict[str, int]
|
|
175
|
+
) -> None:
|
|
176
|
+
cur.executemany(
|
|
177
|
+
"INSERT OR REPLACE INTO budgets (id, name, last_knowledge_of_server) VALUES (?, ?, ?)",
|
|
178
|
+
((bid := b["id"], b["name"], lkos[bid]) for b in budgets),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def insert_transactions(
|
|
183
|
+
cur: sqlite3.Cursor, budget_id: str, transactions: list[dict[str, Any]]
|
|
184
|
+
) -> None:
|
|
185
|
+
return insert_nested_entries(
|
|
186
|
+
cur, budget_id, transactions, "Transactions", "transactions", "subtransactions"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@overload
|
|
191
|
+
def insert_nested_entries(
|
|
192
|
+
cur: sqlite3.Cursor,
|
|
193
|
+
budget_id: str,
|
|
194
|
+
entries: list[dict[str, Any]],
|
|
195
|
+
desc: Literal["Transactions"],
|
|
196
|
+
entries_name: Literal["transactions"],
|
|
197
|
+
subentries_name: Literal["subtransactions"],
|
|
198
|
+
) -> None: ...
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@overload
|
|
202
|
+
def insert_nested_entries(
|
|
203
|
+
cur: sqlite3.Cursor,
|
|
204
|
+
budget_id: str,
|
|
205
|
+
entries: list[dict[str, Any]],
|
|
206
|
+
desc: Literal["Categories"],
|
|
207
|
+
entries_name: Literal["category_groups"],
|
|
208
|
+
subentries_name: Literal["categories"],
|
|
209
|
+
) -> None: ...
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def insert_nested_entries(
|
|
213
|
+
cur: sqlite3.Cursor,
|
|
214
|
+
budget_id: str,
|
|
215
|
+
entries: list[dict[str, Any]],
|
|
216
|
+
desc: Literal["Transactions"] | Literal["Categories"],
|
|
217
|
+
entries_name: Literal["transactions"] | Literal["category_groups"],
|
|
218
|
+
subentries_name: Literal["subtransactions"] | Literal["categories"],
|
|
219
|
+
) -> None:
|
|
220
|
+
if not entries:
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
with tqdm(
|
|
224
|
+
total=sum(1 + len(e[subentries_name]) for e in entries),
|
|
225
|
+
desc=desc,
|
|
226
|
+
) as pbar:
|
|
227
|
+
for entry in entries:
|
|
228
|
+
subentries = entry.pop(subentries_name, [])
|
|
229
|
+
insert_entry(cur, entries_name, budget_id, entry)
|
|
230
|
+
pbar.update()
|
|
231
|
+
|
|
232
|
+
for subentry in subentries:
|
|
233
|
+
insert_entry(cur, subentries_name, budget_id, subentry)
|
|
234
|
+
pbar.update()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def insert_payees(
|
|
238
|
+
cur: sqlite3.Cursor, budget_id: str, payees: list[dict[str, Any]]
|
|
239
|
+
) -> None:
|
|
240
|
+
if not payees:
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
for payee in tqdm(payees, desc="Payees"):
|
|
244
|
+
insert_entry(cur, "payees", budget_id, payee)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def insert_category_groups(
|
|
248
|
+
cur: sqlite3.Cursor, budget_id: str, category_groups: list[dict[str, Any]]
|
|
249
|
+
) -> None:
|
|
250
|
+
return insert_nested_entries(
|
|
251
|
+
cur, budget_id, category_groups, "Categories", "category_groups", "categories"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def insert_entry(
|
|
256
|
+
cur: sqlite3.Cursor,
|
|
257
|
+
table: _EntryTable,
|
|
258
|
+
budget_id: str,
|
|
259
|
+
entry: dict[str, Any],
|
|
260
|
+
) -> None:
|
|
261
|
+
ekeys, evalues = zip(*entry.items(), strict=True)
|
|
262
|
+
keys, values = ekeys + ("budget_id",), evalues + (budget_id,)
|
|
263
|
+
|
|
264
|
+
cur.execute(
|
|
265
|
+
f'INSERT OR REPLACE INTO {table} ({", ".join(keys)}) VALUES ({", ".join("?" * len(values))})',
|
|
266
|
+
values,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class SupportsYnabClient(Protocol):
|
|
271
|
+
async def __call__(
|
|
272
|
+
self, path: str, last_knowledge_of_server: int | None = None
|
|
273
|
+
) -> dict[str, Any]: ...
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@dataclass
|
|
277
|
+
class ProgressYnabClient:
|
|
278
|
+
yc: YnabClient
|
|
279
|
+
pbar: tqdm[Never]
|
|
280
|
+
|
|
281
|
+
async def __call__(
|
|
282
|
+
self, path: str, last_knowledge_of_server: int | None = None
|
|
283
|
+
) -> dict[str, Any]:
|
|
284
|
+
try:
|
|
285
|
+
return await self.yc(path, last_knowledge_of_server)
|
|
286
|
+
finally:
|
|
287
|
+
self.pbar.update()
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@dataclass
|
|
291
|
+
class YnabClient:
|
|
292
|
+
BASE_SCHEME: ClassVar[str] = "https"
|
|
293
|
+
BASE_NETLOC: ClassVar[str] = "api.ynab.com"
|
|
294
|
+
BASE_PATH: ClassVar[str] = "v1/"
|
|
295
|
+
|
|
296
|
+
token: str
|
|
297
|
+
session: aiohttp.ClientSession
|
|
298
|
+
|
|
299
|
+
async def __call__(
|
|
300
|
+
self, path: str, last_knowledge_of_server: int | None = None
|
|
301
|
+
) -> dict[str, Any]:
|
|
302
|
+
headers = {
|
|
303
|
+
"Authorization": f"Bearer {self.token}",
|
|
304
|
+
"Content-Type": "application/json",
|
|
305
|
+
}
|
|
306
|
+
url = urlunparse(
|
|
307
|
+
(
|
|
308
|
+
self.BASE_SCHEME,
|
|
309
|
+
self.BASE_NETLOC,
|
|
310
|
+
urljoin(self.BASE_PATH, path),
|
|
311
|
+
"",
|
|
312
|
+
urlencode(
|
|
313
|
+
{"last_knowledge_of_server": last_knowledge_of_server}
|
|
314
|
+
if last_knowledge_of_server
|
|
315
|
+
else {}
|
|
316
|
+
),
|
|
317
|
+
"",
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
async with self.session.get(url, headers=headers) as resp:
|
|
322
|
+
body = await resp.text()
|
|
323
|
+
|
|
324
|
+
return json.loads(body)["data"]
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def main() -> int:
|
|
328
|
+
return asyncio.run(async_main())
|
|
File without changes
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS budgets (id TEXT primary key, name TEXT, last_knowledge_of_server INT)
|
|
2
|
+
;
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS payees (
|
|
5
|
+
id TEXT primary key,
|
|
6
|
+
budget_id TEXT,
|
|
7
|
+
name TEXT,
|
|
8
|
+
transfer_account_id TEXT,
|
|
9
|
+
deleted BOOLEAN,
|
|
10
|
+
foreign key (budget_id) references budgets (id)
|
|
11
|
+
)
|
|
12
|
+
;
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS category_groups (
|
|
15
|
+
id TEXT primary key,
|
|
16
|
+
budget_id TEXT,
|
|
17
|
+
name TEXT,
|
|
18
|
+
hidden BOOLEAN,
|
|
19
|
+
deleted BOOLEAN,
|
|
20
|
+
foreign key (budget_id) references budgets (id)
|
|
21
|
+
)
|
|
22
|
+
;
|
|
23
|
+
|
|
24
|
+
CREATE TABLE IF NOT EXISTS categories (
|
|
25
|
+
id TEXT primary key,
|
|
26
|
+
budget_id TEXT,
|
|
27
|
+
category_group_id TEXT,
|
|
28
|
+
category_group_name TEXT,
|
|
29
|
+
name TEXT,
|
|
30
|
+
hidden BOOLEAN,
|
|
31
|
+
original_category_group_id TEXT,
|
|
32
|
+
note TEXT,
|
|
33
|
+
budgeted INT,
|
|
34
|
+
activity INT,
|
|
35
|
+
balance INT,
|
|
36
|
+
goal_type TEXT,
|
|
37
|
+
goal_needs_whole_amount BOOLEAN,
|
|
38
|
+
goal_day INT,
|
|
39
|
+
goal_cadence INT,
|
|
40
|
+
goal_cadence_frequency INT,
|
|
41
|
+
goal_creation_month text,
|
|
42
|
+
goal_target INT,
|
|
43
|
+
goal_target_month text,
|
|
44
|
+
goal_percentage_complete INT,
|
|
45
|
+
goal_months_to_budget INT,
|
|
46
|
+
goal_under_funded INT,
|
|
47
|
+
goal_overall_funded INT,
|
|
48
|
+
goal_overall_left INT,
|
|
49
|
+
deleted BOOLEAN,
|
|
50
|
+
foreign key (budget_id) references budgets (id),
|
|
51
|
+
foreign key (category_group_id) references category_groups (id)
|
|
52
|
+
)
|
|
53
|
+
;
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS transactions (
|
|
56
|
+
id TEXT primary key,
|
|
57
|
+
budget_id TEXT,
|
|
58
|
+
account_id TEXT,
|
|
59
|
+
account_name TEXT,
|
|
60
|
+
amount INT,
|
|
61
|
+
approved BOOLEAN,
|
|
62
|
+
category_id TEXT,
|
|
63
|
+
category_name TEXT,
|
|
64
|
+
cleared TEXT,
|
|
65
|
+
DATE TEXT,
|
|
66
|
+
debt_transaction_type TEXT,
|
|
67
|
+
deleted BOOLEAN,
|
|
68
|
+
flag_color TEXT,
|
|
69
|
+
flag_name TEXT,
|
|
70
|
+
import_id TEXT,
|
|
71
|
+
import_payee_name TEXT,
|
|
72
|
+
import_payee_name_original TEXT,
|
|
73
|
+
matched_transaction_id TEXT,
|
|
74
|
+
memo TEXT,
|
|
75
|
+
payee_id TEXT,
|
|
76
|
+
payee_name TEXT,
|
|
77
|
+
transfer_account_id TEXT,
|
|
78
|
+
transfer_transaction_id TEXT,
|
|
79
|
+
foreign key (budget_id) references budgets (id)
|
|
80
|
+
)
|
|
81
|
+
;
|
|
82
|
+
|
|
83
|
+
CREATE TABLE IF NOT EXISTS subtransactions (
|
|
84
|
+
id TEXT primary key,
|
|
85
|
+
budget_id TEXT,
|
|
86
|
+
amount INT,
|
|
87
|
+
category_id TEXT,
|
|
88
|
+
category_name TEXT,
|
|
89
|
+
deleted BOOLEAN,
|
|
90
|
+
memo TEXT,
|
|
91
|
+
payee_id TEXT,
|
|
92
|
+
payee_name TEXT,
|
|
93
|
+
transaction_id TEXT,
|
|
94
|
+
transfer_account_id TEXT,
|
|
95
|
+
transfer_transaction_id TEXT,
|
|
96
|
+
foreign key (budget_id) references budget (id),
|
|
97
|
+
foreign key (transaction_id) references transaction_id (id)
|
|
98
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
DROP TABLE IF EXISTS budgets
|
|
2
|
+
;
|
|
3
|
+
|
|
4
|
+
DROP TABLE IF EXISTS payees
|
|
5
|
+
;
|
|
6
|
+
|
|
7
|
+
DROP TABLE IF EXISTS category_groups
|
|
8
|
+
;
|
|
9
|
+
|
|
10
|
+
DROP TABLE IF EXISTS categories
|
|
11
|
+
;
|
|
12
|
+
|
|
13
|
+
DROP TABLE IF EXISTS transactions
|
|
14
|
+
;
|
|
15
|
+
|
|
16
|
+
DROP TABLE IF EXISTS subtransactions
|
|
17
|
+
;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sqlite_export_for_ynab
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: SQLite Export for YNAB - Export YNAB Budget data to SQLite
|
|
5
|
+
Home-page: https://github.com/mxr/sqlite-export-for-ynab
|
|
6
|
+
Author: Max R
|
|
7
|
+
Author-email: maxr@outlook.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: ynab,sqlite,sql,budget,cli
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
14
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: aiohttp
|
|
18
|
+
Requires-Dist: tqdm
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
setup.cfg
|
|
3
|
+
setup.py
|
|
4
|
+
sqlite_export_for_ynab/__init__.py
|
|
5
|
+
sqlite_export_for_ynab/__main__.py
|
|
6
|
+
sqlite_export_for_ynab/_main.py
|
|
7
|
+
sqlite_export_for_ynab.egg-info/PKG-INFO
|
|
8
|
+
sqlite_export_for_ynab.egg-info/SOURCES.txt
|
|
9
|
+
sqlite_export_for_ynab.egg-info/dependency_links.txt
|
|
10
|
+
sqlite_export_for_ynab.egg-info/entry_points.txt
|
|
11
|
+
sqlite_export_for_ynab.egg-info/requires.txt
|
|
12
|
+
sqlite_export_for_ynab.egg-info/top_level.txt
|
|
13
|
+
sqlite_export_for_ynab/ddl/__init__.py
|
|
14
|
+
sqlite_export_for_ynab/ddl/create-tables.sqlite
|
|
15
|
+
sqlite_export_for_ynab/ddl/drop-tables.sqlite
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sqlite_export_for_ynab
|