#!/usr/bin/env python3 import os import sys import csv import json import logging import datetime import ofxtools from typing import List from dataclasses import dataclass from ofxtools import OFXClient from ofxtools.Client import StmtRq, CcStmtEndRq, CcStmtRq from ofxtools.Parser import OFXTree @dataclass class ClientConfig: url: str userid: str org: str clientuid: str fid: str bankid: str version: int @dataclass class AccountConfig: name: str accttype: str acctid: str csv_file: str fields: List[str] @dataclass class Config: """ Basic class for abstracting the configuration. """ secret: str client: ClientConfig accounts: List[AccountConfig] @dataclass class Transaction: date: str description: str amount: str def get_transactions(client: OFXClient, secret: str, account: AccountConfig): dtstart = datetime.datetime(2020, 1, 1, tzinfo=ofxtools.utils.UTC) dtend = datetime.datetime(2020, 12, 31, tzinfo=ofxtools.utils.UTC) if account.accttype.upper() in ("CHECKING", "SAVINGS"): rq = StmtRq(acctid=account.acctid, accttype=account.accttype.upper(), dtstart=dtstart, dtend=dtend) else: rq = CcStmtRq(acctid=account.acctid, dtstart=dtstart, dtend=dtend) response = client.request_statements(secret, rq) parser = OFXTree() parser.parse(response) ofx = parser.convert() ts = [Transaction(t.dtposted.strftime("%m/%d/%Y"), t.name + " " + t.memo if t.memo else t.name, str(t.trnamt)) for t in ofx.statements[0].banktranlist] return ts def write_csv(account: AccountConfig, transactions: List[Transaction]): def transaction_to_csv_row(t: Transaction) -> List[str]: """ This allows to user to specify how to order the fields in the CSV file. I have implemented this feature because the columns in my checking account and in my credit card accounts are different. If the field is one of 'date', 'description', or 'amount' we get that attribute from the transaction. Otherwise, we use the field itself (usually an empty string in my case). """ return [getattr(t, f) if hasattr(t, f) else f for f in account.fields] status = "no change" csv_file = account.csv_file if not os.path.isfile(csv_file): status = "new" with open(account.csv_file, "w") as f: csv_writer = csv.writer(f) csv_writer.writerow(["date", "description", "amount"]) for t in transactions: r = transaction_to_csv_row(t) csv_writer.writerow(r) else: # TODO: diff rows and append only the new once. pass logging.warning(f"{account.name:30} -> {account.csv_file:30} | {status}") def get_client(c: ClientConfig) -> OFXClient: return OFXClient(c.url, userid=c.userid, org=c.org, fid=c.fid, clientuid=c.clientuid, bankid=c.bankid, version=c.version, prettyprint=True) def parse_config(config_file: str) -> Config: with open(config_file, 'r') as f: # We could use the dacite package if the configureation # gets more complex and for automatical type ckecking, but # probably not worth it as this point. config = Config(**json.load(f)) config.client = ClientConfig(**config.client) config.accounts = [AccountConfig(**a) for a in config.accounts] return config def main(config: Config): client = get_client(config.client) for account in config.accounts: transactions = get_transactions(client, config.secret, account) write_csv(account, transactions) if __name__ == "__main__": logging.basicConfig(level=logging.WARNING, format='%(message)s') try: config_file = sys.argv[1] except IndexError: config_file = "getofx.json" config = parse_config(config_file) main(config)