def _password_page_soup(self) -> BeautifulSoup: """ Gets the soup for the page where we enter the password. A lazy property, meaning we only do the request/parse on the first access. """ data = { "QuestionAnswer": _find_security_question_answer( self._security_challenge_soup.body.text, self.security_questions), "ChallengeType": get_input_value_ided_as(self._security_challenge_soup, "ChallengeType"), "MFARegister": 0, "RID": get_input_value_ided_as(self._security_challenge_soup, "RID"), "SecurityChallenge": get_input_value_ided_as(self._security_challenge_soup, "SecurityChallenge"), "Submit": "Submit", "SPTN": get_input_value_ided_as(self._security_challenge_soup, "SPTN") } response = self.session.post(self._security_challenge_post_url, data=data) assert response.status_code == requests.codes.ok, \ "Failed to POST security challenge. Received status: {}".format(response.status_code) return BeautifulSoup(response.text, BEAUTIFULSOUP_PARSER)
def _get_export_form_data(form_page_soup: BeautifulSoup, acct_session_id: str, sptn: str, from_date: datetime.date=None, to_date: datetime.date=None, format: str='OFX', cycle: int=0, iifaccount: str='') -> dict: """ Returns dict for POSTing to download transactions. :param form_page_soup: The soup of the page to extract fields from. :param acct_session_id: The per-session account number or id. :param sptn: The per-session identifier. :param from_date: The starting date to download transactions from. Only valid when `cycle` is 0. :param to_date: The ending date to download transactions to. Only valid when `cycle` is 0. Defaults to today. :param format: One of ['QFX', 'QIF', 'QBO', 'IIF', 'OFX', 'CSV']. If IIF, must provide `iifaccount`. :param cycle: One of [0, 1, 2, 3]. If 0, must provide `from_date` and optionally `to_date`. '1' is for recent transactions, '2' is for current statement, and 3 is for previous statement. :param iifaccount: The name of the Quickbooks account if downloading IIF format. :return: dict of POSTable data. """ assert -1 < cycle < 4, "Cycle must be between -1 and 4." if cycle == 0: if not to_date: to_date = datetime.date.today() assert from_date <= to_date, "You must provide a from_date lower than a to_date." from_date = from_date.strftime('%m/%d/%y') to_date = to_date.strftime('%m/%d/%y') else: from_date = None to_date = None valid_formats = ['QFX', 'QIF', 'QBO', 'IIF', 'OFX', 'CSV'] assert format in valid_formats, '{} is an invalid format. It must be one of: {}'.format(format, valid_formats) if format == 'IIF': assert iifaccount, "You must provide an account name for Quickbooks when downloading to IIF." return { 'Cycle': '0{}'.format(cycle), 'Tran_Type': '09', # This seems to always be this number 'FromDate': from_date, 'ThruDate': to_date, 'lstFormat': format, 'Submit': 'Export', 'IIFAccount': get_input_value_ided_as(form_page_soup, 'IIFAccount'), 'WO': get_input_value_ided_as(form_page_soup, 'WO'), 'ESPTN': get_input_value_ided_as(form_page_soup, 'ESPTN'), 'Number': acct_session_id, 'Type': '001', # This seems to always be this number, 'SPTN': sptn, }
def root_page_soup(self) -> BeautifulSoup: """ Lazily get the content of the root page. This creates an authenticated session in self.session. A lazy property, meaning that the soup is only calculated/retrieved/parsed on first access. """ modulus = get_input_value_ided_as(self._password_page_soup.body, 'Modulus') public_exp = get_input_value_ided_as(self._password_page_soup.body, 'PublicExponent') rsaiv = get_input_value_ided_as(self._password_page_soup.body, 'RSAIV') rid = get_input_value_ided_as(self._password_page_soup.body, 'RID') sptn = get_input_value_ided_as(self._password_page_soup.body.form, 'SPTN') rsa_encrypt_script_url = _get_RSAEncrypt_url(self._password_page_soup, self._access_id_post_url) # TODO: Figure out how to do this in python crypted_password = _rsa_encrypt(self.session, rsa_encrypt_script_url, modulus, public_exp, rsaiv, self.password) display_password = "******" * len(self.password) password_post_url = urljoin( self._access_id_post_url, self._password_page_soup.body.form['action']) assert "WCE=PasswordSubmit" in password_post_url, \ "Unable to find accurate password POST url. Found: {}".format(password_post_url) data = { "DisplayPassword": display_password, "Password": crypted_password, "RID": rid, "Submit": "Submit", "Modulus": modulus, "PublicExponent": public_exp, "RSAIV": rsaiv, "SPTN": sptn } headers = dict(BASIC_HEADERS) headers["Referer"] = self._security_challenge_post_url response = self.session.post(password_post_url, data=data, headers=headers) assert _is_root_page( response.text ), "Unknown content received when expecting root account page." return BeautifulSoup(response.text, BEAUTIFULSOUP_PARSER)
def root_page_soup(self) -> BeautifulSoup: """ Lazily get the content of the root page. This creates an authenticated session in self.session. A lazy property, meaning that the soup is only calculated/retrieved/parsed on first access. """ modulus = get_input_value_ided_as(self._password_page_soup.body, 'Modulus') public_exp = get_input_value_ided_as(self._password_page_soup.body, 'PublicExponent') rsaiv = get_input_value_ided_as(self._password_page_soup.body, 'RSAIV') rid = get_input_value_ided_as(self._password_page_soup.body, 'RID') sptn = get_input_value_ided_as(self._password_page_soup.body.form, 'SPTN') rsa_encrypt_script_url = _get_RSAEncrypt_url(self._password_page_soup, self._access_id_post_url) # TODO: Figure out how to do this in python crypted_password = _rsa_encrypt(self.session, rsa_encrypt_script_url, modulus, public_exp, rsaiv, self.password) display_password = "******" * len(self.password) password_post_url = urljoin(self._access_id_post_url, self._password_page_soup.body.form['action']) assert "WCE=PasswordSubmit" in password_post_url, \ "Unable to find accurate password POST url. Found: {}".format(password_post_url) data = { "DisplayPassword": display_password, "Password": crypted_password, "RID": rid, "Submit": "Submit", "Modulus": modulus, "PublicExponent": public_exp, "RSAIV": rsaiv, "SPTN": sptn } headers = dict(BASIC_HEADERS) headers["Referer"] = self._security_challenge_post_url response = self.session.post(password_post_url, data=data, headers=headers) assert _is_root_page(response.text), "Unknown content received when expecting root account page." return BeautifulSoup(response.text, BEAUTIFULSOUP_PARSER)
def _password_page_soup(self) -> BeautifulSoup: """ Gets the soup for the page where we enter the password. A lazy property, meaning we only do the request/parse on the first access. """ data = { "QuestionAnswer": _find_security_question_answer(self._security_challenge_soup.body.text, self.security_questions), "ChallengeType": get_input_value_ided_as(self._security_challenge_soup, "ChallengeType"), "MFARegister": 0, "RID": get_input_value_ided_as(self._security_challenge_soup, "RID"), "SecurityChallenge": get_input_value_ided_as(self._security_challenge_soup, "SecurityChallenge"), "Submit": "Submit", "SPTN": get_input_value_ided_as(self._security_challenge_soup, "SPTN") } response = self.session.post(self._security_challenge_post_url, data=data) assert response.status_code == requests.codes.ok, \ "Failed to POST security challenge. Received status: {}".format(response.status_code) return BeautifulSoup(response.text, BEAUTIFULSOUP_PARSER)