#!/usr/bin/env python import sys import csv import os import base64 import getpass import getopt import platform import re import urllib import urllib2 import xml.etree.ElementTree as ET client_id = "Abn2Wesabe-Uploader" # Application name client_version = "0.2" # Application version number system_name = platform.system() system_release = platform.release() api_version = "1.0.0" # Document which API version I am using user_agent = "%s/%s (%s %s) Wesabe-API/%s" % \ (client_id, client_version, system_name, system_release, api_version) accounts_list = {} accounts_url = "https://www.wesabe.com/accounts.xml" upload_url = "https://api.wesabe.com/rest/upload/statement" # Functions and classes def ask_for_credentials(): sys.stdout.write("Wesabe username: ") username = sys.stdin.readline().rstrip() password = getpass.getpass("Password: ") return (username, password) def ask_for_accid(): sys.stdout.write("Id of the account to be updated: ") id = sys.stdin.readline().rstrip() return id ## # Makes a request to Wesabe and returns the list of bank accounts # (filtering cash ones) corresponding to the given user # # @param username Wesabe Username # @param password Password for the user # @return List of dictionaries containing account information def get_accounts_list(username, password): credentials = base64.encodestring('%s:%s' % (username, password))[:-1] request = urllib2.Request(accounts_url) request.add_header("User-Agent", user_agent) request.add_header("Authorization", "Basic %s" % credentials) stream = urllib2.urlopen(request) response = stream.read() stream.close() return [ { 'id' : account.find('id').text, 'number' : account.find('account-number').text, 'type' : account.find('account-type').text, 'wesabe_id' : account.find('financial-institution').find('id').text, 'name' : account.find('name').text, 'balance' : account.find('current-balance').text } # Filtering out the Cash accounts for account in ET.fromstring(response).findall('account') if account.find('account-type').text != 'Cash' ] ## # Uploads the actual transaction list information into Wesabe # # @param xml Statement upload XML, which includes the QIF contents def upload_data(xml, acctid, accttype, balance, wesabe_id, username, password): root = ET.Element('upload') statement = ET.SubElement(root, 'statement', acctid=acctid, accttype=accttype, balance=balance, wesabe_id=wesabe_id) statement.text = xml credentials = base64.encodestring('%s:%s' % (username, password))[:-1] headers = { "Authorization" : "Basic %s" % credentials, "User-Agent" : user_agent, "Content-type" : "application/xml", "Accept" : "*/* application/xml" } request = urllib2.Request(upload_url, ET.tostring(root), headers) return urllib2.urlopen(request).read() ## # The following function uses part of the "ABN Amro 2 QIF Converter" by Johan Kool. # More information at http://www.johankool.nl/software/abnamro2qif/ # # It converts ABN AMRO bank statements into qif files. # # @param inputfile The statement file generated by the bank # @param tempfile Temporary file # @return string containing the QIF converted statement def convert2qif(inputfile_name): output = "!Type:Bank\n" inputfile = open(inputfile_name, 'rb') tempfile_name = inputfile_name + '.temp' tempfile = open(tempfile_name, 'w') class Custom(csv.excel_tab): skipinitialspace = True delimiter = '\t' csv.register_dialect('custom', Custom) # add account currency date1 misc1 misc2 date2 amount description line to inputfile tempfile.write("account\tcurrency\tdate1\tmisc1\tmisc2\tdate2\tamount\tdescription\r\n") # zap useless ascii control characters identity_map = ''.join([chr(x) for x in range(256)]) bad_map = ''.join([chr(x) for x in range(8)] + [chr(x) for x in range(11,32)] + [chr(124)]) clean_text = inputfile.read().translate(identity_map, bad_map) tempfile.write(clean_text) tempfile.close() tempfile = open(tempfile_name, 'rb') reader = csv.DictReader(tempfile, dialect='custom') for row in reader: # convert date from yyyymmdd to dd/mm/yyyy date = row["date1"][6:8]+"/"+row["date1"][4:6]+"/"+row["date1"][0:4] # convert amount from -nnn,nn to -nnn.nn amount = row["amount"].replace(",",".") data = "D%s\nP%s\nT%s\n^\n" % (date, row["description"], amount) output += data # cleanup temp file os.remove(tempfile_name) return output def uploadFile(username, password, inputfile_name): finalbalance = '' for account in get_accounts_list(username, password): accounts_list[account['id']] = account print "%s:\t%s\t%s" % (account['id'], account['name'], account['balance']) chosen_acc = ask_for_accid() if (chosen_acc in accounts_list): print "Uploading data to Wesabe account..." try: # Get current final balance from bank statement inputfile = open(inputfile_name, 'rb') r = re.compile("^\d+\s[A-Z]{3}\s\d{8}\s[\d,]+\s(\d+,\d+)") finalbalance = re.match(r, inputfile.read()).group(1).replace(',', '.') inputfile.close() except: print >>sys.stderr, "\Warning:\nThe final balance could not be retrieved." acc = accounts_list[chosen_acc] return upload_data(convert2qif(inputfile_name), acc['number'], acc['type'], finalbalance, acc['wesabe_id'], username, password) else: return '' def main(argv=None): if argv is None: argv = sys.argv if len(argv) < 2: print >>sys.stderr, "No statement file provided\nUsage: abn2wesabe STATEMENTFILE.TAB" sys.exit() inputfile_name = argv[1] accountlist = '' while 1: try: (username, password) = ask_for_credentials() accountlist = uploadFile(username, password, inputfile_name) if not accountlist == '': if ET.fromstring(accountlist).get('status') == 'failed': print >>sys.stderr, "Error: Wesabe could not process the transaction." else: print "Success!" break else: print >>sys.stderr, "\nError:\nWrong account ID. Try again." except urllib2.HTTPError, ex: if (ex.code == 401): print >>sys.stderr, "The username and password combination is not valid, try again." except: print >>sys.stderr, "Unexpected error: ", sys.exc_info()[0] break if __name__ == "__main__": sys.exit(main())