add bmo parser, some refactors

This commit is contained in:
2025-04-24 18:17:20 -07:00
parent bb1fcb3d94
commit d1845cc5d9
6 changed files with 94 additions and 43 deletions

View File

@@ -6,20 +6,13 @@ import logging
import os
import ssl
from dataclasses import dataclass
from email.message import EmailMessage
from imaplib import IMAP4
from logging import debug
from logging import error
from logging import info
from logging import warning
from logging import debug, error, info, warning
from os import getenv
from pprint import pprint
from typing import cast
from model import Transaction
from parsers import TransactionParsingFailed
CONFIG_PATH = getenv("CONFIG_PATH", "/data/config.py")
ACTUAL_PATH = "./cli.js"
TIMEOUT = 30
@@ -33,6 +26,8 @@ async def ticker(interval: float):
def load_config_module(path: str):
spec = importlib.util.spec_from_file_location("config", path)
if spec is None or spec.loader is None:
raise ImportError(f"Could not find config module at {path}")
config = importlib.util.module_from_spec(spec)
spec.loader.exec_module(config)
return config
@@ -43,12 +38,12 @@ class AppConfig:
@staticmethod
def from_environ() -> "AppConfig":
return AppConfig(
imap_server=getenv("IMAP_SERVER"),
imap_server=os.environ["IMAP_SERVER"],
imap_port=int(getenv("IMAP_PORT", 143)),
imap_user=getenv("IMAP_USER"),
imap_pass=getenv("IMAP_PASS"),
imap_user=os.environ["IMAP_USER"],
imap_pass=os.environ["IMAP_PASS"],
imap_mailbox=getenv("IMAP_MAILBOX", "INBOX"),
imap_interval=float(getenv("IMAP_INTERVAL", 300)),
imap_interval=int(getenv("IMAP_INTERVAL", 300)),
imap_starttls=bool(getenv("IMAP_STARTTLS", True)),
)
@@ -71,10 +66,10 @@ class App:
cmd = (
ACTUAL_PATH
+ f' -a "{t.account}"'
+ f' -p "{t.payee}"'
+ (f' -p "{t.payee}"' if t.payee else "")
+ f' -m "{t.amount}"'
+ f' -d "{t.date}"'
f' -n "{t.notes}"'
+ (f' -n "{t.notes}"' if t.notes else "")
)
debug("Actual command: %s", cmd)
proc = await asyncio.create_subprocess_shell(
@@ -83,16 +78,13 @@ class App:
stderr=asyncio.subprocess.STDOUT,
stdout=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
stdout, _stderr = await proc.communicate()
if proc.returncode != 0:
error("Submitting to actual failed: %s", stdout)
async def process_message(self, msg_b: bytes):
debug("parsing message")
msg = cast(
EmailMessage, email.message_from_bytes(msg_b, policy=email.policy.default)
)
pprint(msg)
msg = email.message_from_bytes(msg_b, policy=email.policy.default)
info(
"Found message from %s to %s subject %s",
msg.get("From", "<unknown>"),
@@ -106,8 +98,11 @@ class App:
if parser.match(msg):
info("Parser %s claimed message", type(parser).__name__)
trans = parser.extract(msg)
info("Submitting transaction to Actual: %s", trans)
await self.submit_transaction(trans)
if trans is not None:
info("Submitting transaction to Actual: %s", trans)
await self.submit_transaction(trans)
else:
warning("Parser %s returned None", type(parser).__name__)
except TransactionParsingFailed as e:
warning("Unable to parse message %s", e)
except Exception as e:
@@ -121,15 +116,23 @@ class App:
M.starttls(context)
M.login(self._config.imap_user, self._config.imap_pass)
M.select(self._config.imap_mailbox)
status, m_set = M.search(None, "UNSEEN")
_status, m_set = M.search(None, "UNSEEN")
for msg_id in m_set[0].split():
debug("Retrieving msg id %s", msg_id)
status, msg = M.fetch(msg_id, "(RFC822)")
await self.process_message(msg[0][1])
_status, msg = M.fetch(msg_id, "(RFC822)")
if _status != "OK" or msg[0] is None:
error("Unable to fetch message %s", msg_id)
continue
msg_body = msg[0][1]
if isinstance(msg_body, int):
error("Unable to fetch message %s", msg_id)
continue
debug("Processing message %s", msg_id)
await self.process_message(msg_body)
async def run(self):
async for tick in ticker(self._config.imap_interval):
async for _ in ticker(self._config.imap_interval):
await self.poll_imap()