def request_tax1099( self, password: str, *taxyears: str, acctnum: str = None, recid: str = None, dryrun: bool = False, verify_ssl: bool = True, timeout: Optional[float] = None, ) -> BinaryIO: """ Request US federal income tax form 1099 (TAX1099RQ) """ logger.info("Creating tax 1099 request") signon = self.signon(password) rq = TAX1099RQ(*taxyears, recid=recid or None) msgs = TAX1099MSGSRQV1(TAX1099TRNRQ(trnuid=self.uuid, tax1099rq=rq)) logger.debug(f"Wrapped tax 1099 request messages: {msgs}") ofx = OFX(signonmsgsrqv1=signon, tax1099msgsrqv1=msgs) return self.download(ofx, dryrun=dryrun, verify_ssl=verify_ssl, timeout=timeout)
def request_profile( self, user=None, password=None, dryrun=False, prettyprint=False, close_elements=True, ): """ Package and send OFX profile requests (PROFRQ). """ dtprofup = datetime.datetime(1990, 1, 1, tzinfo=UTC) profrq = PROFRQ(clientrouting="NONE", dtprofup=dtprofup) trnuid = self.uuid proftrnrq = PROFTRNRQ(trnuid=trnuid, profrq=profrq) msgs = PROFMSGSRQV1(proftrnrq) user = user or "{:0<32}".format("anonymous") password = password or "{:0<32}".format("anonymous") signonmsgs = self.signon(user, password) ofx = OFX(signonmsgsrqv1=signonmsgs, profmsgsrqv1=msgs) return self.download( ofx, dryrun=dryrun, prettyprint=prettyprint, close_elements=close_elements )
def request_accounts( self, password: str, dtacctup: datetime.datetime, dryrun: bool = False, version: Optional[int] = None, verify_ssl: bool = True, timeout: Optional[float] = None, ) -> BinaryIO: """ Package and send OFX account info requests (ACCTINFORQ) """ logger.info("Creating account info request") signon = self.signon(password) acctinforq = ACCTINFORQ(dtacctup=dtacctup) acctinfotrnrq = ACCTINFOTRNRQ(trnuid=self.uuid, acctinforq=acctinforq) msgs = SIGNUPMSGSRQV1(acctinfotrnrq) logger.debug(f"Wrapped account info request messages: {msgs}") ofx = OFX(signonmsgsrqv1=signon, signupmsgsrqv1=msgs) return self.download(ofx, dryrun=dryrun, verify_ssl=verify_ssl, timeout=timeout)
def serialize(self, ofx: OFX, version: Optional[int] = None, prettyprint: Optional[bool] = None, close_elements: Optional[bool] = None, ) -> bytes: if version is None: version = self.version if prettyprint is None: prettyprint = self.prettyprint if close_elements is None: close_elements = self.close_elements header = bytes(str(make_header(version=version, newfileuid=self.uuid)), "utf_8") tree = ofx.to_etree() if prettyprint: utils.indent(tree) # Some servers choke on OFXv1 requests including ending tags for # elements (which are optional per the spec). if close_elements is False: if version >= 200: msg = "OFX version {} requires ending tags for elements" raise ValueError(msg.format(version)) body = utils.tostring_unclosed_elements(tree) else: # ``method="html"`` skips the initial XML declaration body = ET.tostring(tree, encoding="utf_8", method="html") return header + body
def request_statements( self, password: str, *requests: RequestParam, dryrun: bool = False, verify_ssl: bool = True, timeout: Optional[float] = None, ) -> BinaryIO: """ Package and send OFX statement requests (STMTRQ/CCSTMTRQ/INVSTMTRQ/STMTENDRQ/CCSTMTENDRQ). """ logger.info(f"Creating statement requests for {requests}") # Group requests by type and pass to the appropriate *TRNRQ handler # function (see singledispatch setup below). # # *StmtRq/*StmtEndRqs (namedtuples) don't have rich comparison methods; # can't sort by class. As a proxy, we sort by class name, even though # we actually group by request class so we can use it when iterating # over groupby(). stmtSortKey = attrgetter("__class__.__name__") stmtGroupKey = attrgetter("__class__") trnrqs = [ wrap_stmtrq(cls(), rqs, self) for cls, rqs in itertools.groupby( sorted(requests, key=stmtSortKey), key=stmtGroupKey ) ] # trnrqs is a pair of (models.*MSGSRQV1, [*TRNRQ]) # Can't sort *MSGSRQV1 by class, either, so we use the same trick # of sorting by class name and grouping by class. def trnSortKey(pair): return pair[0].__name__ trnGroupKey = itemgetter(0) trnrqs.sort(key=trnSortKey) # N.B. we need to annotate first arg as typing.Type here to indicate that # we're passing in a class not an instance. def msg_args( msgcls: Union[ Type[BANKMSGSRQV1], Type[CREDITCARDMSGSRQV1], Type[INVSTMTMSGSRQV1] ], trnrqs: Iterator[Request], ) -> Tuple[str, Message]: trnrqs_ = list(itertools.chain.from_iterable(t[1] for t in trnrqs)) attr_name = msgcls.__name__.lower() return (attr_name, msgcls(*trnrqs_)) msgs = dict( msg_args(msgcls, _trnrqs) for msgcls, _trnrqs in itertools.groupby(trnrqs, key=trnGroupKey) ) logger.debug(f"Wrapped statement request messages: {msgs}") signon = self.signon(password) ofx = OFX(signonmsgsrqv1=signon, **msgs) return self.download(ofx, dryrun=dryrun, verify_ssl=verify_ssl, timeout=timeout)
def serialize( self, ofx: OFX, version: Optional[int] = None, oldfileuid: Optional[str] = None, newfileuid: Optional[str] = None, prettyprint: Optional[bool] = None, close_elements: Optional[bool] = None, ) -> bytes: """ Transform a ``models.OFX`` instance into bytestring representation with OFX header prepended. N.B. ``version`` / ``prettyprint`` / ``close_elements`` kwargs are basically hacks for ``scripts.ofxget.scan_profile()``; ordinarily you should initialize the ``OFXClient`` with the proper version# and formatting parameters, rather than overriding the client config here. Optional kwargs: ``version`` - OFX version to report in header ``oldfileuid`` - OLDFILEUID to report in header ``newfileuid`` - NEWFILEUID to report in header ``prettyprint`` - add newlines between tags and indentation ``close_elements`` - add markup closing tags to leaf elements """ if version is None: version = self.version if prettyprint is None: prettyprint = self.prettyprint if close_elements is None: close_elements = self.close_elements header = bytes( str( make_header( version=version, oldfileuid=oldfileuid, newfileuid=newfileuid ) ), "utf_8", ) tree = ofx.to_etree() if prettyprint: utils.indent(tree) # Some servers choke on OFXv1 requests including ending tags for # elements (which are optional per the spec). if close_elements is False: if version >= 200: raise ValueError( f"OFX version {version} requires ending tags for elements" ) body = utils.tostring_unclosed_elements(tree) else: # ``method="html"`` skips the initial XML declaration body = ET.tostring(tree, encoding="utf_8", method="html") return header + body
def request_end_statements(self, user, password, *requests, language=None, clientuid=None, appid=None, appver=None, version=None, dryrun=False, prettyprint=False, close_elements=True, timeout=None): """ Package and send OFX end statement requests (STMTENDRQ, CCSTMTENDRQ). Input *requests are instances of the corresponding namedtuples (StmtEndRq, CcStmtEndRq) """ msgs = { "bankmsgsrqv1": None, "creditcardmsgsrqv1": None, "invstmtmsgsrqv1": None, } sortkey = attrgetter("__class__.__name__") requests = sorted(requests, key=sortkey) for clsName, rqs in itertools.groupby(requests, key=sortkey): if clsName == "StmtEndRq": msgs["bankmsgsrqv1"] = BANKMSGSRQV1(*[ self.stmtendtrnrq(**dict(rq._asdict(), bankid=self.bankid)) for rq in rqs ]) elif clsName == "CcStmtEndRq": msgs["creditcardmsgsrqv1"] = CREDITCARDMSGSRQV1( *[self.ccstmtendtrnrq(**rq._asdict()) for rq in rqs]) else: msg = "Not a *StmtEndRq: {}".format(clsName) raise ValueError(msg) signon = self.signon(user, password, language=language, clientuid=clientuid, appid=appid, appver=appver) ofx = OFX(signonmsgsrqv1=signon, **msgs) return self.download( ofx, dryrun=dryrun, version=version, prettyprint=prettyprint, close_elements=close_elements, timeout=timeout, )
def aggregate(cls): return OFX( signonmsgsrqv1=msgsets.Signonmsgsrqv1TestCase.aggregate, signupmsgsrqv1=msgsets.Signupmsgsrqv1TestCase.aggregate, bankmsgsrqv1=msgsets.Bankmsgsrqv1TestCase.aggregate, creditcardmsgsrqv1=msgsets.Creditcardmsgsrqv1TestCase.aggregate, invstmtmsgsrqv1=msgsets.Invstmtmsgsrqv1TestCase.aggregate, interxfermsgsrqv1=msgsets.Interxfermsgsrqv1TestCase.aggregate, wirexfermsgsrqv1=msgsets.Wirexfermsgsrqv1TestCase.aggregate, emailmsgsrqv1=msgsets.Emailmsgsrqv1TestCase.aggregate, seclistmsgsrqv1=msgsets.Seclistmsgsrqv1TestCase.aggregate, profmsgsrqv1=msgsets.Profmsgsrqv1TestCase.aggregate)
def request_statements(self, user, password, clientuid=None, stmtrqs=None, ccstmtrqs=None, invstmtrqs=None, dryrun=False, prettyprint=None): """ Package and send OFX statement requests (STMTRQ/CCSTMTRQ/INVSTMTRQ). Input *rqs are sequences of the corresponding namedtuples (StmtRq, CcStmtRq, InvStmtRq) """ signonmsgs = self.signon(user, password, clientuid=clientuid) bankmsgs = None if stmtrqs: # bankid comes from OFXClient instance attribute, # not StmtRq namedtuple stmttrnrqs = [ self.stmttrnrq(**dict(stmtrq._asdict(), bankid=self.bankid)) for stmtrq in stmtrqs ] bankmsgs = BANKMSGSRQV1(*stmttrnrqs) creditcardmsgs = None if ccstmtrqs: ccstmttrnrqs = [ self.ccstmttrnrq(**stmtrq._asdict()) for stmtrq in ccstmtrqs ] creditcardmsgs = CREDITCARDMSGSRQV1(*ccstmttrnrqs) invstmtmsgs = None if invstmtrqs: # brokerid comes from OFXClient instance attribute, # not StmtRq namedtuple invstmttrnrqs = [ self.invstmttrnrq( **dict(stmtrq._asdict(), brokerid=self.brokerid)) for stmtrq in invstmtrqs ] invstmtmsgs = INVSTMTMSGSRQV1(*invstmttrnrqs) ofx = OFX(signonmsgsrqv1=signonmsgs, bankmsgsrqv1=bankmsgs, creditcardmsgsrqv1=creditcardmsgs, invstmtmsgsrqv1=invstmtmsgs) return self.download(ofx, dryrun=dryrun, prettyprint=prettyprint)
def request_tax1099( self, password: str, *taxyears: str, acctnum: str = None, recid: str = None, gen_newfileuid: bool = True, dryrun: bool = False, timeout: Optional[float] = None, ) -> BinaryIO: """ Request US federal income tax form 1099 (TAX1099RQ) """ if dryrun: url = "" else: RqCls2url = self._get_service_urls( timeout=timeout, gen_newfileuid=gen_newfileuid, ) # HACK FIXME # As a simplification, we assume that FIs handle all classes # of statement request from a single URL. urls = set(RqCls2url.values()) assert len(urls) == 1 url = urls.pop() logger.info("Creating tax 1099 request") signon = self.signon(password) rq = TAX1099RQ(*taxyears, recid=recid or None) msgs = TAX1099MSGSRQV1(TAX1099TRNRQ(trnuid=self.uuid, tax1099rq=rq)) logger.debug(f"Wrapped tax 1099 request messages: {msgs}") ofx = OFX(signonmsgsrqv1=signon, tax1099msgsrqv1=msgs) if gen_newfileuid: newfileuid = self.uuid else: newfileuid = None return self.download( ofx, newfileuid=newfileuid, dryrun=dryrun, timeout=timeout, url=url, )
def request_accounts( self, password: str, dtacctup: datetime.datetime, dryrun: bool = False, version: Optional[int] = None, gen_newfileuid: bool = True, timeout: Optional[float] = None, ) -> BinaryIO: """ Package and send OFX account info requests (ACCTINFORQ) """ if dryrun: url = "" else: RqCls2url = self._get_service_urls( timeout=timeout, gen_newfileuid=gen_newfileuid, ) # HACK FIXME # As a simplification, we assume that FIs handle all classes # of statement request from a single URL. urls = set(RqCls2url.values()) assert len(urls) == 1 url = urls.pop() logger.info("Creating account info request") signon = self.signon(password) acctinforq = ACCTINFORQ(dtacctup=dtacctup) acctinfotrnrq = ACCTINFOTRNRQ(trnuid=self.uuid, acctinforq=acctinforq) msgs = SIGNUPMSGSRQV1(acctinfotrnrq) logger.debug(f"Wrapped account info request messages: {msgs}") ofx = OFX(signonmsgsrqv1=signon, signupmsgsrqv1=msgs) if gen_newfileuid: newfileuid = self.uuid else: newfileuid = None return self.download( ofx, newfileuid=newfileuid, dryrun=dryrun, timeout=timeout, url=url, )
def request_accounts(self, user, password, dtacctup, clientuid=None, dryrun=False, prettyprint=None, close_elements=True): """ Package and send OFX account info requests (ACCTINFORQ) """ signonmsgs = self.signon(user, password, clientuid=clientuid) acctinforq = ACCTINFORQ(dtacctup=dtacctup) acctinfotrnrq = ACCTINFOTRNRQ(trnuid=uuid.uuid4(), acctinforq=acctinforq) signupmsgs = SIGNUPMSGSRQV1(acctinfotrnrq) ofx = OFX(signonmsgsrqv1=signonmsgs, signupmsgsrqv1=signupmsgs) return self.download(ofx, dryrun=dryrun, prettyprint=prettyprint, close_elements=close_elements)
def request_profile(self, user=None, password=None, dryrun=False): """ Package and send OFX profile requests (PROFRQ). """ dtprofup = datetime.date(1990, 1, 1) profrq = PROFRQ(clientrouting='NONE', dtprofup=dtprofup) trnuid = uuid.uuid4() proftrnrq = PROFTRNRQ(trnuid=trnuid, profrq=profrq) msgs = PROFMSGSRQV1(proftrnrq) user = user or '{:0<32}'.format('anonymous') password = password or '{:0<32}'.format('anonymous') signonmsgs = self.signon(user, password) ofx = OFX(signonmsgsrqv1=signonmsgs, profmsgsrqv1=msgs) return self.download(ofx, dryrun=dryrun)
def _request_profile( self, dtprofup: Optional[datetime.datetime] = None, version: Optional[int] = None, gen_newfileuid: bool = True, prettyprint: Optional[bool] = None, close_elements: Optional[bool] = None, dryrun: bool = False, timeout: Optional[float] = None, url: Optional[str] = None, ) -> BytesIO: """Package and send OFX profile requests (PROFRQ).""" logger.info("Creating profile request") if dtprofup is None: dtprofup = datetime.datetime(1990, 1, 1, tzinfo=UTC) profrq = PROFRQ(clientrouting="NONE", dtprofup=dtprofup) proftrnrq = PROFTRNRQ(trnuid=self.uuid, profrq=profrq) logger.debug(f"Wrapped profile request: {proftrnrq}") user = password = AUTH_PLACEHOLDER signon = self.signon(password, userid=user) ofx = OFX(signonmsgsrqv1=signon, profmsgsrqv1=PROFMSGSRQV1(proftrnrq)) if gen_newfileuid: newfileuid = self.uuid else: newfileuid = None return self.download( ofx, version=version, newfileuid=newfileuid, prettyprint=prettyprint, close_elements=close_elements, dryrun=dryrun, timeout=timeout, url=url, )
def request_accounts(self, user, password, dtacctup, language=None, clientuid=None, appid=None, appver=None, dryrun=False, version=None, prettyprint=False, close_elements=True, timeout=None): """ Package and send OFX account info requests (ACCTINFORQ) """ signon = self.signon(user, password, language=language, clientuid=clientuid, appid=appid, appver=appver) acctinforq = ACCTINFORQ(dtacctup=dtacctup) acctinfotrnrq = ACCTINFOTRNRQ(trnuid=self.uuid, acctinforq=acctinforq) signupmsgs = SIGNUPMSGSRQV1(acctinfotrnrq) ofx = OFX(signonmsgsrqv1=signon, signupmsgsrqv1=signupmsgs) return self.download( ofx, dryrun=dryrun, version=version, prettyprint=prettyprint, close_elements=close_elements, timeout=timeout, )
def request_profile( self, version: Optional[int] = None, prettyprint: Optional[bool] = None, close_elements: Optional[bool] = None, dryrun: bool = False, verify_ssl: bool = True, timeout: Optional[float] = None, ) -> BinaryIO: """ Package and send OFX profile requests (PROFRQ). ofxget.scan_profile() overrides version/prettyprint/close_elements. """ logger.info("Creating profile request") dtprofup = datetime.datetime(1990, 1, 1, tzinfo=UTC) profrq = PROFRQ(clientrouting="NONE", dtprofup=dtprofup) proftrnrq = PROFTRNRQ(trnuid=self.uuid, profrq=profrq) logger.debug(f"Wrapped profile request: {proftrnrq}") user = password = AUTH_PLACEHOLDER signon = self.signon(password, userid=user) ofx = OFX(signonmsgsrqv1=signon, profmsgsrqv1=PROFMSGSRQV1(proftrnrq)) return self.download( ofx, version=version, prettyprint=prettyprint, close_elements=close_elements, dryrun=dryrun, verify_ssl=verify_ssl, timeout=timeout, )
def request_statements( self, password: str, *requests: RequestParam, gen_newfileuid: bool = True, dryrun: bool = False, timeout: Optional[float] = None, ) -> BinaryIO: """ Package and send OFX statement requests (STMTRQ/CCSTMTRQ/INVSTMTRQ/STMTENDRQ/CCSTMTENDRQ). """ if dryrun: url = "" else: RqCls2url = self._get_service_urls( timeout=timeout, gen_newfileuid=gen_newfileuid, ) # HACK FIXME # As a simplification, we assume that FIs handle all classes # of statement request from a single URL. urls = set(RqCls2url.values()) assert len(urls) == 1 url = urls.pop() logger.info(f"Creating statement requests for {requests}") # Group requests by type and pass to the appropriate *TRNRQ handler # function (see singledispatch setup below). # # Classes don't have rich comparison methods, so we can't sort by class. # As a proxy, we sort by class name, even though we actually group by class # so we can use it when iterating over groupby(). sortKey = attrgetter("__class__.__name__") groupKey = attrgetter("__class__") trnrqs = [ wrap_stmtrq(cls(), rqs, self) for cls, rqs in itertools.groupby(sorted(requests, key=sortKey), key=groupKey) ] # trnrqs is a pair of (models.*MSGSRQV1, [*TRNRQ]) # Can't sort *MSGSRQV1 by class, either, so we use the same trick # of sorting by class name and grouping by class. def trnSortKey(pair): return pair[0].__name__ trnGroupKey = itemgetter(0) trnrqs.sort(key=trnSortKey) # N.B. we need to annotate first arg as typing.Type here to indicate that # we're passing in a class not an instance. def msg_args( msgcls: Union[Type[BANKMSGSRQV1], Type[CREDITCARDMSGSRQV1], Type[INVSTMTMSGSRQV1]], trnrqs: Iterator[Request], ) -> Tuple[str, Message]: trnrqs_ = list(itertools.chain.from_iterable(t[1] for t in trnrqs)) attr_name = msgcls.__name__.lower() return (attr_name, msgcls(*trnrqs_)) msgs = dict( msg_args(msgcls, _trnrqs) for msgcls, _trnrqs in itertools.groupby(trnrqs, key=trnGroupKey)) logger.debug(f"Wrapped statement request messages: {msgs}") signon = self.signon(password) ofx = OFX(signonmsgsrqv1=signon, **msgs) if gen_newfileuid: newfileuid = self.uuid else: newfileuid = None return self.download( ofx, newfileuid=newfileuid, dryrun=dryrun, timeout=timeout, url=url, )