1. Dan Connolly
  2. quacken


quacken / txget.py

The default branch has multiple heads

'''Partially automate getting transactions from bank and credit card web sites.


import ConfigParser
import logging

from selenium.webdriver.support import wait
from selenium.webdriver.support.ui import Select

log = logging.getLogger(__name__)

def main(argv, open_arg, mkBrowser, cal):
    .. note: We're using least-authority idioms; see also `_with_caps`.
    config_fn = argv[1]
    config = ConfigParser.SafeConfigParser()
    config.readfp(open_arg(config_fn), config_fn)

    browser = mkBrowser()
    for section in argv[2:]:
        site = AcctSite(browser, cal)
        ofx = site.txget(config, section)
        log.info('OFX from %s: %s', section, ofx)

class AcctSite(object):
    def __init__(self, ua, cal):
        self.__ua = ua
        self._cal = cal

    def txget(self, conf, section):
        self.login(conf.get(section, 'home'),
                   conf.get(section, 'logged_in'))

        while conf.has_option(section, 'next'):
            section = conf.get(section, 'next')
            if conf.has_option(section, 'link'):
                self.follow_link(conf.get(section, 'link'))
            if conf.has_option(section, 'submit'):
                self.form_fill(conf, section, conf.get(section, 'submit'))
            if conf.has_option(section, 'ofx'):
                return conf.get(section, 'ofx')

        raise ValueError('no ofx option in any section')

    def login(self, home, logged_in):
        log.info('opening home: %s', home)
        wt = wait.WebDriverWait(self.__ua, 60, 3)

        def login_text_found(ua):
            return ua.find_element_by_xpath(
                "//div[contains(normalize-space(.), '%s')]" % logged_in)

        log.info('Waiting for user to log in...')

    def follow_link(self, which):
        if which.startswith('"'):
            e = self.__ua.find_element_by_xpath(
                '//a[%s]' % which[1:-1])
            e = self.__ua.find_element_by_link_text(which)

    def form_fill(self, conf, section, submit):
        f = self.__ua.find_element_by_xpath(
            conf.get(section, 'form')[1:-1])

        for n, v in conf.items(section):
            if n.startswith('select_'):
                name, idx = v.split(' ', 1)
                select_option(f, name, int(idx))
            if n.startswith('radio_'):
                name, value = v.split(' ', 1)
                set_radio(f, name, value)
            elif n.startswith('text_'):
                name, value = v.split(' ', 1)
                set_text(f, name, value)
            elif n == 'today':
                mdy = self._cal.today().strftime("%02m/%02d/%Y")
                set_text(f, v, mdy)
            elif n == 'submit':
                submit = v

        btn = (f.find_element_by_xpath(submit[1:-1])
               if submit.startswith('"')
               else f.find_element_by_name(submit))

def select_option(f, name, idx):
    sel = Select(f.find_element_by_name(name))

def set_radio(f, name, value):
    val_constraint = (("and @id='%s'" % value[3:])
                      if value.startswith('id=') else
                      ("and @value='%s'" % value))
    radio = f.find_element_by_xpath(
        ".//input[@type='radio' and @name='%s' %s]" % (
            name, val_constraint))

def set_text(f, name, value):
    field = f.find_element_by_name(name)

if __name__ == '__main__':
    def _config_logging(level=logging.INFO):

    def _with_caps():
        from sys import argv
        import datetime

        from selenium import webdriver

        def open_arg(n):
            if n not in argv:
                raise IOError

            return open(n)

        main(argv, open_arg,
             mkbrowser=lambda: webdriver.Chrome())