Exemple #1
0
    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)
Exemple #2
0
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,
    }
Exemple #3
0
    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)
Exemple #4
0
    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)
Exemple #5
0
    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)