Skip to content

Commit ab7891c

Browse files
authored
Add sync toggle (#47)
#### Problem Some tools always refresh the sqlite-export-for-ynab database before running. That makes it harder to intentionally operate against an existing local export. #### Solution Add a default-on `--sync` boolean option to the sqlite-backed tools. Passing `--no-sync` skips the refresh and uses the existing database contents.
1 parent 5d82850 commit ab7891c

12 files changed

Lines changed: 131 additions & 12 deletions

File tree

manager_for_ynab/add_transaction/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55
It works interactively by autocompleting payees, categories etc already in your plan.
66

77
Use `--for-real` to create the transaction. Without it, the command only previews the transaction that would be sent.
8+
9+
By default, the command refreshes the local sqlite-export-for-ynab database before reading from it. Pass `--no-sync` to use the existing database contents without syncing.

manager_for_ynab/add_transaction/__init__.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ def build_parser() -> argparse.ArgumentParser:
9797
action="store_true",
9898
help="Whether to refresh the SQLite DB from scratch.",
9999
)
100+
parser.add_argument(
101+
"--sync",
102+
action=argparse.BooleanOptionalAction,
103+
default=True,
104+
help="Refresh the SQLite DB before using it.",
105+
)
100106
return parser
101107

102108

@@ -120,6 +126,7 @@ async def run(
120126
quiet=args.quiet,
121127
db=args.sqlite_export_for_ynab_db,
122128
full_refresh=args.sqlite_export_for_ynab_full_refresh,
129+
should_sync=args.sync,
123130
token_override=token_override,
124131
)
125132

@@ -137,13 +144,15 @@ async def add_transaction(
137144
quiet: bool,
138145
db: Path,
139146
full_refresh: bool,
147+
should_sync: bool = True,
140148
token_override: str | None,
141149
) -> int:
142150
token = resolve_token(token_override)
143151

144-
_print("** Refreshing SQLite DB **", quiet=quiet)
145-
await sync(token, db, full_refresh, quiet=quiet)
146-
_print("** Done **", quiet=quiet)
152+
if should_sync:
153+
_print("** Refreshing SQLite DB **", quiet=quiet)
154+
await sync(token, db, full_refresh, quiet=quiet)
155+
_print("** Done **", quiet=quiet)
147156

148157
try:
149158
async with aiosqlite.connect(db) as con:

manager_for_ynab/auto_approve/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ Apply the approval updates:
2323
```console
2424
$ manager-for-ynab auto-approve --for-real
2525
```
26+
27+
By default, the command refreshes the local sqlite-export-for-ynab database before reading from it. Pass `--no-sync` to use the existing database contents without syncing.

manager_for_ynab/auto_approve/__init__.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,21 @@ async def run(
5252
"--sqlite-export-for-ynab-db", type=Path, default=default_db_path()
5353
)
5454
parser.add_argument("--sqlite-export-for-ynab-full-refresh", action="store_true")
55+
parser.add_argument("--sync", action=argparse.BooleanOptionalAction, default=True)
5556
parser.add_argument("--for-real", action="store_true")
5657
parser.add_argument("--quiet", action="store_true")
5758

5859
args = parser.parse_args(argv)
5960
db: Path = args.sqlite_export_for_ynab_db
6061
full_refresh: bool = args.sqlite_export_for_ynab_full_refresh
62+
should_sync: bool = args.sync
6163
for_real: bool = args.for_real
6264
quiet: bool = args.quiet
6365

6466
result = await auto_approve(
6567
db=db,
6668
full_refresh=full_refresh,
69+
should_sync=should_sync,
6770
for_real=for_real,
6871
token_override=token_override,
6972
quiet=quiet,
@@ -79,15 +82,17 @@ async def auto_approve(
7982
*,
8083
db: Path,
8184
full_refresh: bool,
85+
should_sync: bool = True,
8286
for_real: bool,
8387
token_override: str | None,
8488
quiet: bool,
8589
) -> AutoApproveResult:
8690
token = resolve_token(token_override)
8791

88-
_print("** Refreshing SQLite DB **", quiet=quiet)
89-
await sync(token, db, full_refresh, quiet=quiet)
90-
_print("** Done **", quiet=quiet)
92+
if should_sync:
93+
_print("** Refreshing SQLite DB **", quiet=quiet)
94+
await sync(token, db, full_refresh, quiet=quiet)
95+
_print("** Done **", quiet=quiet)
9196

9297
async with aiosqlite.connect(db) as con:
9398
con.row_factory = aiosqlite.Row

manager_for_ynab/pending_income/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ Exclude already matched transactions (to avoid changing the date once YNAB picks
2929
```console
3030
$ manager-for-ynab pending-income --skip-matched
3131
```
32+
33+
By default, the command refreshes the local sqlite-export-for-ynab database before reading from it. Pass `--no-sync` to use the existing database contents without syncing.

manager_for_ynab/pending_income/__init__.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,23 @@ async def run(
5252
"--sqlite-export-for-ynab-db", type=Path, default=default_db_path()
5353
)
5454
parser.add_argument("--sqlite-export-for-ynab-full-refresh", action="store_true")
55+
parser.add_argument("--sync", action=argparse.BooleanOptionalAction, default=True)
5556
parser.add_argument("--for-real", action="store_true")
5657
parser.add_argument("--skip-matched", action="store_true")
5758
parser.add_argument("--quiet", action="store_true")
5859

5960
args = parser.parse_args(argv)
6061
db: Path = args.sqlite_export_for_ynab_db
6162
full_refresh: bool = args.sqlite_export_for_ynab_full_refresh
63+
should_sync: bool = args.sync
6264
for_real: bool = args.for_real
6365
skip_matched: bool = args.skip_matched
6466
quiet: bool = args.quiet
6567

6668
result = await pending_income(
6769
db=db,
6870
full_refresh=full_refresh,
71+
should_sync=should_sync,
6972
for_real=for_real,
7073
skip_matched=skip_matched,
7174
token_override=token_override,
@@ -83,16 +86,18 @@ async def pending_income(
8386
*,
8487
db: Path,
8588
full_refresh: bool,
89+
should_sync: bool = True,
8690
for_real: bool,
8791
skip_matched: bool,
8892
token_override: str | None,
8993
quiet: bool,
9094
) -> PendingIncomeResult:
9195
token = resolve_token(token_override)
9296

93-
_print("** Refreshing SQLite DB **", quiet=quiet)
94-
await sync(token, db, full_refresh, quiet=quiet)
95-
_print("** Done **", quiet=quiet)
97+
if should_sync:
98+
_print("** Refreshing SQLite DB **", quiet=quiet)
99+
await sync(token, db, full_refresh, quiet=quiet)
100+
_print("** Done **", quiet=quiet)
96101

97102
async with aiosqlite.connect(db) as con:
98103
con.row_factory = aiosqlite.Row

manager_for_ynab/reconciler/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ $ manager-for-ynab reconciler --mode interactive-batch --account-likes Checking
4949
Target balances in matching order, separated by spaces: 500 290
5050
```
5151

52+
By default, the command refreshes the local sqlite-export-for-ynab database before reading from it. Pass `--no-sync` to use the existing database contents without syncing.
53+
5254
### All Options
5355

5456
```console
@@ -59,6 +61,7 @@ usage: manager-for-ynab reconciler [-h] [--mode {single,batch,interactive-batch}
5961
[--for-real]
6062
[--sqlite-export-for-ynab-db SQLITE_EXPORT_FOR_YNAB_DB]
6163
[--sqlite-export-for-ynab-full-refresh]
64+
[--sync | --no-sync]
6265

6366
Find and automatically reconciles unreconciled transactions.
6467

@@ -86,4 +89,5 @@ options:
8689
--sqlite-export-for-ynab-full-refresh
8790
Whether to **DROP ALL TABLES** and fetch all plan data again. If unset, this
8891
tool only does an incremental refresh
92+
--sync, --no-sync Refresh the SQLite DB before using it.
8993
```

manager_for_ynab/reconciler/__init__.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,12 @@ def build_parser() -> argparse.ArgumentParser:
156156
action="store_true",
157157
help="Whether to **DROP ALL TABLES** and fetch all plan data again. If unset, this tool only does an incremental refresh",
158158
)
159+
parser.add_argument(
160+
"--sync",
161+
action=argparse.BooleanOptionalAction,
162+
default=True,
163+
help="Refresh the SQLite DB before using it.",
164+
)
159165
return parser
160166

161167

@@ -172,6 +178,7 @@ async def run(
172178
for_real=args.for_real,
173179
db=args.sqlite_export_for_ynab_db,
174180
full_refresh=args.sqlite_export_for_ynab_full_refresh,
181+
should_sync=args.sync,
175182
token_override=token_override,
176183
)
177184

@@ -186,6 +193,7 @@ async def reconciler(
186193
for_real: bool,
187194
db: Path,
188195
full_refresh: bool,
196+
should_sync: bool = True,
189197
token_override: str | None,
190198
) -> int:
191199
target_set = await _resolve_target_set(
@@ -202,9 +210,10 @@ async def reconciler(
202210

203211
token = resolve_token(token_override)
204212

205-
print("** Refreshing SQLite DB **")
206-
await sync(token, db, full_refresh)
207-
print("** Done **")
213+
if should_sync:
214+
print("** Refreshing SQLite DB **")
215+
await sync(token, db, full_refresh)
216+
print("** Done **")
208217

209218
async with aiosqlite.connect(db) as con:
210219
con.row_factory = aiosqlite.Row

tests/add_transaction/test.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ async def test_run_delegates_parsed_args(add_transaction_mock):
203203
"--sqlite-export-for-ynab-db",
204204
"/tmp/db.sqlite",
205205
"--sqlite-export-for-ynab-full-refresh",
206+
"--no-sync",
206207
)
207208
)
208209

@@ -220,6 +221,7 @@ async def test_run_delegates_parsed_args(add_transaction_mock):
220221
assert kwargs["quiet"] is True
221222
assert kwargs["db"] == Path("/tmp/db.sqlite")
222223
assert kwargs["full_refresh"] is True
224+
assert kwargs["should_sync"] is False
223225

224226

225227
@pytest.mark.parametrize(
@@ -275,6 +277,34 @@ async def test_add_transaction_dry_run(
275277
transactions_api_cls.assert_not_called()
276278

277279

280+
@patch("manager_for_ynab.add_transaction.sync", new_callable=AsyncMock)
281+
@pytest.mark.asyncio
282+
async def test_add_transaction_no_sync_uses_existing_db(sync_mock, tmp_path, capsys):
283+
db_path = tmp_path / "add-transaction.sqlite"
284+
_create_add_transaction_db(db_path)
285+
286+
ret = await add_transaction(
287+
plan_name=None,
288+
account_name="Checking",
289+
payee_name="Employer",
290+
category_name="Inflow: Ready to Assign",
291+
date=date(2026, 4, 26),
292+
cleared=None,
293+
amount=Decimal("12.34"),
294+
for_real=False,
295+
quiet=False,
296+
db=db_path,
297+
full_refresh=False,
298+
should_sync=False,
299+
token_override="token",
300+
)
301+
302+
out, _ = capsys.readouterr()
303+
assert ret == 0
304+
sync_mock.assert_not_awaited()
305+
assert "** Refreshing SQLite DB **" not in out
306+
307+
278308
@patch(
279309
"manager_for_ynab.add_transaction._apply_category_budget_delta",
280310
new_callable=AsyncMock,

tests/auto_approve/test.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,20 @@ async def test_run_quiet_suppresses_all_output(sync, db, capsys):
246246
assert out == ""
247247

248248

249+
@patch.dict("os.environ", {_ENV_TOKEN: "token"})
250+
@patch.object(ynab, "TransactionsApi", unexpected_transactions_api)
251+
@patch("manager_for_ynab.auto_approve.sync")
252+
@pytest.mark.asyncio
253+
async def test_run_no_sync_uses_existing_db(sync, db, capsys):
254+
ret = await run(("--sqlite-export-for-ynab-db", str(db), "--no-sync"))
255+
256+
out, _ = capsys.readouterr()
257+
assert ret == 0
258+
sync.assert_not_called()
259+
assert "** Refreshing SQLite DB **" not in out
260+
assert "Found 3 transaction(s) to approve." in out
261+
262+
249263
@patch.dict("os.environ", {_ENV_TOKEN: "token"})
250264
@patch("manager_for_ynab.auto_approve.sync")
251265
@pytest.mark.asyncio

0 commit comments

Comments
 (0)