def driver(): utils.log_debug('driver') global _driver install.setup_chromedriver() try: if is_alive(_driver): return _driver close_orphan_drivers() options = Options() assert os.path.exists(settings.chrome_data_dir) options.add_argument('user-data-dir={}'.format( settings.chrome_data_dir)) options.add_argument('--disable-extensions') assert os.path.exists(settings.chromedriver_path) _driver = webdriver.Chrome(options=options, executable_path=settings.chromedriver_path) except BaseException: quit() if 'data directory is already in use' in traceback.format_exc(): utils.log_exception_debug() utils.log_error('Must close Selenium-controlled Chrome.') if input('Try again? [Y/n]\n').lower() != 'n': utils.log_info('Trying again') return driver() sys.exit() else: utils.log_exception() assert _driver return _driver
def do_delete(): utils.log_debug('do_delete') for a in account_delete_queue.values(): queue_delete_transactions(afy.Assistant.transactions.by_name(a.name)) do_delete_transactions() do_delete_accounts() gui.quit()
def adjust_subtransaction_rows(t): utils.log_debug('add_subtransactions_rows', len(t.subtransactions)) # Remove existing subtransactions memo = gui.get_by_text('user-entered-text', t.id, count=1) # TODO: why was this partial? gui.click(memo, 2) removes = gui.get('ynab-grid-sub-remove', require=False, wait=1) while removes: gui.click(removes) removes = gui.get('ynab-grid-sub-remove', require=False, wait=.5) # Add rows for the new ones n = len(t.subtransactions) if n == 0: return gui.get_by_placeholder('accounts-text-field', 'category').clear() # needed? category_dropdown = gui.get_by_placeholder('dropdown-text-field', 'category') category_dropdown.send_keys(' ') split = gui.get('modal-account-categories-split-transaction') gui.click(split) for i in range(n - 2): # gui.clicking split means we already have two gui.click(gui.get('ynab-grid-split-add-sub-transaction')) if n == 1: # which is weird but in principle OK gui.click(gui.get('ynab-grid-sub-remove')) assert len(gui.get('ynab-grid-sub-remove')) == n
def __init__(self, d): self._parent_dict = d utils.log_debug(d) self.order_date = parse_date(d['Order Date']) self.id = d['Order ID'] self.shipment_date = parse_date(d['Shipment Date']) self.total_charged = parse_money(d['Total Charged'])
def enter_transaction(t): utils.log_debug('enter_transaction', t) accounts_sidebar = gui.get_by_text('user-entered-text', t.account_name) gui.click(accounts_sidebar) # handles that it contains two elements locate_transaction(t) assert t.account_name not in ('Annotated', 'Test Data' ) # don't overwrite test data adjust_subtransaction_rows(t) date, payees, categories, memos = map( lambda p: gui.get_by_placeholder('accounts-text-field', p), ('date', 'payee', 'category', 'memo')) date.send_keys(gui_format_date(t.date)) outflows, inflows = map( lambda p: gui.get_by_placeholder('ember-text-field', p), ('outflow', 'inflow')) if len(t.subtransactions) == 0: enter(t, payees, categories, memos, outflows, inflows) ' TODO: do not approve, only save? ' ' Maybe it is only approving things that are already approved? ' approve = gui.get_by_text('button-primary', ['Approve', 'Save']) utils.log_debug('approve/save?', approve.text) gui.click(approve) else: for i, s in enumerate(t.subtransactions): enter(s, payees[i + 1], categories[i + 1], memos[i + 1], outflows[i + 1], inflows[i + 1]) outflows[-1].send_keys(gui.Keys.ENTER)
def load(data_type): utils.log_debug('load', data_type) assert data_type in data_parsers target_path = csv_paths[data_type] try: if not os.path.exists(target_path) or stale(target_path): d = gui.driver() url = 'https://smile.amazon.com/gp/b2b/reports' if url not in d.current_url: d.get(url) d.find_element_by_id('report-use-today').click() enter_start_date() d.find_element_by_id('report-type').click() d.find_element_by_id('report-type').send_keys(data_type) d.find_element_by_id('report-confirm').click() path = wait_for_download() utils.log_debug(path, target_path) os.rename(path, target_path) except BaseException: utils.log_exception_debug() if 'AttributeError' in traceback.format_exc(): # TODO: only retry on missing csv utils.log_info('real error', traceback.format_exc()) sys.exit() if input('One more try? [Y/n]').lower() != 'n': load(data_type) list_of_dicts = read(target_path) utils.log_info('Found %s %s' % (len(list_of_dicts), data_type)) return data_parsers[data_type](list_of_dicts)
def load_path(path): utils.log_debug('loading', path) if not os.path.exists(path): return [] with open(path, 'r') as f: raw = f.read() decoded = jsonpickle.decode(raw) return decoded
def load(t, n=None, predicates=()): utils.log_debug('load', t, n) if n is None: return sum(map(lambda n: load(t, n), range(versions[t] + 1)), []) if n == -1: # ie, most recent n = versions[t] loaded = load_path(get_backup_path(t, n)) return utils.multi_filter(predicates, loaded)
def make_get_by_name(self): grouped = utils.group_by(self.xs, self.name) self.names = utils.by(map(lambda v: v.pop(), grouped.values()), self.name) for name, g in grouped.items(): if g: utils.log_debug('duplicate name, using first', name, self.names[name], *g)
def get_eligible_transactions(transactions): utils.log_debug('get_eligible_transactions') predicates = newer_than, has_blank_or_WIP_memo, matches_account eligible = utils.multi_filter(predicates, transactions) utils.log_info( 'Found %s transactions to attempt to match with Amazon orders' % len(eligible)) return utils.by(eligible, lambda t: t.id)
def click(element, n=1, pause=.5): utils.log_debug('click') if type(element) in (tuple, list): element = element[0] for i in range(n): driver().execute_script('arguments[0].click();', element) if i != n - 1: time.sleep(pause)
def queue_copy_to_account(ts, account): utils.log_debug('queue_copy_to_account', ts, account) assert afy.Assistant.accounts.get(account.id) to_copy = utils.copy(ts) for t in to_copy: modify_transaction_for_moving(t, account) utils.log_debug('to_copy', to_copy) queue_create(to_copy)
def trim_memo_length(t): utils.log_debug('trim_memo_length', t) type_assert_st(t) if t.memo is not None: t.memo = t.memo[:200] if isinstance(t, ynab_api.TransactionDetail): [trim_memo_length(st) for st in t.subtransactions] return t
def check_category(st, categories): utils.log_debug('check_category', st) type_assert_st(st) assert not st.category_id or st.category_id in categories if st.category_id and st.__dict__.get('category_name'): assert categories[st.category_id].name == st.category_name if isinstance(st, ynab_api.TransactionDetail): [check_category(s, categories) for s in st.subtransactions]
def get_transactions(): utils.log_debug('get_transactions') initialized or init() response = transactions_api.get_transactions(settings.budget_id) ts = response.data.transactions assert all(isinstance(t, ynab_api.TransactionDetail) for t in ts) ts.sort(key=lambda t: t.date, reverse=True) return ts
def get_accounts(): utils.log_debug('get_accounts') initialized or init() response = accounts_api.get_accounts(settings.budget_id) acs = response.data.accounts assert all(isinstance(ac, ynab_api.Account) for ac in acs) acs.sort(key=lambda ac: ac.name, reverse=True) return acs
def get_payees(): utils.log_debug('get_payees') initialized or init() response = payees_api.get_payees(settings.budget_id) ps = response.data.payees assert all(isinstance(p, ynab_api.Payee) for p in ps) ps.sort(key=lambda p: p.name) return ps
def install(): utils.log_info('Installing') setup_chromedriver() make_dirs() setup_ynab_auth() setup_ynab_budget_id() settings.init() utils.log_debug('Settings:', settings._s.settings) utils.log_info('Installed!')
def combine_orders(orders): utils.log_debug('combine_orders') combined = {} for order in orders: if order.id in combined: combined[order.id] += order # overloaded operator else: combined[order.id] = order return combined
def is_alive(d): utils.log_debug('is_alive') try: d.execute(Command.STATUS) d.current_url return True except BaseException: utils.log_exception_debug() return False
def update_transactions(transactions): utils.log_debug('update_transactions') initialized or init() assert all(isinstance(t, ynab_api.TransactionDetail) for t in transactions) ut = utils.convert(transactions, ynab_api.UpdateTransaction) utw = ynab_api.UpdateTransactionsWrapper(transactions=ut) ts = transactions_api.update_transactions(settings.budget_id, utw).data.transactions assert all(isinstance(t, ynab_api.TransactionDetail) for t in ts) return ts
def create_transactions(transactions): utils.log_debug('create_transactions') initialized or init() assert all(isinstance(t, ynab_api.TransactionDetail) for t in transactions) st = utils.convert(transactions, ynab_api.SaveTransaction) stw = ynab_api.SaveTransactionsWrapper(transactions=st) ts = transactions_api.create_transaction(settings.budget_id, stw).data.transactions assert all(isinstance(t, ynab_api.TransactionDetail) for t in ts) return ts
def enter(st, payee_element, category_element, memo_element, outflow_element, inflow_element): utils.log_debug('enter', st) type_assert_st(st) category = get_category(st) payee = get_payee(st) amount = get_amount(st) outflow = 0 if amount > 0 else abs(amount) inflow = 0 if amount < 0 else abs(amount) enter_fields((payee_element, category_element, memo_element, outflow_element), (payee, category, st.memo, outflow, inflow))
def check_payee(st, payees): utils.log_debug('check_payee', st) type_assert_st(st) assert not st.payee_id or st.payee_id in payees # Need get because this is a field that isn't on the api model # I just add it for gui_client convenience in amazon.amazon if st.payee_id and st.__dict__.get('payee_name'): assert payees[st.payee_id].name == st.payee_name if isinstance(st, ynab_api.TransactionDetail): [check_payee(s, payees) for s in st.subtransactions]
def get_category_groups(): utils.log_debug('get_category_groups') initialized or init() response = categories_api.get_categories(settings.budget_id) groups = response.data.category_groups assert all( isinstance(g, ynab_api.CategoryGroupWithCategories) for g in groups) categories = [c for g in groups for c in g.categories] assert all(isinstance(c, ynab_api.Category) for c in categories) return groups
def match_all(transactions, orders): utils.log_debug('match_all', len(transactions), len(orders)) orders_by_transaction_id = {} for t_id, t in transactions.items(): order = get_order(t, orders) if not order: continue orders_by_transaction_id[t_id] = order utils.log_info('Found %s matches' % len(orders_by_transaction_id)) return orders_by_transaction_id
def queue(ts, mode, payees, categories): utils.log_debug('queue', ts, mode) assert mode in rest_modes assert all(isinstance(t, ynab_api.TransactionDetail) for t in ts) for t in ts: if payees is not None: # TODO: check them all at once. Or even like actually use this.. check_payee(t, payees) if categories is not None: check_category(t, categories) enqueue(t, (gui_queue if t.subtransactions else rest_queue)[mode])
def add_unlinked_account(account_name, balance=0, account_type='credit'): utils.log_debug('add_unlinked_account', account_name, balance, account_type) gui_client.add_unlinked_account(account_name, balance, account_type) gui.quit() afy.Assistant.download_ynab(accounts=True) start = time.time() while time.time() - start < 30: # TODO: generic retrier new_account = afy.Assistant.accounts.by_name(account_name) if new_account: return new_account assert False
def do_delete_transactions(): utils.log_debug('do_delete_transactions', transaction_delete_queue) if not transaction_delete_queue: return utils.log_info('Set deletion memo on %s transactions via YNAB REST API' % len(transaction_delete_queue)) for t in transaction_delete_queue.values(): t.memo = delete_key api_client.update_transactions(transaction_delete_queue) utils.log_info('delete %s transactions via YNAB webapp' % len(transaction_delete_queue)) gui_client.delete_transactions() transaction_delete_queue.clear()
def close_orphan_drivers(): return # TODO: this doesn't work from WSL utils.log_debug('close_orphan_drivers') p = subprocess.Popen(['ps', '-A'], stdout=subprocess.PIPE) out, err = p.communicate() for line in out.splitlines(): if settings.chromedriver_filename in str(line.lower()): pid = int(line.split(None, 1)[0]) utils.log_debug('killing', line, pid) os.kill(pid, signal.SIGKILL)