ofxclient-1.3.8/0000755000076500000240000000000012124303072015114 5ustar davidbartlestaff00000000000000ofxclient-1.3.8/MANIFEST.in0000644000076500000240000000001512123420306016645 0ustar davidbartlestaff00000000000000include *.md ofxclient-1.3.8/ofxclient/0000755000076500000240000000000012124303072017107 5ustar davidbartlestaff00000000000000ofxclient-1.3.8/ofxclient/__init__.py0000644000076500000240000000025412124247234021230 0ustar davidbartlestaff00000000000000from institution import Institution from account import Account, BrokerageAccount, CreditCardAccount, BankAccount from client import Client from version import __version__ ofxclient-1.3.8/ofxclient/account.py0000644000076500000240000002315312124247220021123 0ustar davidbartlestaff00000000000000from ofxparse import OfxParser, AccountType import datetime import StringIO import time import hashlib class Account(object): """Base class for accounts at an institution :param number: The account number :type number: string :param institution: The bank this belongs to :type institution: :py:class:`ofxclient.Institution` object :param description: optional account description :type description: string or None This class is almost never never instantiated on it's own. Instead, sub-classes are instantiated. In most cases these subclasses are either being deserialized from a config file entry, a serialization hash, or returned by the :py:meth:`ofxclient.Institution.accounts` method. Example from a saved config entry:: from ofxclient.config import OfxConfig account = OfxConfig().account('local_id() string') Example of deserialization:: from ofxclient import BankAccount # assume 'inst' is an Institution() a1 = BankAccount(number='asdf',institution=inst) data1 = a1.serialize() a2 = Account.deserialize(data1) Example by querying the bank directly:: from ofxclient import Institution # assume an Institution() is configured with # a username/password etc accounts = institution.accounts() .. seealso:: :py:class:`ofxclient.BankAccount` :py:class:`ofxclient.BrokerageAccount` :py:class:`ofxclient.CreditCardAccount` """ def __init__(self, number, institution, description=None): self.institution = institution self.number = number self.description = description or self._default_description() def local_id(self): """Locally generated unique account identifier. :rtype: string """ return hashlib.sha256("%s%s" % ( self.institution.local_id(), self.number)).hexdigest() def number_masked(self): """Masked version of the account number for privacy. :rtype: string """ return "***%s" % self.number[-4:] def long_description(self): """Long description of the account (includes institution description). :rtype: string """ return "%s: %s" % (self.institution.description, self.description) def _default_description(self): return self.number_masked() def download(self, days=60): """Downloaded OFX response for the given time range :param days: Number of days to look back at :type days: integer :rtype: :py:class:`StringIO.StringIO` """ days_ago = datetime.datetime.now() - datetime.timedelta(days=days) as_of = time.strftime("%Y%m%d", days_ago.timetuple()) query = self._download_query(as_of=as_of) response = self.institution.client().post(query) return StringIO.StringIO(response) def download_parsed(self, days=60): """Downloaded OFX response parsed by :py:meth:`OfxParser.parse` :param days: Number of days to look back at :type days: integer :rtype: :py:class:`ofxparser.Ofx` """ return OfxParser.parse(self.download(days=days)) def statement(self, days=60): """Download the :py:class:`ofxparse.Statement` given the time range :param days: Number of days to look back at :type days: integer :rtype: :py:class:`ofxparser.Statement` """ parsed = self.download_parsed(days=days) return parsed.account.statement def transactions(self, days=60): """Download a a list of :py:class:`ofxparse.Transaction` objects :param days: Number of days to look back at :type days: integer :rtype: list of :py:class:`ofxparser.Transaction` objects """ return self.statement(days=days).transactions def serialize(self): """Serialize predictably for use in configuration storage. Output look like this:: { 'local_id': 'string', 'number': 'account num', 'description': 'descr', 'broker_id': 'may be missing - type dependent', 'routing_number': 'may be missing - type dependent, 'account_type': 'may be missing - type dependent, 'institution': { # ... see :py:meth:`ofxclient.Institution.serialize` } } :rtype: nested dictionary """ data = { 'local_id': self.local_id(), 'institution': self.institution.serialize(), 'number': self.number, 'description': self.description } if hasattr(self, 'broker_id'): data['broker_id'] = self.broker_id elif hasattr(self, 'routing_number'): data['routing_number'] = self.routing_number data['account_type'] = self.account_type return data @staticmethod def deserialize(raw): """Instantiate :py:class:`ofxclient.Account` subclass from dictionary :param raw: serilized Account :param type: dict as given by :py:meth:`~ofxclient.Account.serialize` :rtype: subclass of :py:class:`ofxclient.Account` """ from ofxclient.institution import Institution institution = Institution.deserialize(raw['institution']) del raw['institution'] del raw['local_id'] if 'broker_id' in raw: a = BrokerageAccount(institution=institution, **raw) elif 'routing_number' in raw: a = BankAccount(institution=institution, **raw) else: a = CreditCardAccount(institution=institution, **raw) return a @staticmethod def from_ofxparse(data, institution): """Instantiate :py:class:`ofxclient.Account` subclass from ofxparse module :param data: an ofxparse account :type data: An :py:class:`ofxparse.Account` object :param institution: The parent institution of the account :type institution: :py:class:`ofxclient.Institution` object """ description = data.desc if hasattr(data, 'desc') else None if data.type == AccountType.Bank: return BankAccount( institution=institution, number=data.account_id, routing_number=data.routing_number, account_type=data.account_type, description=description) elif data.type == AccountType.CreditCard: return CreditCardAccount( institution=institution, number=data.account_id, description=description) elif data.type == AccountType.Investment: return BrokerageAccount( institution=institution, number=data.account_id, broker_id=data.brokerid, description=description) raise ValueError("unknown account type: %s" % data.type) class BrokerageAccount(Account): """:py:class:`ofxclient.Account` subclass for brokerage/investment accounts In addition to the parameters it's superclass requires, the following parameters are needed. :param broker_id: Broker ID of the account :type broker_id: string .. seealso:: :py:class:`ofxclient.Account` """ def __init__(self, broker_id, **kwargs): super(BrokerageAccount, self).__init__(**kwargs) self.broker_id = broker_id def _download_query(self, as_of): """Formulate the specific query needed for download Not intended to be called by developers directly. :param as_of: Date in 'YYYYMMDD' format :type as_of: string """ c = self.institution.client() q = c.brokerage_account_query( number=self.number, date=as_of, broker_id=self.broker_id) return q class BankAccount(Account): """:py:class:`ofxclient.Account` subclass for a checking/savings account In addition to the parameters it's superclass requires, the following parameters are needed. :param routing_number: Routing number or account number of the account :type routing_number: string :param account_type: Account type per OFX spec can be empty but not None :type account_type: string .. seealso:: :py:class:`ofxclient.Account` """ def __init__(self, routing_number, account_type, **kwargs): super(BankAccount, self).__init__(**kwargs) self.routing_number = routing_number self.account_type = account_type def _download_query(self, as_of): """Formulate the specific query needed for download Not intended to be called by developers directly. :param as_of: Date in 'YYYYMMDD' format :type as_of: string """ c = self.institution.client() q = c.bank_account_query( number=self.number, date=as_of, account_type=self.account_type, bank_id=self.routing_number) return q class CreditCardAccount(Account): """:py:class:`ofxclient.Account` subclass for a credit card account No additional parameters to the constructor are needed. .. seealso:: :py:class:`ofxclient.Account` """ def __init__(self, **kwargs): super(CreditCardAccount, self).__init__(**kwargs) def _download_query(self, as_of): """Formulate the specific query needed for download Not intended to be called by developers directly. :param as_of: Date in 'YYYYMMDD' format :type as_of: string """ c = self.institution.client() q = c.credit_card_account_query(number=self.number, date=as_of) return q ofxclient-1.3.8/ofxclient/cli.py0000644000076500000240000001512512124302667020245 0ustar davidbartlestaff00000000000000from ofxclient.account import BankAccount, BrokerageAccount, CreditCardAccount from ofxclient.institution import Institution from ofxclient.util import combined_download from ofxhome import OFXHome import argparse import config import getpass import os import os.path import client import sys AUTO_OPEN_DOWNLOADS = 1 DOWNLOAD_DAYS = 30 GlobalConfig = config.OfxConfig() def run(): accounts = GlobalConfig.accounts() account_ids = [a.local_id() for a in accounts] parser = argparse.ArgumentParser(prog='ofxclient') parser.add_argument('-a', '--account', choices=account_ids) parser.add_argument('-d', '--download', type=argparse.FileType('wb', 0)) parser.add_argument('-o', '--open', action='store_true') args = parser.parse_args() if args.download: if accounts: if args.account: a = GlobalConfig.account(args.account) ofxdata = a.download(days=DOWNLOAD_DAYS) else: ofxdata = combined_download(accounts, days=DOWNLOAD_DAYS) args.download.write(ofxdata.read()) if args.open: open_with_ofx_handler(args.download.name) sys.exit(0) else: print "no accounts configured" main_menu() def main_menu(): while 1: menu_title("Main\nEdit %s to\nchange descriptions or ofx options" % GlobalConfig.file_name) accounts = GlobalConfig.accounts() for idx, account in enumerate(accounts): menu_item(idx, account.long_description()) menu_item('A', 'Add an account') if accounts: menu_item('D', 'Download all combined') menu_item('Q', 'Quit') choice = prompt().lower() if choice == 'a': add_account_menu() elif choice == 'd': if not accounts: print "no accounts on file" else: ofxdata = combined_download(accounts, days=DOWNLOAD_DAYS) wrote = write_and_handle_download( ofxdata, 'combined_download.ofx' ) print "wrote: %s" % wrote elif choice in ['q', '']: return elif int(choice) < len(accounts): account = accounts[int(choice)] view_account_menu(account) def add_account_menu(): menu_title("Add account") while 1: query = prompt('enter part of a bank name eg. express> ') if query.lower() in ['']: return found = OFXHome.search(query) if not found: error("No banks found") continue while 1: for idx, bank in enumerate(found): menu_item(idx, bank['name']) choice = prompt().lower() if choice in ['q', '']: return elif int(choice) < len(found): bank = OFXHome.lookup(found[int(choice)]['id']) if login_check_menu(bank): return def view_account_menu(account): while 1: menu_title(account.long_description()) institution = account.institution client = institution.client() print "Overview:" print " Name: %s" % account.description print " Account Number: %s" % account.number_masked() print " Institution: %s" % institution.description print " Main Type: %s" % str(type(account)) if hasattr(account, 'routing_number'): print " Routing Number: %s" % account.routing_number print " Sub Type: %s" % account.account_type if hasattr(account, 'broker_id'): print " Broker ID: %s" % account.broker_id print "Nerdy Info:" print " Download Up To: %s days" % DOWNLOAD_DAYS print " Username: %s" % institution.username print " Local Account ID: %s" % account.local_id() print " Local Institution ID: %s" % institution.local_id() print " FI Id: %s" % institution.id print " FI Org: %s" % institution.org print " FI Url: %s" % institution.url if institution.broker_id: print " FI Broker Id: %s" % institution.broker_id print " Client Id: %s" % client.id print " App Ver: %s" % client.app_version print " App Id: %s" % client.app_id print " OFX Ver: %s" % client.ofx_version print " Config File: %s" % GlobalConfig.file_name menu_item('D', 'Download') choice = prompt().lower() if choice == 'd': out = account.download(days=DOWNLOAD_DAYS) wrote = write_and_handle_download(out, "%s.ofx" % account.local_id()) print "wrote: %s" % wrote return def login_check_menu(bank_info): while 1: username = '' while not username: username = prompt('username> ') password = '' while not password: password = getpass.getpass('password> ') i = Institution( id=bank_info['fid'], org=bank_info['org'], url=bank_info['url'], broker_id=bank_info['brokerid'], description=bank_info['name'], username=username, password=password ) try: i.authenticate() except Exception, e: print "authentication failed: %s" % e continue accounts = i.accounts() for a in accounts: GlobalConfig.add_account(a) GlobalConfig.save() return 1 def write_and_handle_download(ofx_data, name): outfile = open(name, 'w') outfile.write(ofx_data.read()) outfile.close() if AUTO_OPEN_DOWNLOADS: open_with_ofx_handler(name) return os.path.abspath(name) def prompt(text='choice> '): got = raw_input(text) return got def error(text=''): print "!! %s" % text def menu_item(key, description): print "(%s) %s" % (key, description) def menu_title(name): print "+----------------------------------" print "%s" % name print "+----------------------------------" def open_with_ofx_handler(filename): import platform sysname = platform.system() if sysname == 'Darwin': os.system("/usr/bin/open '%s'" % filename) elif sysname == 'Windows': os.startfile(filename) else: # linux os.system("xdg-open '%s'" % filename) if __name__ == '__main__': run() ofxclient-1.3.8/ofxclient/client.py0000644000076500000240000001461712124250466020760 0ustar davidbartlestaff00000000000000import httplib import time import urllib2 DEFAULT_APP_ID = 'QWIN' DEFAULT_APP_VERSION = '2200' DEFAULT_OFX_VERSION = '102' LINE_ENDING = "\r\n" def ofx_uid(): import uuid return str(uuid.uuid4().hex) class Client: """This communicates with the banks via the OFX protocol :param institution: institution to connect to :type institution: :py:class:`ofxclient.Institution` :param id: client id (optional need for OFX version >= 103) :type id: string :param app_id: OFX app id :type app_id: string :param app_version: OFX app version :type app_version: string :param ofx_version: OFX spec version :type ofx_version: string """ def __init__( self, institution, id=ofx_uid(), app_id=DEFAULT_APP_ID, app_version=DEFAULT_APP_VERSION, ofx_version=DEFAULT_OFX_VERSION ): self.institution = institution self.id = id self.app_id = app_id self.app_version = app_version self.ofx_version = ofx_version self.cookie = 3 def authenticated_query( self, with_message=None, username=None, password=None ): """Authenticated query If you pass a 'with_messages' array those queries will be passed along otherwise this will just be an authentication probe query only. """ u = username or self.institution.username p = password or self.institution.password contents = ['OFX', self._signOn(username=u, password=p)] if with_message: contents.append(with_message) return str.join(LINE_ENDING, [ self.header(), _tag(*contents) ]) def bank_account_query(self, number, date, account_type, bank_id): """Bank account statement request""" return self.authenticated_query( self._bareq(number, date, account_type, bank_id) ) def credit_card_account_query(self, number, date): """CC Statement request""" return self.authenticated_query(self._ccreq(number, date)) def brokerage_account_query(self, number, date, broker_id): return self.authenticated_query( self._invstreq(broker_id, number, date)) def account_list_query(self, date='19700101000000'): return self.authenticated_query(self._acctreq(date)) def post(self, query): # N.B. urllib doesn't honor user Content-type, use urllib2 i = self.institution garbage, path = urllib2.splittype(i.url) host, selector = urllib2.splithost(path) h = httplib.HTTPSConnection(host) h.request('POST', selector, query, { "Content-type": "application/x-ofx", "Accept": "*/*, application/x-ofx" }) res = h.getresponse() response = res.read() res.close() return response def next_cookie(self): self.cookie += 1 return str(self.cookie) def header(self): parts = [ "OFXHEADER:100", "DATA:OFXSGML", "VERSION:%d" % int(self.ofx_version), "SECURITY:NONE", "ENCODING:USASCII", "CHARSET:1252", "COMPRESSION:NONE", "OLDFILEUID:NONE", "NEWFILEUID:"+ofx_uid(), "" ] return str.join(LINE_ENDING, parts) """Generate signon message""" def _signOn(self, username=None, password=None): i = self.institution u = username or i.username p = password or i.password fidata = [_field("ORG", i.org)] if i.id: fidata.append(_field("FID", i.id)) client_uid = '' if str(self.ofx_version) == '103': client_uid = _field('CLIENTUID', self.id) return _tag("SIGNONMSGSRQV1", _tag("SONRQ", _field("DTCLIENT", now()), _field("USERID", u), _field("USERPASS", p), _field("LANGUAGE", "ENG"), _tag("FI", *fidata), _field("APPID", self.app_id), _field("APPVER", self.app_version), client_uid )) def _acctreq(self, dtstart): req = _tag("ACCTINFORQ", _field("DTACCTUP", dtstart)) return self._message("SIGNUP", "ACCTINFO", req) # this is from _ccreq below and reading page 176 of the latest OFX doc. def _bareq(self, acctid, dtstart, accttype, bankid): req = _tag("STMTRQ", _tag("BANKACCTFROM", _field("BANKID", bankid), _field("ACCTID", acctid), _field("ACCTTYPE", accttype)), _tag("INCTRAN", _field("DTSTART", dtstart), _field("INCLUDE", "Y"))) return self._message("BANK", "STMT", req) def _ccreq(self, acctid, dtstart): req = _tag("CCSTMTRQ", _tag("CCACCTFROM", _field("ACCTID", acctid)), _tag("INCTRAN", _field("DTSTART", dtstart), _field("INCLUDE", "Y"))) return self._message("CREDITCARD", "CCSTMT", req) def _invstreq(self, brokerid, acctid, dtstart): req = _tag("INVSTMTRQ", _tag("INVACCTFROM", _field("BROKERID", brokerid), _field("ACCTID", acctid)), _tag("INCTRAN", _field("DTSTART", dtstart), _field("INCLUDE", "Y")), _field("INCOO", "Y"), _tag("INCPOS", _field("DTASOF", now()), _field("INCLUDE", "Y")), _field("INCBAL", "Y")) return self._message("INVSTMT", "INVSTMT", req) def _message(self, msgType, trnType, request): return _tag(msgType+"MSGSRQV1", _tag(trnType+"TRNRQ", _field("TRNUID", ofx_uid()), _field("CLTCOOKIE", self.next_cookie()), request)) def _field(tag, value): return "<"+tag+">"+value def _tag(tag, *contents): return str.join(LINE_ENDING, ["<"+tag+">"]+list(contents)+[""]) def now(): return time.strftime("%Y%m%d%H%M%S", time.localtime()) ofxclient-1.3.8/ofxclient/config.py0000644000076500000240000002320412124254011020725 0ustar davidbartlestaff00000000000000from __future__ import with_statement from ofxclient.account import Account from ConfigParser import ConfigParser import os import os.path try: import keyring KEYRING_AVAILABLE = True except: KEYRING_AVAILABLE = False try: DEFAULT_CONFIG = os.path.expanduser(os.path.join('~', 'ofxclient.ini')) except: DEFAULT_CONFIG = None class SecurableConfigParser(ConfigParser): """:py:class:`ConfigParser.ConfigParser` subclass that knows how to store options marked as secure into the OS specific keyring/keychain. To mark an option as secure, the caller must call 'set_secure' at least one time for the particular option and from then on it will be seen as secure and will be stored / retrieved from the keychain. Example:: from ofxclient.config import SecurableConfigParser # password will not be saved in the config file c = SecurableConfigParser() c.add_section('Info') c.set('Info','username','bill') c.set_secure('Info','password','s3cre7') with open('config.ini','w') as fp: c.write(fp) """ _secure_placeholder = '%{secured}' def __init__(self, keyring_name='ofxclient', keyring_available=KEYRING_AVAILABLE, **kwargs): ConfigParser.__init__(self) self.keyring_name = keyring_name self.keyring_available = keyring_available self._unsaved = {} self.keyring_name = keyring_name def is_secure_option(self, section, option): """Test an option to see if it is secured or not. :param section: section id :type section: string :param option: option name :type option: string :rtype: boolean otherwise. """ if not self.has_section(section): return False if not self.has_option(section, option): return False if ConfigParser.get(self, section, option) == self._secure_placeholder: return True return False def has_secure_option(self, section, option): """See is_secure_option""" return self.is_secure_option(section, option) def items(self, section): """Get all items for a section. Subclassed, to ensure secure items come back with the unencrypted data. :param section: section id :type section: string """ items = [] for k, v in ConfigParser.items(self, section): if self.is_secure_option(section, k): v = self.get(section, k) items.append((k, v)) return items def secure_items(self, section): """Like items() but only return secure items. :param section: section id :type section: string """ return [x for x in self.items(section) if self.is_secure_option(section, x[0])] def set(self, section, option, value): """Set an option value. Knows how to set options properly marked as secure.""" if self.is_secure_option(section, option): self.set_secure(section, option, value) else: ConfigParser.set(self, section, option, value) def set_secure(self, section, option, value): """Set an option and mark it as secure. Any subsequent uses of 'set' or 'get' will also now know that this option is secure as well. """ if self.keyring_available: s_option = "%s%s" % (section, option) self._unsaved[s_option] = ('set', value) value = self._secure_placeholder ConfigParser.set(self, section, option, value) def get(self, section, option, *args): """Get option value from section. If an option is secure, populates the plain text.""" if self.is_secure_option(section, option) and self.keyring_available: s_option = "%s%s" % (section, option) if self._unsaved.get(s_option, [''])[0] == 'set': return self._unsaved[s_option][1] else: return keyring.get_password(self.keyring_name, s_option) return ConfigParser.get(self, section, option, *args) def remove_option(self, section, option): """Removes the option from ConfigParser as well as the secure storage backend """ if self.is_secure_option(section, option) and self.keyring_available: s_option = "%s%s" % (section, option) self._unsaved[s_option] = ('delete', None) ConfigParser.remove_option(self, section, option) def write(self, *args): """See ConfigParser.write(). Also writes secure items to keystore.""" ConfigParser.write(self, *args) if self.keyring_available: for key, thing in self._unsaved.items(): action = thing[0] value = thing[1] if action == 'set': keyring.set_password(self.keyring_name, key, value) elif action == 'delete': try: keyring.delete_password(self.keyring_name, key) except: pass self._unsaved = {} class OfxConfig(object): """Default config file handler for other tools to use. This can read and write from the default config which is $USERS_HOME/ofxclient.ini :param file_name: absolute path to a config file (optional) :type file_name: string or None Example usage:: from ofxclient.config import OfxConfig from ofxclient import Account a = Account() c = OfxConfig(file_name='/tmp/new.ini') c.add_account(a) c.save() account_list = c.accounts() one_account = c.account( a.local_id() ) """ def __init__(self, file_name=None): self.secured_field_names = [ 'institution.username', 'institution.password' ] f = file_name or DEFAULT_CONFIG if f is None: raise ValueError('file_name is required') self._load(f) def reload(self): """Reload the config file from disk""" return self._load() def accounts(self): """List of confgured :py:class:`ofxclient.Account` objects""" return [self._section_to_account(s) for s in self.parser.sections()] def encrypted_accounts(self): return [a for a in self.accounts() if self.is_encrypted_account(a.local_id())] def unencrypted_accounts(self): return [a for a in self.accounts() if not self.is_encrypted_account(a.local_id())] def account(self, id): """Get :py:class:`ofxclient.Account` by section id""" if self.parser.has_section(id): return self._section_to_account(id) return None def add_account(self, account): """Add Account to config (does not save)""" serialized = account.serialize() section_items = flatten_dict(serialized) section_id = section_items['local_id'] if not self.parser.has_section(section_id): self.parser.add_section(section_id) for key in sorted(section_items): self.parser.set(section_id, key, section_items[key]) self.encrypt_account(id=section_id) return self def encrypt_account(self, id): """Make sure that certain fields are encrypted.""" for key in self.secured_field_names: value = self.parser.get(id, key) self.parser.set_secure(id, key, value) return self def is_encrypted_account(self, id): """Are all fields for the account id encrypted?""" for key in self.secured_field_names: if not self.parser.is_secure_option(id, key): return False return True def remove_account(self, id): """Add Account from config (does not save)""" if self.parser.has_section(id): self.parser.remove_section(id) return True return False def save(self): """Save changes to config file""" with open(self.file_name, 'w') as fp: self.parser.write(fp) return self def _load(self, file_name=None): self.parser = None file_name = file_name or self.file_name if not os.path.exists(file_name): with open(file_name, 'a'): os.utime(file_name, None) self.file_name = file_name conf = SecurableConfigParser() conf.readfp(open(self.file_name)) self.parser = conf return self def _section_to_account(self, section): section_items = dict(self.parser.items(section)) serialized = unflatten_dict(section_items) return Account.deserialize(serialized) def unflatten_dict(dict, prefix=None, separator='.'): ret = {} for k, v in dict.items(): key_parts = k.split(separator) if len(key_parts) == 1: ret[k] = v else: first = key_parts[0] rest = key_parts[1:] temp = ret.setdefault(first, {}) for idx, part in enumerate(rest): if (idx+1) == len(rest): temp[part] = v else: temp = temp.setdefault(part, {}) return ret def flatten_dict(dict_, prefix=None, separator='.'): ret = {} for k, v in dict_.items(): if prefix: flat_key = separator.join([prefix, k]) else: flat_key = k if isinstance(v, dict): deflated = flatten_dict(v, prefix=flat_key) for dk, dv in deflated.items(): ret[dk] = dv else: ret[flat_key] = v return ret ofxclient-1.3.8/ofxclient/institution.py0000644000076500000240000001425712124252347022073 0ustar davidbartlestaff00000000000000import StringIO import hashlib from ofxclient.client import Client from ofxparse import OfxParser from BeautifulSoup import BeautifulStoneSoup class Institution(object): """Represents an institution or bank :param id: FI Id :type id: string :param org: FI Org :type org: string :param url: FI Url :type url: string :param username: Customer username or member id :type username: string :param password: Customer password or PIN :type password: string :param broker_id: FI Broker ID (optional) :type broker_id: string :param description: Description of the bank (optional) :type description: string or None :param client_args: :py:class:`ofxclient.Client` kwargs (optional) :type client_args: dict Values for many of the parameters need to come from some sort of OFX registry which knows about each banks particular setup. For help obtaining this sort of information; please see the :py:mod:`ofxhome` python module and/or the `OFX Home `_ website. Example:: from ofxclient import Institution inst = Institution( id = '3101', org = 'AMEX', url = 'https://online.americanexpress.com/myca\ /ofxdl/desktop/desktop Download.do?\ request_type=nl_ofxdownload', username = 'gene', password = 'wilder' ) for a in inst.accounts(): print a.statement(days=5).balance """ def __init__(self, id, org, url, username, password, broker_id='', description=None, client_args={}): self.id = id self.org = org self.url = url self.broker_id = broker_id self.username = username self.password = password self.description = description or self._default_description() self.client_args = client_args def client(self): """Build a :py:class:`ofxclient.Client` for talking with the bank It implicitly passes in the ``client_args`` that were passed when instantiating this ``Institution``. :rtype: :py:class:`ofxclient.Client` """ return Client(institution=self, **self.client_args) def local_id(self): """Locally generated unique account identifier. :rtype: string """ return hashlib.sha256("%s%s" % ( self.id, self.username)).hexdigest() def _default_description(self): return self.org def authenticate(self, username=None, password=None): """Test the authentication credentials Raises a ``ValueError`` if there is a problem authenticating with the human readable reason given by the institution. :param username: optional username (use self.username by default) :type username: string or None :param password: optional password (use self.password by default) :type password: string or None """ u = self.username p = self.password if username and password: u = username p = password client = self.client() query = client.authenticated_query(username=u, password=p) res = client.post(query) ofx = BeautifulStoneSoup(res) sonrs = ofx.find('sonrs') code = int(sonrs.find('code').contents[0].strip()) try: status = sonrs.find('message').contents[0].strip() except Exception: status = '' if code == 0: return 1 raise ValueError(status) def accounts(self): """Ask the bank for the known :py:class:`ofxclient.Account` list. :rtype: list of :py:class:`ofxclient.Account` objects """ from ofxclient.account import Account client = self.client() query = client.account_list_query() resp = client.post(query) resp_handle = StringIO.StringIO(resp) parsed = OfxParser.parse(resp_handle) return [Account.from_ofxparse(a, institution=self) for a in parsed.accounts] def serialize(self): """Serialize predictably for use in configuration storage. Output looks like this:: { 'local_id': 'unique local identifier', 'id': 'FI Id', 'org': 'FI Org', 'url': 'FI OFX Endpoint Url', 'broker_id': 'FI Broker Id', 'username': 'Customer username', 'password': 'Customer password', 'description': 'descr', 'client_args': { 'id': 'random client id - see Client() for default', 'app_id': 'app name - see Client() for default', 'app_version': 'app version - see Client() for default', 'ofx_version': 'ofx version - see Client() for default', } } :rtype: nested dictionary """ client = self.client() client_args = { 'id': client.id, 'app_id': client.app_id, 'app_version': client.app_version, 'ofx_version': client.ofx_version, } return { 'id': self.id, 'org': self.org, 'url': self.url, 'broker_id': self.broker_id, 'username': self.username, 'password': self.password, 'description': self.description, 'client_args': client_args, 'local_id': self.local_id() } @staticmethod def deserialize(raw): """Instantiate :py:class:`ofxclient.Institution` from dictionary :param raw: serialized ``Institution`` :param type: dict per :py:method:`~Institution.serialize` :rtype: subclass of :py:class:`ofxclient.Institution` """ return Institution( id=raw['id'], org=raw['org'], url=raw['url'], broker_id=raw.get('broker_id', ''), username=raw['username'], password=raw['password'], description=raw.get('description', None), client_args=raw.get('client_args', {}) ) ofxclient-1.3.8/ofxclient/util.py0000644000076500000240000000117712124251647020456 0ustar davidbartlestaff00000000000000from ofxclient.client import Client from StringIO import StringIO def combined_download(accounts, days=60): """Download OFX files and combine them into one It expects an 'accounts' list of ofxclient.Account objects as well as an optional 'days' specifier which defaults to 60 """ client = Client(institution=None) out_file = StringIO() out_file.write(client.header()) for a in accounts: ofx = a.download(days=days).read() stripped = ofx.partition('')[2].partition('')[0] out_file.write(stripped) out_file.write("") out_file.seek(0) return out_file ofxclient-1.3.8/ofxclient/version.py0000644000076500000240000000002612124276363021160 0ustar davidbartlestaff00000000000000__version__ = '1.3.8' ofxclient-1.3.8/ofxclient.egg-info/0000755000076500000240000000000012124303072020601 5ustar davidbartlestaff00000000000000ofxclient-1.3.8/ofxclient.egg-info/dependency_links.txt0000644000076500000240000000000112124303040024642 0ustar davidbartlestaff00000000000000 ofxclient-1.3.8/ofxclient.egg-info/entry_points.txt0000644000076500000240000000006112124303040024067 0ustar davidbartlestaff00000000000000[console_scripts] ofxclient = ofxclient.cli:run ofxclient-1.3.8/ofxclient.egg-info/not-zip-safe0000644000076500000240000000000112122426325023034 0ustar davidbartlestaff00000000000000 ofxclient-1.3.8/ofxclient.egg-info/PKG-INFO0000644000076500000240000000205112124303040021667 0ustar davidbartlestaff00000000000000Metadata-Version: 1.0 Name: ofxclient Version: 1.3.8 Summary: OFX client for dowloading transactions from banks Home-page: https://github.com/captin411/ofxclient Author: David Bartle Author-email: captindave@gmail.com License: MIT License Description: Overview ========= Simple ofxclient command line utility and OFX client libraries for development. Full Documentation ================== http://captin411.github.com/ofxclient/ Quick Start =========== > $ ofxclient --download combined.ofx --open Keywords: ofx,Open Financial Exchange,download transactions Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Classifier: License :: OSI Approved :: MIT License ofxclient-1.3.8/ofxclient.egg-info/requires.txt0000644000076500000240000000007012124303040023171 0ustar davidbartlestaff00000000000000argparse keyring ofxhome ofxparse>0.8 BeautifulSoup>=3.0ofxclient-1.3.8/ofxclient.egg-info/SOURCES.txt0000644000076500000240000000067312124303040022466 0ustar davidbartlestaff00000000000000MANIFEST.in README README.md setup.cfg setup.py ofxclient/__init__.py ofxclient/account.py ofxclient/cli.py ofxclient/client.py ofxclient/config.py ofxclient/institution.py ofxclient/util.py ofxclient/version.py ofxclient.egg-info/PKG-INFO ofxclient.egg-info/SOURCES.txt ofxclient.egg-info/dependency_links.txt ofxclient.egg-info/entry_points.txt ofxclient.egg-info/not-zip-safe ofxclient.egg-info/requires.txt ofxclient.egg-info/top_level.txtofxclient-1.3.8/ofxclient.egg-info/top_level.txt0000644000076500000240000000001212124303040023317 0ustar davidbartlestaff00000000000000ofxclient ofxclient-1.3.8/PKG-INFO0000644000076500000240000000205112124303072016207 0ustar davidbartlestaff00000000000000Metadata-Version: 1.0 Name: ofxclient Version: 1.3.8 Summary: OFX client for dowloading transactions from banks Home-page: https://github.com/captin411/ofxclient Author: David Bartle Author-email: captindave@gmail.com License: MIT License Description: Overview ========= Simple ofxclient command line utility and OFX client libraries for development. Full Documentation ================== http://captin411.github.com/ofxclient/ Quick Start =========== > $ ofxclient --download combined.ofx --open Keywords: ofx,Open Financial Exchange,download transactions Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Classifier: License :: OSI Approved :: MIT License ofxclient-1.3.8/README0000644000076500000240000000037212124276652016013 0ustar davidbartlestaff00000000000000Overview ========= Simple ofxclient command line utility and OFX client libraries for development. Full Documentation ================== http://captin411.github.com/ofxclient/ Quick Start =========== > $ ofxclient --download combined.ofx --open ofxclient-1.3.8/README.md0000644000076500000240000000000012124276652021715 1ofxclient-1.3.8/READMEustar davidbartlestaff00000000000000ofxclient-1.3.8/setup.cfg0000644000076500000240000000016012124303072016732 0ustar davidbartlestaff00000000000000[egg_info] tag_svn_revision = 0 tag_build = tag_date = 0 [aliases] release = register sdist bdist_egg upload ofxclient-1.3.8/setup.py0000644000076500000240000000311512124276507016642 0ustar davidbartlestaff00000000000000from setuptools import setup, find_packages import os import re VERSIONFILE = "ofxclient/version.py" verstrline = open(VERSIONFILE, "rt").read() VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" mo = re.search(VSRE, verstrline, re.M) if mo: verstr = mo.group(1) else: raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,)) setup(name='ofxclient', version=verstr, description="OFX client for dowloading transactions from banks", long_description=open("./README.md", "r").read(), classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Utilities", "License :: OSI Approved :: MIT License", ], keywords='ofx, Open Financial Exchange, download transactions', author='David Bartle', author_email='captindave@gmail.com', url='https://github.com/captin411/ofxclient', license='MIT License', packages=find_packages(exclude=[ 'ez_setup', 'example', 'tests', 'external']), include_package_data=True, zip_safe=False, entry_points={ 'console_scripts': [ 'ofxclient = ofxclient.cli:run' ] }, install_requires=[ "argparse", "keyring", "ofxhome", "ofxparse>0.8", "BeautifulSoup>=3.0", ], test_suite='tests', )