1
0
Fork 0

xactfetch: Suppress asyncio InvalidStateError
dustin/xactfetch/pipeline/head This commit looks good Details

There is currently a [bug][0] in the Python Playwright API that causes
_asyncio_ to raise an `InvalidStateError` occasionally when the
`PlaywrightContextManager` exits.  This causes the program to exit
with a nonzero return code, even though it actually completed
successfully, which will cause the Job to be retried.  To avoid this,
we can catch and ignore the spurious exception.

I've reorganized the code a bit here because we have to wrap the whole
`with` block in the `try`/`except`; moving the contents of the block
into a function keeps the indentation level from getting out of control.

[0]: https://github.com/microsoft/playwright-python/issues/2238
master
Dustin 2024-07-11 21:16:40 -05:00
parent 3ff18d1042
commit bdcb8c93b6
1 changed files with 51 additions and 36 deletions

View File

@ -13,17 +13,21 @@ from types import TracebackType
from typing import Any, Optional, Type from typing import Any, Optional, Type
import httpx import httpx
from playwright.async_api import Page from playwright.async_api import Playwright, Page
from playwright.async_api import async_playwright from playwright.async_api import async_playwright
log = logging.getLogger('xactfetch') log = logging.getLogger('xactfetch')
NTFY_URL = os.environ['NTFY_URL'] NTFY_URL = os.environ.get('NTFY_URL', 'https://ntfy.pyrocufflink.blue')
NTFY_TOPIC = os.environ['NTFY_TOPIC'] NTFY_TOPIC = os.environ.get('NTFY_TOPIC', 'dustin')
FIREFLY_III_URL = os.environ['FIREFLY_III_URL'] FIREFLY_III_URL = os.environ.get(
FIREFLY_III_IMPORTER_URL = os.environ['FIREFLY_IMPORT_URL'] 'FIREFLY_III_URL', 'https://firefly.pyrocufflink.blue'
)
FIREFLY_III_IMPORTER_URL = os.environ.get(
'FIREFLY_IMPORT_URL', 'https://firefly-importer.pyrocufflink.blue'
)
SECRET_SOCKET_PATH = os.environ.get('SECRET_SOCKET_PATH') SECRET_SOCKET_PATH = os.environ.get('SECRET_SOCKET_PATH')
XDG_RUNTIME_DIR = os.environ.get('XDG_RUNTIME_DIR') XDG_RUNTIME_DIR = os.environ.get('XDG_RUNTIME_DIR')
@ -166,6 +170,7 @@ async def get_last_transaction_date(key: int, token: str) -> datetime.date:
'Authorization': f'Bearer {token}', 'Authorization': f'Bearer {token}',
'Accept': 'application/vnd.api+json', 'Accept': 'application/vnd.api+json',
}, },
timeout=10,
) )
r.raise_for_status() r.raise_for_status()
last_date = datetime.datetime.min last_date = datetime.datetime.min
@ -283,8 +288,12 @@ class ntfyerror:
) )
if os.environ.get('DEBUG_NTFY', '1') == '0': if os.environ.get('DEBUG_NTFY', '1') == '0':
return True return True
if ss := await self.page.screenshot(): try:
await asyncio.to_thread(save_screenshot, ss) if ss := await self.page.screenshot():
await asyncio.to_thread(save_screenshot, ss)
except Exception:
log.exception('Failed to get screenshot:')
ss = None
await ntfy( await ntfy(
message=str(exc_value), message=str(exc_value),
title=f'xactfetch failed for {self.bank}', title=f'xactfetch failed for {self.bank}',
@ -702,10 +711,7 @@ class Chase:
return secret.decode() return secret.decode()
async def amain() -> None: async def fetch_transactions(pw: Playwright, secrets: SecretsClient) -> bool:
logging.basicConfig(level=logging.DEBUG)
secrets = SecretsClient()
await secrets.connect()
log.debug('Getting Firefly III access token') log.debug('Getting Firefly III access token')
token = (await secrets.get_secret('firefly.token')).decode() token = (await secrets.get_secret('firefly.token')).decode()
import_secret = ( import_secret = (
@ -720,31 +726,40 @@ async def amain() -> None:
) )
end_date = datetime.date.today() - datetime.timedelta(days=1) end_date = datetime.date.today() - datetime.timedelta(days=1)
failed = False failed = False
async with async_playwright() as pw, secrets: browser = await pw.chromium.launch(headless=False)
browser = await pw.chromium.launch(headless=False) context = await browser.new_context()
context = await browser.new_context() await context.tracing.start(screenshots=True, snapshots=True)
await context.tracing.start(screenshots=True, snapshots=True) page = await context.new_page()
page = await context.new_page() banks = sys.argv[1:] or list(ACCOUNTS.keys())
banks = sys.argv[1:] or list(ACCOUNTS.keys()) if 'commerce' in banks:
if 'commerce' in banks: if not await download_commerce(
if not await download_commerce( page, secrets, end_date, token, importer
page, secrets, end_date, token, importer ):
): failed = True
failed = True if 'chase' in banks:
if 'chase' in banks: if not await download_chase(page, secrets, end_date, token, importer):
if not await download_chase( failed = True
page, secrets, end_date, token, importer if failed:
): await context.tracing.stop(path='trace.zip')
failed = True with open('trace.zip', 'rb') as f:
if failed: await ntfy(
await context.tracing.stop(path='trace.zip') 'Downloading one or more transaction lists failed.',
with open('trace.zip', 'rb') as f: attach=f.read(),
await ntfy( filename='trace.zip',
'Downloading one or more transaction lists failed.', )
attach=f.read(), return failed
filename='trace.zip',
)
raise SystemExit(1 if failed else 0) async def amain() -> None:
logging.basicConfig(level=logging.DEBUG)
async with SecretsClient() as secrets:
try:
async with async_playwright() as pw:
failed = await fetch_transactions(pw, secrets)
raise SystemExit(1 if failed else 0)
except asyncio.exceptions.InvalidStateError as e:
log.debug('Ignoring exception: %s', e, exc_info=sys.exc_info())
def main(): def main():