Source code for ofxclient.account

from __future__ import absolute_import
from __future__ import unicode_literals
import datetime
import hashlib
try:
    # python 3
    from io import StringIO
except ImportError:
    # python 2
    from StringIO import StringIO
import time

from ofxparse import OfxParser, AccountType


[docs]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` """
[docs] 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)).encode()).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` """ 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(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)
[docs]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` """
[docs] 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
[docs]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` """
[docs] 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
[docs]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` """
[docs] 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