Skip to content

Commit

Permalink
venmo: Login fix, month at a time transaction downloads (#84)
Browse files Browse the repository at this point in the history
* Catch up with Sign In page UI changes where the password field is
interactable only after the username is submitted.
Additionally, use a different mechanism to wait for page loading because
it was failing.

* venmo: retrieve transactions one month at a time (code by chandler150).
  • Loading branch information
Zburatorul authored Nov 13, 2023
1 parent 4bacc5c commit 196faef
Showing 1 changed file with 70 additions and 13 deletions.
83 changes: 70 additions & 13 deletions finance_dl/venmo.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def CONFIG_venmo():
import os
import time
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException
from selenium.common.exceptions import NoSuchElementException, ElementNotInteractableException, StaleElementReferenceException
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.keys import Keys

Expand Down Expand Up @@ -146,19 +146,67 @@ def __init__(self, credentials, output_directory,
def check_after_wait(self):
check_url(self.driver.current_url)

def find_venmo_username(self):
for frame in self.for_each_frame():
try:
return self.driver.find_elements(By.XPATH, '//input[@type="text" or @type="email"]')
except NoSuchElementException:
pass
raise NoSuchElementException()

def find_venmo_password(self):
for frame in self.for_each_frame():
try:
return self.driver.find_elements(By.XPATH, '//input[@type="password"]')
except NoSuchElementException:
pass
raise NoSuchElementException()

def wait_for(self, condition_function):
start_time = time.time()
while time.time() < start_time + 3:
if condition_function():
return True
else:
time.sleep(0.1)
raise Exception(
'Timeout waiting for {}'.format(condition_function.__name__)
)

def click_through_to_new_page(self, button_text):
link = self.driver.find_element(By.XPATH, f'//button[@name="{button_text}"]')
link.click()

def link_has_gone_stale():
try:
# poll the link with an arbitrary call
link.find_elements(By.XPATH, 'doesnt-matter')
return False
except StaleElementReferenceException:
return True

self.wait_for(link_has_gone_stale)

def login(self):
if self.logged_in:
return
logger.info('Initiating log in')
self.driver.get('https://venmo.com/account/sign-in')

(username, password), = self.wait_and_return(
self.find_username_and_password_in_any_frame)
logger.info('Entering username and password')
username.send_keys(self.credentials['username'])
#(username, password), = self.wait_and_return(
# self.find_username_and_password_in_any_frame)
username = self.wait_and_return(self.find_venmo_username)[0][0]
try:
logger.info('Entering username')
username.send_keys(self.credentials['username'])
username.send_keys(Keys.ENTER)
except ElementNotInteractableException:
# indicates that username already filled in
logger.info("Skipped")
password = self.wait_and_return(self.find_venmo_password)[0][0]
logger.info('Entering password')
password.send_keys(self.credentials['password'])
with self.wait_for_page_load():
password.send_keys(Keys.ENTER)
self.click_through_to_new_page("Sign in")
logger.info('Logged in')
self.logged_in = True

Expand All @@ -173,7 +221,7 @@ def goto_statement(self, start_date, end_date):
def download_csv(self):
logger.info('Looking for CSV link')
download_button, = self.wait_and_locate(
(By.XPATH, '//a[text() = "Download CSV"]'))
(By.XPATH, '//*[text() = "Download CSV"]'))
self.click(download_button)
logger.info('Waiting for CSV download')
download_result, = self.wait_and_return(self.get_downloaded_file)
Expand All @@ -182,18 +230,20 @@ def download_csv(self):

def get_balance(self, balance_type):
try:
balance_node = self.driver.find_element(
By.XPATH, '//*[@class="%s"]/child::*[@class="balance-amt"]' %
balance_node = self.driver.find_element(
By.XPATH, '//*[text() = "%s"]/following-sibling::*' %
balance_type)
return balance_node.text
except NoSuchElementException:
return None

def get_balances(self):
def maybe_get_balance():
start_balance = self.get_balance('start-balance')
end_balance = self.get_balance('end-balance')
start_balance = self.get_balance('Beginning amount')
end_balance = self.get_balance('Ending amount')
if start_balance is not None and end_balance is not None:
start_balance = start_balance.replace("\n", "")
end_balance = end_balance.replace("\n", "")
return (start_balance, end_balance)
try:
error_node = self.driver.find_element(
Expand Down Expand Up @@ -303,13 +353,20 @@ def fetch_history(self):

while start_date <= self.latest_history_date:
end_date = min(self.latest_history_date,
start_date + datetime.timedelta(days=89))
self.last_day_of_month(start_date))
self.fetch_statement(start_date, end_date)
start_date = end_date + datetime.timedelta(days=1)

logger.debug('Venmo hack: waiting 5 seconds between requests')
time.sleep(5)


def last_day_of_month(self, any_day):
# The day 28 exists in every month. 4 days later, it's always next month
next_month = any_day.replace(day=28) + datetime.timedelta(days=4)
# subtracting the number of the current day brings us back one month
return next_month - datetime.timedelta(days=next_month.day)

def run(self):
self.login()
self.fetch_history()
Expand Down

0 comments on commit 196faef

Please sign in to comment.