From 7cab766c38799866688f166b8e867c1705638ccc Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Thu, 11 May 2023 22:52:35 -0500 Subject: [PATCH] Refactor error handling The `ntfyerror` context manager replaces `screenshot_failure` for handling online banking interaction failures. It has several advantages, notably: * takes a screenshot of the browser page *before* logging out * cleaner suppression of exceptions, with success tracking * sends an `ntfy` message, with the screenshot attached --- xactfetch.py | 125 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 74 insertions(+), 51 deletions(-) diff --git a/xactfetch.py b/xactfetch.py index 5654654..824ea89 100644 --- a/xactfetch.py +++ b/xactfetch.py @@ -1,4 +1,3 @@ -import contextlib import copy import datetime import json @@ -15,7 +14,6 @@ from typing import Any, Optional, Type import requests from playwright.sync_api import Page -from playwright.sync_api import TimeoutError as PlaywrightTimeout from playwright.sync_api import sync_playwright @@ -35,21 +33,34 @@ ACCOUNTS = { def ntfy( - message: str, + message: Optional[str] = None, topic: str = NTFY_TOPIC, title: Optional[str] = None, tags: Optional[str] = None, + attach: Optional[bytes] = None, + filename: Optional[str] = None, ) -> None: + assert message or attach headers = { 'Title': title or 'xactfetch', } if tags: headers['Tags'] = tags - r = requests.post( - f'{NTFY_URL}/{topic}', - headers=headers, - data=message, - ) + url = f'{NTFY_URL}/{topic}' + if message: + r = requests.post( + url, + headers=headers, + data=message, + ) + else: + if filename: + headers['Filename'] = filename + r = requests.put( + url, + headers=headers, + data=attach, + ) r.raise_for_status() @@ -176,8 +187,8 @@ def get_last_transaction_date(key: int, token: str) -> datetime.date: return last_date.date() + datetime.timedelta(days=1) -def download_chase(page: Page, end_date: datetime.date, token: str) -> None: - with Chase(page) as c: +def download_chase(page: Page, end_date: datetime.date, token: str) -> bool: + with Chase(page) as c, ntfyerror('Chase', page) as r: c.login() key = ACCOUNTS['chase'] try: @@ -187,22 +198,23 @@ def download_chase(page: Page, end_date: datetime.date, token: str) -> None: 'Skipping Chase account: could not get last transaction: %s', e, ) - return + return False if start_date >= end_date: log.info( 'Skipping Chase account: last transaction was %s', start_date, ) - return + return True csv = c.download_transactions(start_date, end_date) log.info('Importing transactions from Chase into Firefly III') c.firefly_import(csv, key, token) + return r.success -def download_commerce(page: Page, end_date: datetime.date, token: str) -> None: +def download_commerce(page: Page, end_date: datetime.date, token: str) -> bool: log.info('Downloading transaction lists from Commerce Bank') csvs = [] - with CommerceBank(page) as c: + with CommerceBank(page) as c, ntfyerror('Commerce Bank', page) as r: c.login() for name, key in ACCOUNTS['commerce'].items(): try: @@ -231,6 +243,51 @@ def download_commerce(page: Page, end_date: datetime.date, token: str) -> None: log.info('Importing transactions from Commerce Bank into Firefly III') for key, csv in csvs: c.firefly_import(csv, key, token) + return r.success + + +class ntfyerror: + def __init__(self, bank: str, page: Page) -> None: + self.bank = bank + self.page = page + self.success = True + + def __enter__(self) -> 'ntfyerror': + return self + + def __exit__( + self, + exc_type: Optional[Type[Exception]], + exc_value: Optional[Exception], + tb: Optional[TracebackType], + ) -> bool: + if exc_type and exc_value and tb: + self.success = False + log.exception( + 'Swallowed exception:', exc_info=(exc_type, exc_value, tb) + ) + if ss := self.page.screenshot(): + save_screenshot(ss) + ntfy( + title=f'xactfetch failed for {self.bank}', + tags='warning', + attach=ss, + filename='screenshot.png', + ) + return True + + +def save_screenshot(screenshot: bytes): + now = datetime.datetime.now() + filename = now.strftime('screenshot_%Y%m%d%H%M%S.png') + log.debug('Saving browser screenshot to %s', filename) + try: + with open(filename, 'wb') as f: + f.write(screenshot) + except Exception as e: + log.error('Failed to save browser screenshot: %s', e) + else: + log.info('Browser screenshot saved as %s', filename) class CommerceBank: @@ -543,26 +600,6 @@ class Chase: firefly_import(csv, config, token) -@contextlib.contextmanager -def screenshot_failure(page: Page): - try: - yield - except Exception: - log.exception('Failed to download transactions:') - now = datetime.datetime.now() - filename = now.strftime('screenshot_%Y%m%d%H%M%S.png') - log.debug('Saving browser screenshot to %s', filename) - try: - screenshot = page.screenshot() - with open(filename, 'wb') as f: - f.write(screenshot) - except Exception as e: - log.error('Failed to save browser screenshot: %s', e) - else: - log.error('Browser screenshot saved as %s', filename) - raise - - def main() -> None: logging.basicConfig(level=logging.DEBUG) if not rbw_unlocked(): @@ -578,23 +615,9 @@ def main() -> None: browser = pw.firefox.launch(headless=headless) page = browser.new_page() failed = False - try: - with screenshot_failure(page): - download_commerce(page, end_date, token) - except Exception: - ntfy( - 'Downloading transactions from Commerce Bank failed', - tags='warning', - ) - failed = True - try: - with screenshot_failure(page): - download_chase(page, end_date, token) - except Exception: - ntfy( - 'Downloading transactions from Chase failed', - tags='warning', - ) + if not download_commerce(page, end_date, token): + failed = True + if not download_chase(page, end_date, token): failed = True raise SystemExit(1 if failed else 0)