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 attachedmaster
parent
7683ff5760
commit
7cab766c38
125
xactfetch.py
125
xactfetch.py
|
@ -1,4 +1,3 @@
|
||||||
import contextlib
|
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
@ -15,7 +14,6 @@ from typing import Any, Optional, Type
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from playwright.sync_api import Page
|
from playwright.sync_api import Page
|
||||||
from playwright.sync_api import TimeoutError as PlaywrightTimeout
|
|
||||||
from playwright.sync_api import sync_playwright
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,21 +33,34 @@ ACCOUNTS = {
|
||||||
|
|
||||||
|
|
||||||
def ntfy(
|
def ntfy(
|
||||||
message: str,
|
message: Optional[str] = None,
|
||||||
topic: str = NTFY_TOPIC,
|
topic: str = NTFY_TOPIC,
|
||||||
title: Optional[str] = None,
|
title: Optional[str] = None,
|
||||||
tags: Optional[str] = None,
|
tags: Optional[str] = None,
|
||||||
|
attach: Optional[bytes] = None,
|
||||||
|
filename: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
assert message or attach
|
||||||
headers = {
|
headers = {
|
||||||
'Title': title or 'xactfetch',
|
'Title': title or 'xactfetch',
|
||||||
}
|
}
|
||||||
if tags:
|
if tags:
|
||||||
headers['Tags'] = tags
|
headers['Tags'] = tags
|
||||||
r = requests.post(
|
url = f'{NTFY_URL}/{topic}'
|
||||||
f'{NTFY_URL}/{topic}',
|
if message:
|
||||||
headers=headers,
|
r = requests.post(
|
||||||
data=message,
|
url,
|
||||||
)
|
headers=headers,
|
||||||
|
data=message,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if filename:
|
||||||
|
headers['Filename'] = filename
|
||||||
|
r = requests.put(
|
||||||
|
url,
|
||||||
|
headers=headers,
|
||||||
|
data=attach,
|
||||||
|
)
|
||||||
r.raise_for_status()
|
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)
|
return last_date.date() + datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
|
||||||
def download_chase(page: Page, end_date: datetime.date, token: str) -> None:
|
def download_chase(page: Page, end_date: datetime.date, token: str) -> bool:
|
||||||
with Chase(page) as c:
|
with Chase(page) as c, ntfyerror('Chase', page) as r:
|
||||||
c.login()
|
c.login()
|
||||||
key = ACCOUNTS['chase']
|
key = ACCOUNTS['chase']
|
||||||
try:
|
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',
|
'Skipping Chase account: could not get last transaction: %s',
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
return
|
return False
|
||||||
if start_date >= end_date:
|
if start_date >= end_date:
|
||||||
log.info(
|
log.info(
|
||||||
'Skipping Chase account: last transaction was %s',
|
'Skipping Chase account: last transaction was %s',
|
||||||
start_date,
|
start_date,
|
||||||
)
|
)
|
||||||
return
|
return True
|
||||||
csv = c.download_transactions(start_date, end_date)
|
csv = c.download_transactions(start_date, end_date)
|
||||||
log.info('Importing transactions from Chase into Firefly III')
|
log.info('Importing transactions from Chase into Firefly III')
|
||||||
c.firefly_import(csv, key, token)
|
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')
|
log.info('Downloading transaction lists from Commerce Bank')
|
||||||
csvs = []
|
csvs = []
|
||||||
with CommerceBank(page) as c:
|
with CommerceBank(page) as c, ntfyerror('Commerce Bank', page) as r:
|
||||||
c.login()
|
c.login()
|
||||||
for name, key in ACCOUNTS['commerce'].items():
|
for name, key in ACCOUNTS['commerce'].items():
|
||||||
try:
|
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')
|
log.info('Importing transactions from Commerce Bank into Firefly III')
|
||||||
for key, csv in csvs:
|
for key, csv in csvs:
|
||||||
c.firefly_import(csv, key, token)
|
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:
|
class CommerceBank:
|
||||||
|
@ -543,26 +600,6 @@ class Chase:
|
||||||
firefly_import(csv, config, token)
|
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:
|
def main() -> None:
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
if not rbw_unlocked():
|
if not rbw_unlocked():
|
||||||
|
@ -578,23 +615,9 @@ def main() -> None:
|
||||||
browser = pw.firefox.launch(headless=headless)
|
browser = pw.firefox.launch(headless=headless)
|
||||||
page = browser.new_page()
|
page = browser.new_page()
|
||||||
failed = False
|
failed = False
|
||||||
try:
|
if not download_commerce(page, end_date, token):
|
||||||
with screenshot_failure(page):
|
failed = True
|
||||||
download_commerce(page, end_date, token)
|
if not download_chase(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',
|
|
||||||
)
|
|
||||||
failed = True
|
failed = True
|
||||||
raise SystemExit(1 if failed else 0)
|
raise SystemExit(1 if failed else 0)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue