class STMTTRN(Aggregate, Origcurrency): """ OFX section 11.4.3 """ trntype = OneOf(*TRNTYPES, required=True) dtposted = DateTime(required=True) dtuser = DateTime() dtavail = DateTime() trnamt = Decimal(required=True) fitid = String(255, required=True) correctfitid = String(255) correctaction = OneOf("REPLACE", "DELETE") srvrtid = String(10) checknum = String(12) refnum = String(32) sic = Integer() payeeid = String(12) name = String(32) payee = SubAggregate(PAYEE) extdname = String(100) bankacctto = SubAggregate(BANKACCTTO) ccacctto = SubAggregate(CCACCTTO) memo = String(255) imagedata = Unsupported() currency = SubAggregate(CURRENCY) origcurrency = SubAggregate(ORIGCURRENCY) inv401ksource = OneOf(*INV401KSOURCES) optionalMutexes = [ ("name", "payee"), ("ccacctto", "bankacctto"), ("currency", "origcurrency"), ]
def __init__(self, tranlist): # Initialize with *TRANLIST Element dtstart, dtend = tranlist[0:2] tranlist = tranlist[2:] self.dtstart = DateTime().convert(dtstart.text) self.dtend = DateTime().convert(dtend.text) self.extend([Aggregate.from_etree(tran) for tran in tranlist])
class SONRS(Aggregate): """ OFX section 2.5.1.6 """ status = SubAggregate(STATUS, required=True) dtserver = DateTime(required=True) userkey = String(64) tskeyexpire = DateTime() language = OneOf(*LANG_CODES, required=True) dtprofup = DateTime() dtacctup = DateTime() fi = SubAggregate(FI) sesscookie = String(1000) accesskey = String(1000) ofxextension = Unsupported() @staticmethod def groom(elem): """ Remove proprietary tags e.g. INTU.XXX """ # Keep input free of side effects elem = deepcopy(elem) for child in set(elem): if "." in child.tag: elem.remove(child) return super(SONRS, SONRS).groom(elem) # Human-friendly attribute aliases @property def org(self): return self.fi.org @property def fid(self): return self.fi.fid
class PROCDET_V100(Aggregate): """OFX tax extensions section 2.2.11.2""" form8949code = String(1) dtaqd = DateTime() dtvar = Bool() dtsale = DateTime(required=True) secname = String(120) saledescription = String(120) numshrs = Decimal() costbasis = Decimal() salespr = Decimal(required=True) accruedmktdiscount = Decimal() longshort = OneOf("LONG", "SHORT") ordinary = Bool() washsale = Bool() fedtaxwh = Decimal() washsalelossdisallowed = Decimal() noncoveredsecurity = Bool() lossnotallowed = Bool() basisnotshown = Bool() form1099bnotreceived = Bool() collectible = Bool() statecode = String(2) stateidnum = String(32) statetaxwheld = Decimal() statecode2 = String(2) stateidnum2 = String(32) statetaxwheld2 = Decimal() fatca = Bool() requiredMutexes = [["dtaqd", "dtvar"]]
class CCSTMTENDRQ(Aggregate): """ OFX section 11.5.3 """ ccacctfrom = SubAggregate(CCACCTFROM, required=True) dtstart = DateTime() dtend = DateTime() incstmtimg = Bool()
class BANKTRANLIST(TranList): """ OFX section 11.4.2.2 """ dtstart = DateTime(required=True) dtend = DateTime(required=True) memberTags = [ 'STMTTRN', ]
class INVTRAN(Aggregate): """ OFX section 13.9.2.4.2 """ fitid = String(255, required=True) srvrtid = String(10) dttrade = DateTime(required=True) dtsettle = DateTime() reversalfitid = String(255) memo = String(255)
class SONRS(FI, STATUS): dtserver = DateTime(required=True) userkey = String(64) tskeyexpire = DateTime() language = OneOf(*LANG_CODES) dtprofup = DateTime() dtacctup = DateTime() sesscookie = String(1000) accesskey = String(1000)
def inctran(self, inctran, dtstart, dtend): """ """ tran = ET.Element('INCTRAN') if dtstart: ET.SubElement(tran, 'DTSTART').text = DateTime().unconvert(dtstart) if dtend: ET.SubElement(tran, 'DTEND').text = DateTime().unconvert(dtend) ET.SubElement(tran, 'INCLUDE').text = Bool().unconvert(inctran) return tran
class INTRARS(Aggregate): """ OFX section 11.7.1.2 """ curdef = OneOf(*CURRENCY_CODES, required=True) srvrtid = String(10, required=True) xferinfo = SubAggregate(XFERINFO, required=True) dtxferprj = DateTime() dtposted = DateTime() recsrvrtid = String(10) xferprcsts = SubAggregate(XFERPRCSTS) optionalMutexes = [("dtxferprj", "dtposted")]
class TranList(Aggregate): """ Base class for OFX *TRANLIST Cf. OFX section 3.2.7 """ dtstart = DateTime(required=True) dtend = DateTime(required=True) def __repr__(self) -> str: return "<{} dtstart='{}' dtend='{}' len={}>".format( self.__class__.__name__, self.dtstart, self.dtend, len(self))
class TranList(List): """ Base class for OFX *TRANLIST """ dtstart = DateTime(required=True) dtend = DateTime(required=True) def __init__(self, dtstart, dtend, *members): self.dtstart = dtstart self.dtend = dtend super(TranList, self).__init__(*members) def __repr__(self): return "<{} dtstart='{}' dtend='{}' len={}>".format( self.__class__.__name__, self.dtstart, self.dtend, len(self))
class DEBTINFO(SECINFO): parvalue = Decimal(required=True) debttype = OneOf('COUPON', 'ZERO', required=True) debtclass = OneOf('TREASURY', 'MUNICIPAL', 'CORPORATE', 'OTHER') couponrt = Decimal(4) dtcoupon = DateTime() couponfreq = OneOf('MONTHLY', 'QUARTERLY', 'SEMIANNUAL', 'ANNUAL', 'OTHER') callprice = Decimal(4) yieldtocall = Decimal(4) dtcall = DateTime() calltype = OneOf('CALL', 'PUT', 'PREFUND', 'MATURITY') yieldtomat = Decimal(4) dtmat = DateTime() assetclass = OneOf(*ASSETCLASSES) fiassetclass = String(32)
class PROFRS(Aggregate): """ OFX section 7.2 """ msgsetlist = SubAggregate(MSGSETLIST, required=True) signoninfolist = SubAggregate(SIGNONINFOLIST, required=True) dtprofup = DateTime(required=True) finame = String(32, required=True) addr1 = String(32, required=True) addr2 = String(32) addr3 = String(32) city = String(32, required=True) state = String(5, required=True) postalcode = String(11, required=True) country = OneOf(*COUNTRY_CODES, required=True) csphone = String(32) tsphone = String(32) faxphone = String(32) url = String(255) email = String(80) @staticmethod def groom(elem): """ Remove proprietary tags e.g. INTU.XXX """ # Keep input free of side effects elem = deepcopy(elem) for child in set(elem): if "." in child.tag: elem.remove(child) return super(PROFRS, PROFRS).groom(elem)
class OPTINFO(SECINFO): opttype = OneOf('CALL', 'PUT', required=True) strikeprice = Decimal(required=True) dtexpire = DateTime(required=True) shperctrct = Integer(required=True) assetclass = OneOf(*ASSETCLASSES) fiassetclass = String(32) def __init__(self, elem): """ Strip SECID of underlying so it doesn't overwrite SECID of option during _flatten() """ # Do all XPath searches before removing nodes from the tree # which seems to mess up the DOM in Python3 and throw an # AttributeError on subsequent searches. secid = elem.find('./SECID') if secid is not None: # A <SECID> aggregate referring to the security underlying the # option is, in general, *not* going to be contained in <SECLIST> # (because you don't necessarily have a position in the underlying). # Since the <SECID> for the underlying only gives us fields for # (uniqueidtype, uniqueid) we can't really go ahead and use this # information to create a corresponding SECINFO instance (since we # lack information about the security subclass). It's unclear that # the SECID of the underlying is really needed for anything, so we # disregard it. elem.remove(secid) super(OPTINFO, self).__init__(elem)
class STOCKINFO(Aggregate): """ OFX Section 13.8.5.6 """ secinfo = SubAggregate(SECINFO, required=True) stocktype = OneOf('COMMON', 'PREFERRED', 'CONVERTIBLE', 'OTHER') yld = Decimal() dtyieldasof = DateTime() typedesc = String(32) assetclass = OneOf(*ASSETCLASSES) fiassetclass = String(32) @staticmethod def groom(elem): """ Rename all Elements tagged YIELD (reserved Python keyword) to YLD """ yld = elem.find('./YIELD') if yld is not None: yld.tag = 'YLD' return super(STOCKINFO, STOCKINFO).groom(elem) @staticmethod def ungroom(elem): """ Rename YLD back to YLD """ yld = elem.find('./YLD') if yld is not None: yld.tag = 'YIELD' return super(STOCKINFO, STOCKINFO).ungroom(elem)
class ENROLLRQ(Aggregate): """OFX section 8.4.2""" firstname = String(32, required=True) middlename = String(32) lastname = String(32, required=True) addr1 = String(32, required=True) addr2 = String(32) addr3 = String(32) city = String(32, required=True) state = String(5, required=True) postalcode = String(11, required=True) country = OneOf(*COUNTRY_CODES) dayphone = String(32) evephone = String(32) email = String(80, required=True) userid = String(32) taxid = String(32) securityname = String(32) datebirth = DateTime() bankacctfrom = SubAggregate(BANKACCTFROM) ccacctfrom = SubAggregate(CCACCTFROM) invacctfrom = SubAggregate(INVACCTFROM) optionalMutexes = [ ["bankacctfrom", "ccacctfrom"], ["bankacctfrom", "invacctfrom"], ["ccacctfrom", "invacctfrom"], ]
class MFINFO(Aggregate): """ OFX section 13.8.5.3 """ secinfo = SubAggregate(SECINFO, required=True) mftype = OneOf('OPENEND', 'CLOSEEND', 'OTHER') yld = Decimal() dtyieldasof = DateTime() mfassetclass = SubAggregate(MFASSETCLASS) fimfassetclass = SubAggregate(FIMFASSETCLASS) @staticmethod def groom(elem): """ Rename all Elements tagged YIELD (reserved Python keyword) to YLD """ yld = elem.find('./YIELD') if yld is not None: yld.tag = 'YLD' return super(STOCKINFO, STOCKINFO).groom(elem) @staticmethod def ungroom(elem): """ Rename YLD back to YLD """ yld = elem.find('./YLD') if yld is not None: yld.tag = 'YIELD' return super(MFINFO, MFINFO).ungroom(elem)
class ADJUSTMENT(Aggregate): """OFX Section 12.5.2.4""" adjno = String(32) adjdesc = String(80, required=True) adjamt = Decimal(required=True) adjdate = DateTime()
class CHKDESC(Aggregate): """OFX section 11.6.1.1.2""" name = String(32, required=True) chknum = String(12) dtuser = DateTime() trnamt = Decimal()
class DISCOUNT(Aggregate): """OFX Section 12.5.2.3""" dscrate = Decimal(required=True) dscamt = Decimal(required=True) dscdate = DateTime() dscdesc = String(80, required=True)
class STOCKINFO(SECINFO): stocktype = OneOf('COMMON', 'PREFERRED', 'CONVERTIBLE', 'OTHER') yld = Decimal(4) dtyieldasof = DateTime() typedesc = String(32) assetclass = OneOf(*ASSETCLASSES) fiassetclass = String(32) def __init__(self, elem): """ Rename 'yield' (Python reserved word) to 'yld' """ extra_attrs = {} # Do all XPath searches before removing nodes from the tree # which seems to mess up the DOM in Python3 and throw an # AttributeError on subsequent searches. yld = elem.find('./YIELD') if yld is not None: # Rename; save for later extra_attrs['yld'] = yld.text elem.remove(yld) super(STOCKINFO, self).__init__(elem) # Add back data previously stripped/mangled for attr, val in extra_attrs.items(): setattr(self, attr, val)
class INVSTMTRS(Aggregate): """ OFX section 13.9.2.1 """ dtasof = DateTime(required=True) curdef = OneOf(*CURRENCY_CODES, required=True) invacctfrom = SubAggregate(INVACCTFROM, required=True) invtranlist = SubAggregate(INVTRANLIST) invposlist = SubAggregate(INVPOSLIST) invbal = SubAggregate(INVBAL) invoolist = SubAggregate(INVOOLIST) mktginfo = String(360) inv401kbal = SubAggregate(INV401KBAL) inv401k = SubAggregate(INV401K) @property def account(self): return self.invacctfrom @property def transactions(self): return self.invtranlist @property def positions(self): return self.invposlist @property def balances(self): return self.invbal
class BAL(Aggregate): """ OFX section 3.1.4 """ name = String(32, required=True) desc = String(80, required=True) baltype = OneOf('DOLLAR', 'PERCENT', 'NUMBER', required=True) value = Decimal(required=True) dtasof = DateTime() currency = SubAggregate(CURRENCY)
class DEPMAILRS(Aggregate): """OFX section 11.11.3.2""" bankacctfrom = SubAggregate(BANKACCTFROM, required=True) mail = SubAggregate(MAIL, required=True) trnamt = Decimal(required=True) dtuser = DateTime() fee = Decimal()
class WIRERS(Aggregate): """ OFX section 11.9.1.2 """ curdef = OneOf(*CURRENCY_CODES, required=True) srvrtid = String(10, required=True) bankacctfrom = SubAggregate(BANKACCTFROM, required=True) wirebeneficiary = SubAggregate(WIREBENEFICIARY, required=True) wiredestbank = SubAggregate(WIREDESTBANK) trnamt = Decimal(required=True) dtdue = DateTime() payinstruct = String(255) dtxferprj = DateTime() dtposted = DateTime() fee = Decimal() confmsg = String(255) optionalMutexes = [("dtxferprj", "dtposted")]
def do_stmt(args): """ Construct OFX statement request from CLI/config args; send to server. Returns a file-like object (BytesIO) that can be passed to OFXTree.parse() """ client = init_client(args) # Convert dtstart/dtend/dtasof to Python datetime type D = DateTime().convert dt = {d[2:]: D(getattr(args, d)) for d in ('dtstart', 'dtend', 'dtasof')} # Define statement requests stmtrqs = defaultdict(list) for accttype in ('checking', 'savings', 'moneymrkt', 'creditline'): acctids = getattr(args, accttype, []) stmtrqs['stmtrqs'].extend([ StmtRq(acctid=acctid, accttype=accttype.upper(), dtstart=dt['start'], dtend=dt['end'], inctran=args.inctran) for acctid in acctids ]) for acctid in args.creditcard: stmtrqs['ccstmtrqs'].append( CcStmtRq(acctid=acctid, dtstart=dt['start'], dtend=dt['end'], inctran=args.inctran)) for acctid in args.investment: stmtrqs['invstmtrqs'].append( InvStmtRq(acctid=acctid, dtstart=dt['start'], dtend=dt['end'], dtasof=dt['asof'], inctran=args.inctran, incoo=args.incoo, incpos=args.incpos, incbal=args.incbal)) # Use dummy password for dummy request if args.dryrun: password = '******'.format('anonymous') else: password = getpass() response = client.request_statements(args.user, password, clientuid=args.clientuid, dryrun=args.dryrun, **stmtrqs).read() if hasattr(response, 'decode'): response = response.decode() print(response)
class LOANINFO(Aggregate): """ OFX section 13.9.3 """ loanid = String(32, required=True) loandesc = String(32) initialloanbal = Decimal() loanstartdate = DateTime() currentloanbal = Decimal(required=True) dtasof = DateTime(required=True) loanrate = Decimal() loanpmtamt = Decimal() loanpmtfreq = OneOf(*LOANPMTFREQUENCIES) loanpmtsinitial = Integer(5) loanpmtsremaining = Integer(5) loanmaturitydate = DateTime() loantotalprojinterest = Decimal() loaninteresttodate = Decimal() loannextpmtdate = DateTime()
class MATCHINFO(Aggregate): """ OFX section 13.9.3 """ matchpct = Decimal(required=True) maxmatchamt = Decimal() maxmatchpct = Decimal() startofyear = DateTime() basematchamt = Decimal() basematchpct = Decimal()
class WIRERQ(Aggregate): """ OFX section 11.9.1.1.1 """ bankacctfrom = SubAggregate(BANKACCTFROM, required=True) wirebeneficiary = SubAggregate(WIREBENEFICIARY, required=True) wiredestbank = SubAggregate(WIREDESTBANK) trnamt = Decimal(required=True) dtdue = DateTime() payinstruct = String(255)