add bmo parser, some refactors
This commit is contained in:
53
watcher.py
53
watcher.py
@@ -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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user