diff options
author | Andrew Shadura <andrewsh@debian.org> | 2016-10-19 17:56:53 +0200 |
---|---|---|
committer | Andrew Shadura <andrewsh@debian.org> | 2016-10-19 17:56:53 +0200 |
commit | bdbd753fe4ab2de979939bae4caf93b16f6b5efc (patch) | |
tree | 5035feeb558cb1941364624600b21e275f29681c /ofxparse/ofxparse.py |
Imported Upstream version 0.14
Diffstat (limited to 'ofxparse/ofxparse.py')
-rw-r--r-- | ofxparse/ofxparse.py | 851 |
1 files changed, 851 insertions, 0 deletions
diff --git a/ofxparse/ofxparse.py b/ofxparse/ofxparse.py new file mode 100644 index 0000000..a66df88 --- /dev/null +++ b/ofxparse/ofxparse.py @@ -0,0 +1,851 @@ +from __future__ import absolute_import, with_statement + +import sys +import decimal +import datetime +import codecs +import re +import collections +import contextlib + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +import six + +if 'OrderedDict' in dir(collections): + odict = collections +else: + import ordereddict as odict + +from . import mcc + +def soup_maker(fh): + try: + from bs4 import BeautifulSoup + return BeautifulSoup(fh) + except ImportError: + from BeautifulSoup import BeautifulStoneSoup + return BeautifulStoneSoup(fh) + + +def try_decode(string, encoding): + if hasattr(string, 'decode'): + string = string.decode(encoding) + return string + +def is_iterable(candidate): + if sys.version_info < (2,6): + return hasattr(candidate, 'next') + return isinstance(candidate, collections.Iterable) + +@contextlib.contextmanager +def save_pos(fh): + """ + Save the position of the file handle, seek to the beginning, and + then restore the position. + """ + orig_pos = fh.tell() + fh.seek(0) + try: + yield fh + finally: + fh.seek(orig_pos) + +class OfxFile(object): + def __init__(self, fh): + """ + fh should be a seekable file-like byte stream object + """ + self.headers = odict.OrderedDict() + self.fh = fh + + if not is_iterable(self.fh): + return + if not hasattr(self.fh, "seek"): + return # fh is not a file object, we're doomed. + + with save_pos(self.fh): + self.read_headers() + self.handle_encoding() + self.replace_NONE_headers() + + def read_headers(self): + head_data = self.fh.read(1024 * 10) + head_data = head_data[:head_data.find(six.b('<'))] + + for line in re.split(six.b('\r?\n?'), head_data): + # Newline? + if line.strip() == six.b(""): + break + + header, value = line.split(six.b(":")) + header, value = header.strip().upper(), value.strip() + + self.headers[header] = value + + def handle_encoding(self): + """ + Decode the headers and wrap self.fh in a decoder such that it + subsequently returns only text. + """ + # decode the headers using ascii + ascii_headers = odict.OrderedDict( + ( + key.decode('ascii', 'replace'), + value.decode('ascii', 'replace'), + ) + for key, value in six.iteritems(self.headers) + ) + + enc_type = ascii_headers.get('ENCODING') + + if not enc_type: + # no encoding specified, use the ascii-decoded headers + self.headers = ascii_headers + # decode the body as ascii as well + self.fh = codecs.lookup('ascii').streamreader(self.fh) + return + + if enc_type == "USASCII": + cp = ascii_headers.get("CHARSET", "1252") + encoding = "cp%s" % (cp, ) + + elif enc_type in ("UNICODE", "UTF-8"): + encoding = "utf-8" + + codec = codecs.lookup(encoding) + + self.fh = codec.streamreader(self.fh) + + # Decode the headers using the encoding + self.headers = odict.OrderedDict( + (key.decode(encoding), value.decode(encoding)) + for key, value in six.iteritems(self.headers) + ) + + def replace_NONE_headers(self): + """ + Any headers that indicate 'none' should be replaced with Python + None values + """ + for header in self.headers: + if self.headers[header].upper() == 'NONE': + self.headers[header] = None + + +class OfxPreprocessedFile(OfxFile): + def __init__(self, fh): + super(OfxPreprocessedFile,self).__init__(fh) + + if self.fh is None: + return + + ofx_string = self.fh.read() + + # find all closing tags as hints + closing_tags = [ t.upper() for t in re.findall(r'(?i)</([a-z0-9_\.]+)>', ofx_string) ] + + # close all tags that don't have closing tags and + # leave all other data intact + last_open_tag = None + tokens = re.split(r'(?i)(</?[a-z0-9_\.]+>)', ofx_string) + new_fh = StringIO() + for idx,token in enumerate(tokens): + is_closing_tag = token.startswith('</') + is_processing_tag = token.startswith('<?') + is_cdata = token.startswith('<!') + is_tag = token.startswith('<') and not is_cdata + is_open_tag = is_tag and not is_closing_tag and not is_processing_tag + if is_tag: + if last_open_tag is not None: + new_fh.write("</%s>" % last_open_tag) + last_open_tag = None + if is_open_tag: + tag_name = re.findall(r'(?i)<([a-z0-9_\.]+)>', token)[0] + if tag_name.upper() not in closing_tags: + last_open_tag = tag_name + new_fh.write(token) + new_fh.seek(0) + self.fh = new_fh + + +class Ofx(object): + def __str__(self): + return "" +# headers = "\r\n".join(":".join(el if el else "NONE" for el in item) for item in six.iteritems(self.headers)) +# headers += "\r\n\r\n" +# +# return headers + str(self.signon) + + +class AccountType(object): + (Unknown, Bank, CreditCard, Investment) = range(0, 4) + + +class Account(object): + def __init__(self): + self.statement = None + self.account_id = '' + self.routing_number = '' + self.branch_id = '' + self.account_type = '' + self.institution = None + self.type = AccountType.Unknown + # Used for error tracking + self.warnings = [] + + @property + def number(self): + # For backwards compatibility. Remove in version 1.0. + return self.account_id + + +class InvestmentAccount(Account): + def __init__(self): + super(InvestmentAccount, self).__init__() + self.brokerid = '' + + +class Security: + def __init__(self, uniqueid, name, ticker, memo): + self.uniqueid = uniqueid + self.name = name + self.ticker = ticker + self.memo = memo + +class Signon: + def __init__(self, code, severity, message): + self.code = code + self.severity = severity + self.message = message + if int(code) == 0: + self.success = True + else: + self.success = False + + def __str__(self): + ret = "\t<SIGNONMSGSRSV1>\r\n" + "\t\t<SONRS>\r\n" + "\t\t\t<STATUS>\r\n" + ret += "\t\t\t\t<CODE>%s\r\n" % self.code + ret += "\t\t\t\t<SEVERITY>%s\r\n" % self.severity + if self.message: + ret += "\t\t\t\t<MESSAGE>%s\r\n" % self.message + ret += "\t\t\t</STATUS>\r\n" + "\t\t</SONRS>\r\n" + "\t</SIGNONMSGSRSV1>\r\n" + return ret + +class Statement(object): + def __init__(self): + self.start_date = '' + self.end_date = '' + self.currency = '' + self.transactions = [] + # Error tracking: + self.discarded_entries = [] + self.warnings = [] + + +class InvestmentStatement(object): + def __init__(self): + self.positions = [] + self.transactions = [] + # Error tracking: + self.discarded_entries = [] + self.warnings = [] + + +class Transaction(object): + def __init__(self): + self.payee = '' + self.type = '' + self.date = None + self.amount = None + self.id = '' + self.memo = '' + self.sic = None + self.mcc = '' + self.checknum = '' + + def __repr__(self): + return "<Transaction units=" + str(self.amount) + ">" + + +class InvestmentTransaction(object): + (Unknown, BuyMF, SellMF, Reinvest, BuyStock, SellStock) = [x for x in range(-1, 5)] + def __init__(self, type): + try: + self.type = ['buymf', 'sellmf', 'reinvest', 'buystock', 'sellstock'].index(type.lower()) + except ValueError: + self.type = InvestmentTransaction.Unknown + self.tradeDate = None + self.settleDate = None + self.security = '' + self.units = decimal.Decimal(0) + self.unit_price = decimal.Decimal(0) + + def __repr__(self): + return "<InvestmentTransaction type=" + str(self.type) + ", units=" + str(self.units) + ">" + + +class Position(object): + def __init__(self): + self.security = '' + self.units = decimal.Decimal(0) + self.unit_price = decimal.Decimal(0) + + +class Institution(object): + def __init__(self): + self.organization = '' + self.fid = '' + + +class OfxParserException(Exception): + pass + + +class OfxParser(object): + @classmethod + def parse(cls_, file_handle, fail_fast=True): + ''' + parse is the main entry point for an OfxParser. It takes a file + handle and an optional log_errors flag. + + If fail_fast is True, the parser will fail on any errors. + If fail_fast is False, the parser will log poor statements in the + statement class and continue to run. Note: the library does not + guarantee that no exceptions will be raised to the caller, only + that statements will include bad transactions (which are marked). + + ''' + cls_.fail_fast = fail_fast + + if isinstance(file_handle, type('')): + raise RuntimeError(six.u("parse() takes in a file handle, not a string")) + + ofx_obj = Ofx() + + # Store the headers + ofx_file = OfxPreprocessedFile(file_handle) + ofx_obj.headers = ofx_file.headers + ofx_obj.accounts = [] + ofx_obj.signon = None + + ofx = soup_maker(ofx_file.fh) + if len(ofx.contents) == 0: + raise OfxParserException('The ofx file is empty!') + + sonrs_ofx = ofx.find('sonrs') + if sonrs_ofx: + ofx_obj.signon = cls_.parseSonrs(sonrs_ofx) + + stmtrs_ofx = ofx.findAll('stmtrs') + if stmtrs_ofx: + ofx_obj.accounts += cls_.parseStmtrs(stmtrs_ofx, AccountType.Bank) + + ccstmtrs_ofx = ofx.findAll('ccstmtrs') + if ccstmtrs_ofx: + ofx_obj.accounts += cls_.parseStmtrs( + ccstmtrs_ofx, AccountType.CreditCard) + + invstmtrs_ofx = ofx.findAll('invstmtrs') + if invstmtrs_ofx: + ofx_obj.accounts += cls_.parseInvstmtrs(invstmtrs_ofx) + seclist_ofx = ofx.find('seclist') + if seclist_ofx: + ofx_obj.security_list = cls_.parseSeclist(seclist_ofx) + else: + ofx_obj.security_list = None + + acctinfors_ofx = ofx.find('acctinfors') + if acctinfors_ofx: + ofx_obj.accounts += cls_.parseAcctinfors(acctinfors_ofx, ofx) + + fi_ofx = ofx.find('fi') + if fi_ofx: + for account in ofx_obj.accounts: + account.institution = cls_.parseOrg(fi_ofx) + + if ofx_obj.accounts: + ofx_obj.account = ofx_obj.accounts[0] + + return ofx_obj + + @classmethod + def parseOfxDateTime(cls_, ofxDateTime): + # dateAsString looks something like 20101106160000.00[-5:EST] + # for 6 Nov 2010 4pm UTC-5 aka EST + + # Some places (e.g. Newfoundland) have non-integer offsets. + res = re.search("\[(?P<tz>[-+]?\d+\.?\d*)\:\w*\]$", ofxDateTime) + if res: + tz = float(res.group('tz')) + else: + tz = 0 + + timeZoneOffset = datetime.timedelta(hours=tz) + + try: + local_date = datetime.datetime.strptime( + ofxDateTime[:14], '%Y%m%d%H%M%S' + ) + return local_date - timeZoneOffset + except: + return datetime.datetime.strptime( + ofxDateTime[:8], '%Y%m%d') - timeZoneOffset + + @classmethod + def parseAcctinfors(cls_, acctinfors_ofx, ofx): + all_accounts = [] + for i in acctinfors_ofx.findAll('acctinfo'): + accounts = [] + if i.find('invacctinfo'): + accounts += cls_.parseInvstmtrs([i]) + elif i.find('ccacctinfo'): + accounts += cls_.parseStmtrs([i], AccountType.CreditCard) + elif i.find('bankacctinfo'): + accounts += cls_.parseStmtrs([i], AccountType.Bank) + else: + continue + + fi_ofx = ofx.find('fi') + if fi_ofx: + for account in ofx_obj.accounts: + account.institution = cls_.parseOrg(fi_ofx) + + desc = i.find('desc') + if hasattr(desc, 'contents'): + for account in accounts: + account.desc = desc.contents[0].strip() + all_accounts += accounts + return all_accounts + + @classmethod + def parseInvstmtrs(cls_, invstmtrs_list): + ret = [] + for invstmtrs_ofx in invstmtrs_list: + account = InvestmentAccount() + acctid_tag = invstmtrs_ofx.find('acctid') + if (hasattr(acctid_tag, 'contents')): + try: + account.account_id = acctid_tag.contents[0].strip() + except IndexError: + account.warnings.append( + six.u("Empty acctid tag for %s") % invstmtrs_ofx) + if cls_.fail_fast: + raise + + brokerid_tag = invstmtrs_ofx.find('brokerid') + if (hasattr(brokerid_tag, 'contents')): + try: + account.brokerid = brokerid_tag.contents[0].strip() + except IndexError: + account.warnings.append( + six.u("Empty brokerid tag for %s") % invstmtrs_ofx) + if cls_.fail_fast: + raise + + account.type = AccountType.Investment + + if (invstmtrs_ofx): + account.statement = cls_.parseInvestmentStatement( + invstmtrs_ofx) + ret.append(account) + return ret + + @classmethod + def parseSeclist(cls_, seclist_ofx): + securityList = [] + for secinfo_ofx in seclist_ofx.findAll('secinfo'): + uniqueid_tag = secinfo_ofx.find('uniqueid') + name_tag = secinfo_ofx.find('secname') + ticker_tag = secinfo_ofx.find('ticker') + memo_tag = secinfo_ofx.find('memo') + if uniqueid_tag and name_tag and ticker_tag: + try: + memo = memo_tag.contents[0].strip() + except AttributeError: + # memo can be empty + memo = None + securityList.append( + Security(uniqueid_tag.contents[0].strip(), + name_tag.contents[0].strip(), + ticker_tag.contents[0].strip(), + memo)) + return securityList + + @classmethod + def parseInvestmentPosition(cls_, ofx): + position = Position() + tag = ofx.find('uniqueid') + if (hasattr(tag, 'contents')): + position.security = tag.contents[0].strip() + tag = ofx.find('units') + if (hasattr(tag, 'contents')): + position.units = decimal.Decimal(tag.contents[0].strip()) + tag = ofx.find('unitprice') + if (hasattr(tag, 'contents')): + position.unit_price = decimal.Decimal(tag.contents[0].strip()) + tag = ofx.find('dtpriceasof') + if (hasattr(tag, 'contents')): + try: + position.date = cls_.parseOfxDateTime(tag.contents[0].strip()) + except ValueError: + raise + return position + + @classmethod + def parseInvestmentTransaction(cls_, ofx): + transaction = InvestmentTransaction(ofx.name) + tag = ofx.find('fitid') + if (hasattr(tag, 'contents')): + transaction.id = tag.contents[0].strip() + tag = ofx.find('memo') + if (hasattr(tag, 'contents')): + transaction.memo = tag.contents[0].strip() + tag = ofx.find('dttrade') + if (hasattr(tag, 'contents')): + try: + transaction.tradeDate = cls_.parseOfxDateTime( + tag.contents[0].strip()) + except ValueError: + raise + tag = ofx.find('dtsettle') + if (hasattr(tag, 'contents')): + try: + transaction.settleDate = cls_.parseOfxDateTime( + tag.contents[0].strip()) + except ValueError: + raise + tag = ofx.find('uniqueid') + if (hasattr(tag, 'contents')): + transaction.security = tag.contents[0].strip() + tag = ofx.find('units') + if (hasattr(tag, 'contents')): + transaction.units = decimal.Decimal(tag.contents[0].strip()) + tag = ofx.find('unitprice') + if (hasattr(tag, 'contents')): + transaction.unit_price = decimal.Decimal(tag.contents[0].strip()) + return transaction + + @classmethod + def parseInvestmentStatement(cls_, invstmtrs_ofx): + statement = InvestmentStatement() + currency_tag = invstmtrs_ofx.find('curdef') + if hasattr(currency_tag, "contents"): + statement.currency = currency_tag.contents[0].strip().lower() + invtranlist_ofx = invstmtrs_ofx.find('invtranlist') + if (invtranlist_ofx is not None): + tag = invtranlist_ofx.find('dtstart') + if (hasattr(tag, 'contents')): + try: + statement.start_date = cls_.parseOfxDateTime( + tag.contents[0].strip()) + except IndexError: + statement.warnings.append(six.u('Empty start date.')) + if cls_.fail_fast: + raise + except ValueError: + e = sys.exc_info()[1] + statement.warnings.append(six.u('Invalid start date: %s') % e) + if cls_.fail_fast: + raise + + tag = invtranlist_ofx.find('dtend') + if (hasattr(tag, 'contents')): + try: + statement.end_date = cls_.parseOfxDateTime( + tag.contents[0].strip()) + except IndexError: + statement.warnings.append(six.u('Empty end date.')) + except ValueError: + e = sys.exc_info()[1] + statement.warnings.append(six.u('Invalid end date: %s') % e) + if cls_.fail_fast: + raise + + for transaction_type in ['posmf', 'posstock', 'posopt']: + try: + for investment_ofx in invstmtrs_ofx.findAll(transaction_type): + statement.positions.append( + cls_.parseInvestmentPosition(investment_ofx)) + except (ValueError, IndexError, decimal.InvalidOperation, + TypeError): + e = sys.exc_info()[1] + if cls_.fail_fast: + raise + statement.discarded_entries.append( + {six.u('error'): six.u("Error parsing positions: ") + str(e), + six.u('content'): investment_ofx} + ) + + for transaction_type in ['buymf', 'sellmf', 'reinvest', 'buystock', + 'sellstock', 'buyopt', 'sellopt']: + try: + for investment_ofx in invstmtrs_ofx.findAll(transaction_type): + statement.transactions.append( + cls_.parseInvestmentTransaction(investment_ofx)) + except (ValueError, IndexError, decimal.InvalidOperation): + e = sys.exc_info()[1] + if cls_.fail_fast: + raise + statement.discarded_entries.append( + {six.u('error'): transaction_type + ": " + str(e), + six.u('content'): investment_ofx} + ) + + return statement + + @classmethod + def parseOrg(cls_, fi_ofx): + institution = Institution() + org = fi_ofx.find('org') + if hasattr(org, 'contents'): + institution.organization = org.contents[0].strip() + + fid = fi_ofx.find('fid') + if hasattr(fid, 'contents'): + institution.fid = fid.contents[0].strip() + + return institution + + @classmethod + def parseSonrs(cls_, sonrs): + + code = int(sonrs.find('code').contents[0].strip()) + severity = sonrs.find('severity').contents[0].strip() + try: + message = sonrs.find('message').contents[0].strip() + except: + message = '' + + return Signon(code,severity,message) + + @classmethod + def parseStmtrs(cls_, stmtrs_list, accountType): + ''' Parse the <STMTRS> tags and return a list of Accounts object. ''' + ret = [] + for stmtrs_ofx in stmtrs_list: + account = Account() + acctid_tag = stmtrs_ofx.find('acctid') + if hasattr(acctid_tag, 'contents'): + account.account_id = acctid_tag.contents[0].strip() + bankid_tag = stmtrs_ofx.find('bankid') + if hasattr(bankid_tag, 'contents'): + account.routing_number = bankid_tag.contents[0].strip() + branchid_tag = stmtrs_ofx.find('branchid') + if hasattr(branchid_tag, 'contents'): + account.branch_id = branchid_tag.contents[0].strip() + type_tag = stmtrs_ofx.find('accttype') + if hasattr(type_tag, 'contents'): + account.account_type = type_tag.contents[0].strip() + account.type = accountType + + if stmtrs_ofx: + account.statement = cls_.parseStatement(stmtrs_ofx) + ret.append(account) + return ret + + @classmethod + def parseStatement(cls_, stmt_ofx): + ''' + Parse a statement in ofx-land and return a Statement object. + ''' + statement = Statement() + dtstart_tag = stmt_ofx.find('dtstart') + if hasattr(dtstart_tag, "contents"): + try: + statement.start_date = cls_.parseOfxDateTime( + dtstart_tag.contents[0].strip()) + except IndexError: + statement.warnings.append( + six.u("Statement start date was empty for %s") % stmt_ofx) + if cls_.fail_fast: + raise + except ValueError: + statement.warnings.append( + six.u("Statement start date was not allowed for %s") % stmt_ofx) + if cls_.fail_fast: + raise + + dtend_tag = stmt_ofx.find('dtend') + if hasattr(dtend_tag, "contents"): + try: + statement.end_date = cls_.parseOfxDateTime( + dtend_tag.contents[0].strip()) + except IndexError: + statement.warnings.append( + six.u("Statement start date was empty for %s") % stmt_ofx) + if cls_.fail_fast: + raise + except ValueError: + ve = sys.exc_info()[1] + msg = six.u("Statement start date was not formatted " + "correctly for %s") + statement.warnings.append(msg % stmt_ofx) + if cls_.fail_fast: + raise + except TypeError: + statement.warnings.append( + six.u("Statement start date was not allowed for %s") % stmt_ofx) + if cls_.fail_fast: + raise + + currency_tag = stmt_ofx.find('curdef') + if hasattr(currency_tag, "contents"): + try: + statement.currency = currency_tag.contents[0].strip().lower() + except IndexError: + statement.warnings.append( + six.u("Currency definition was empty for %s") % stmt_ofx) + if cls_.fail_fast: + raise + + ledger_bal_tag = stmt_ofx.find('ledgerbal') + if hasattr(ledger_bal_tag, "contents"): + balamt_tag = ledger_bal_tag.find('balamt') + if hasattr(balamt_tag, "contents"): + try: + statement.balance = decimal.Decimal( + balamt_tag.contents[0].strip()) + except (IndexError, decimal.InvalidOperation): + ex = sys.exc_info()[1] + statement.warnings.append( + six.u("Ledger balance amount was empty for %s") % stmt_ofx) + if cls_.fail_fast: + raise OfxParserException("Empty ledger balance") + + avail_bal_tag = stmt_ofx.find('availbal') + if hasattr(avail_bal_tag, "contents"): + balamt_tag = avail_bal_tag.find('balamt') + if hasattr(balamt_tag, "contents"): + try: + statement.available_balance = decimal.Decimal( + balamt_tag.contents[0].strip()) + except (IndexError, decimal.InvalidOperation): + ex = sys.exc_info()[1] + msg = six.u("Available balance amount was empty for %s") + statement.warnings.append(msg % stmt_ofx) + if cls_.fail_fast: + raise OfxParserException("Empty available balance") + + for transaction_ofx in stmt_ofx.findAll('stmttrn'): + try: + statement.transactions.append( + cls_.parseTransaction(transaction_ofx)) + except OfxParserException: + ofxError = sys.exc_info()[1] + statement.discarded_entries.append( + {'error': str(ofxError), 'content': transaction_ofx}) + if cls_.fail_fast: + raise + + return statement + + @classmethod + def parseTransaction(cls_, txn_ofx): + ''' + Parse a transaction in ofx-land and return a Transaction object. + ''' + transaction = Transaction() + + type_tag = txn_ofx.find('trntype') + if hasattr(type_tag, 'contents'): + try: + transaction.type = type_tag.contents[0].lower().strip() + except IndexError: + raise OfxParserException(six.u("Empty transaction type")) + except TypeError: + raise OfxParserException( + six.u("No Transaction type (a required field)")) + + name_tag = txn_ofx.find('name') + if hasattr(name_tag, "contents"): + try: + transaction.payee = name_tag.contents[0].strip() + except IndexError: + raise OfxParserException(six.u("Empty transaction name")) + except TypeError: + raise OfxParserException( + six.u("No Transaction name (a required field)")) + + memo_tag = txn_ofx.find('memo') + if hasattr(memo_tag, "contents"): + try: + transaction.memo = memo_tag.contents[0].strip() + except IndexError: + # Memo can be empty. + pass + except TypeError: + pass + + amt_tag = txn_ofx.find('trnamt') + if hasattr(amt_tag, "contents"): + try: + transaction.amount = decimal.Decimal( + amt_tag.contents[0].strip()) + except IndexError: + raise OfxParserException("Invalid Transaction Date") + except decimal.InvalidOperation: + raise OfxParserException( + six.u("Invalid Transaction Amount: '%s'") % amt_tag.contents[0]) + except TypeError: + raise OfxParserException( + six.u("No Transaction Amount (a required field)")) + else: + raise OfxParserException( + six.u("Missing Transaction Amount (a required field)")) + + date_tag = txn_ofx.find('dtposted') + if hasattr(date_tag, "contents"): + try: + transaction.date = cls_.parseOfxDateTime( + date_tag.contents[0].strip()) + except IndexError: + raise OfxParserException("Invalid Transaction Date") + except ValueError: + ve = sys.exc_info()[1] + raise OfxParserException(str(ve)) + except TypeError: + raise OfxParserException( + six.u("No Transaction Date (a required field)")) + else: + raise OfxParserException( + six.u("Missing Transaction Date (a required field)")) + + id_tag = txn_ofx.find('fitid') + if hasattr(id_tag, "contents"): + try: + transaction.id = id_tag.contents[0].strip() + except IndexError: + raise OfxParserException(six.u("Empty FIT id (a required field)")) + except TypeError: + raise OfxParserException(six.u("No FIT id (a required field)")) + else: + raise OfxParserException(six.u("Missing FIT id (a required field)")) + + sic_tag = txn_ofx.find('sic') + if hasattr(sic_tag, 'contents'): + try: + transaction.sic = sic_tag.contents[0].strip() + except IndexError: + raise OfxParserException(six.u("Empty transaction Standard Industry Code (SIC)")) + + if transaction.sic is not None and transaction.sic in mcc.codes: + try: + transaction.mcc = mcc.codes.get(transaction.sic, '').get('combined description') + except IndexError: + raise OfxParserException(six.u("Empty transaction Merchant Category Code (MCC)")) + except AttributeError: + if cls._fail_fast: + raise + + checknum_tag = txn_ofx.find('checknum') + if hasattr(checknum_tag, 'contents'): + try: + transaction.checknum = checknum_tag.contents[0].strip() + except IndexError: + raise OfxParserException(six.u("Empty Check (or other reference) number")) + + return transaction |