Compare commits

..

4 Commits

Author SHA1 Message Date
bd7fe90caf
chore: update dependencies 2026-01-01 19:15:14 +00:00
cee5fe77e1
feat: improve GoCardless error handling
Handle 409 when retriving transactions, which signals the account data
is still being processed, and 429, which signals the rate lite was
exceeded.
2026-01-01 19:15:14 +00:00
46f6ee2af7
feat: log GoCardless responses to file 2026-01-01 19:15:14 +00:00
1aae7d8748
fix: skip command in interactive
Also replace old DB `get` method with `select` and fix issues caught by
mypy.
2026-01-01 19:15:14 +00:00
4 changed files with 1295 additions and 885 deletions

View File

@ -36,23 +36,23 @@ class Interactive:
uncategorized = session.select( uncategorized = session.select(
Transaction, lambda: ~Transaction.category.has() Transaction, lambda: ~Transaction.category.has()
) )
uncategorized.sort() list(uncategorized).sort()
n = len(uncategorized) n = len(uncategorized)
print(f"{n} left to categorize") print(f"{n} left to categorize")
i = 0 i = 0
new = [] new = []
while (command := input("$ ")) != "quit" and i < len(uncategorized): while i < len(uncategorized):
current = uncategorized[i] if len(new) == 0 else new.pop() current = uncategorized[i] if len(new) == 0 else new.pop()
print(current) print(current)
command = input("$ ")
match command: match command:
case "help": case "help":
print(self.help) print(self.help)
case "skip": case "skip":
if len(uncategorized) == 0:
i += 1 i += 1
case "quit": case "quit":
@ -89,7 +89,7 @@ class Interactive:
for tag in tags: for tag in tags:
if tag not in [t.name for t in self.tags]: if tag not in [t.name for t in self.tags]:
session.insert([Tag(tag)]) session.insert([Tag(tag)])
self.tags = session.get(Tag) self.tags = session.select(Tag)
current.tags.add(TransactionTag(tag)) current.tags.add(TransactionTag(tag))
@ -98,7 +98,7 @@ class Interactive:
def split(self, original: Transaction) -> list[SplitTransaction]: def split(self, original: Transaction) -> list[SplitTransaction]:
total = original.amount total = original.amount
new = [] new: list[SplitTransaction] = []
done = False done = False
while not done: while not done:
@ -114,7 +114,7 @@ class Interactive:
amount = decimal.Decimal(input("amount: ")) amount = decimal.Decimal(input("amount: "))
new.append( new.append(
SplitTransaction( SplitTransaction(
original.date, original.description, amount, original.id original.date, original.description, amount, original=original.id
) )
) )

View File

@ -1,6 +1,7 @@
from dataclasses import dataclass from dataclasses import dataclass
import datetime as dt import datetime as dt
import dotenv import dotenv
import json
import nordigen import nordigen
import os import os
import requests import requests
@ -60,11 +61,33 @@ class NordigenClient:
retries += 1 retries += 1
print(f"Request #{retries} timed-out, retrying in 1s") print(f"Request #{retries} timed-out, retrying in 1s")
time.sleep(1) time.sleep(1)
except requests.HTTPError as e:
if (
e.response.status_code == 409
and e.response.type == "AccountProcessing"
):
timeout = 1
while account.get_metadata()["status"] != "READY":
print(f"Waiting for status ready, retrying in {timeout}s")
time.sleep(timeout)
timeout *= 2
elif e.response.status_code == 429:
print("Rate limit exceeded for today, aborting")
break
else:
raise DownloadError(e)
if not downloaded: if not downloaded:
print(f"Couldn't download transactions for {account.get_metadata()}") print(f"Couldn't download transactions for {account.get_metadata()}")
continue continue
with open(
f"logs/{dt.datetime.now().isoformat()}_{requisition_id}.json",
"w",
encoding="utf-8",
) as f:
json.dump(downloaded, f, ensure_ascii=False, indent=4)
if ( if (
"transactions" not in downloaded "transactions" not in downloaded
or "booked" not in downloaded["transactions"] or "booked" not in downloaded["transactions"]

2141
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,7 @@ pytest = "^7.3.0"
pytest-cov = "^4.0.0" pytest-cov = "^4.0.0"
pytest-mock = "^3.10.0" pytest-mock = "^3.10.0"
sqlalchemy = {extras = ["mypy"], version = "^2.0.9"} sqlalchemy = {extras = ["mypy"], version = "^2.0.9"}
ruff = "^0.0.267" ruff = "^0.11.2"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]