refactor config / parser loading
This commit is contained in:
		
							
								
								
									
										80
									
								
								watcher.py
									
									
									
									
									
								
							
							
						
						
									
										80
									
								
								watcher.py
									
									
									
									
									
								
							@@ -1,9 +1,11 @@
 | 
				
			|||||||
#!/usr/bin/env python
 | 
					#!/usr/bin/env python
 | 
				
			||||||
import asyncio
 | 
					import asyncio
 | 
				
			||||||
import email.policy
 | 
					import email.policy
 | 
				
			||||||
 | 
					import importlib.util
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import ssl
 | 
					import ssl
 | 
				
			||||||
 | 
					from dataclasses import dataclass
 | 
				
			||||||
from email.message import EmailMessage
 | 
					from email.message import EmailMessage
 | 
				
			||||||
from imaplib import IMAP4
 | 
					from imaplib import IMAP4
 | 
				
			||||||
from logging import debug
 | 
					from logging import debug
 | 
				
			||||||
@@ -14,19 +16,12 @@ from os import getenv
 | 
				
			|||||||
from pprint import pprint
 | 
					from pprint import pprint
 | 
				
			||||||
from typing import cast
 | 
					from typing import cast
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from config import PARSERS
 | 
					 | 
				
			||||||
from model import Transaction
 | 
					from model import Transaction
 | 
				
			||||||
from parsers import TransactionParsingFailed
 | 
					from parsers import TransactionParsingFailed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
IMAP_SERVER = getenv("IMAP_SERVER")
 | 
					 | 
				
			||||||
IMAP_PORT = int(getenv("IMAP_PORT", 143))
 | 
					 | 
				
			||||||
IMAP_USER = getenv("IMAP_USER")
 | 
					 | 
				
			||||||
IMAP_PASS = getenv("IMAP_PASS")
 | 
					 | 
				
			||||||
IMAP_MAILBOX = getenv("IMAP_MAILBOX", "INBOX")
 | 
					 | 
				
			||||||
IMAP_INTERVAL = float(getenv("IMAP_INTERVAL", 300))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CONFIG_PATH = getenv("CONFIG_PATH", "/data/config.py")
 | 
				
			||||||
ACTUAL_PATH = "./cli.js"
 | 
					ACTUAL_PATH = "./cli.js"
 | 
				
			||||||
 | 
					 | 
				
			||||||
TIMEOUT = 30
 | 
					TIMEOUT = 30
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -36,7 +31,43 @@ async def ticker(interval: float):
 | 
				
			|||||||
        await asyncio.sleep(interval)
 | 
					        await asyncio.sleep(interval)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def submit_transaction(t: Transaction):
 | 
					def load_config_module(path: str):
 | 
				
			||||||
 | 
					    spec = importlib.util.spec_from_file_location("config", path)
 | 
				
			||||||
 | 
					    config = importlib.util.module_from_spec(spec)
 | 
				
			||||||
 | 
					    spec.loader.exec_module(config)
 | 
				
			||||||
 | 
					    return config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class AppConfig:
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def from_environ() -> "AppConfig":
 | 
				
			||||||
 | 
					        return AppConfig(
 | 
				
			||||||
 | 
					            imap_server=getenv("IMAP_SERVER"),
 | 
				
			||||||
 | 
					            imap_port=int(getenv("IMAP_PORT", 143)),
 | 
				
			||||||
 | 
					            imap_user=getenv("IMAP_USER"),
 | 
				
			||||||
 | 
					            imap_pass=getenv("IMAP_PASS"),
 | 
				
			||||||
 | 
					            imap_mailbox=getenv("IMAP_MAILBOX", "INBOX"),
 | 
				
			||||||
 | 
					            imap_interval=float(getenv("IMAP_INTERVAL", 300)),
 | 
				
			||||||
 | 
					            imap_starttls=bool(getenv("IMAP_STARTTLS", True)),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    imap_server: str
 | 
				
			||||||
 | 
					    imap_user: str
 | 
				
			||||||
 | 
					    imap_pass: str
 | 
				
			||||||
 | 
					    imap_port: int = 143
 | 
				
			||||||
 | 
					    imap_mailbox: str = "INBOX"
 | 
				
			||||||
 | 
					    imap_interval: int = 300
 | 
				
			||||||
 | 
					    imap_starttls: bool = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class App:
 | 
				
			||||||
 | 
					    def __init__(self, app_config: AppConfig, parser_config_path: str):
 | 
				
			||||||
 | 
					        config_mod = load_config_module(parser_config_path)
 | 
				
			||||||
 | 
					        self._parsers = config_mod.PARSERS
 | 
				
			||||||
 | 
					        self._config = app_config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def submit_transaction(self, t: Transaction):
 | 
				
			||||||
        cmd = (
 | 
					        cmd = (
 | 
				
			||||||
            ACTUAL_PATH
 | 
					            ACTUAL_PATH
 | 
				
			||||||
            + f' -a "{t.account}"'
 | 
					            + f' -a "{t.account}"'
 | 
				
			||||||
@@ -56,8 +87,7 @@ async def submit_transaction(t: Transaction):
 | 
				
			|||||||
        if proc.returncode != 0:
 | 
					        if proc.returncode != 0:
 | 
				
			||||||
            error("Submitting to actual failed: %s", stdout)
 | 
					            error("Submitting to actual failed: %s", stdout)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def process_message(self, msg_b: bytes):
 | 
				
			||||||
async def process_message(msg_b: bytes):
 | 
					 | 
				
			||||||
        debug("parsing message")
 | 
					        debug("parsing message")
 | 
				
			||||||
        msg = cast(
 | 
					        msg = cast(
 | 
				
			||||||
            EmailMessage, email.message_from_bytes(msg_b, policy=email.policy.default)
 | 
					            EmailMessage, email.message_from_bytes(msg_b, policy=email.policy.default)
 | 
				
			||||||
@@ -70,41 +100,41 @@ async def process_message(msg_b: bytes):
 | 
				
			|||||||
            msg.get("Subject", "<no subject>"),
 | 
					            msg.get("Subject", "<no subject>"),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for parser in PARSERS:
 | 
					        for parser in self._parsers:
 | 
				
			||||||
            debug("Running parser %s", type(parser).__name__)
 | 
					            debug("Running parser %s", type(parser).__name__)
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                if parser.match(msg):
 | 
					                if parser.match(msg):
 | 
				
			||||||
                    info("Parser %s claimed message", type(parser).__name__)
 | 
					                    info("Parser %s claimed message", type(parser).__name__)
 | 
				
			||||||
                    trans = parser.extract(msg)
 | 
					                    trans = parser.extract(msg)
 | 
				
			||||||
                    info("Submitting transaction to Actual: %s", trans)
 | 
					                    info("Submitting transaction to Actual: %s", trans)
 | 
				
			||||||
                await submit_transaction(trans)
 | 
					                    await self.submit_transaction(trans)
 | 
				
			||||||
            except TransactionParsingFailed as e:
 | 
					            except TransactionParsingFailed as e:
 | 
				
			||||||
                warning("Unable to parse message %s", e)
 | 
					                warning("Unable to parse message %s", e)
 | 
				
			||||||
            except Exception as e:
 | 
					            except Exception as e:
 | 
				
			||||||
                warning("Unexpected exception %s", e)
 | 
					                warning("Unexpected exception %s", e)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def poll_imap(self):
 | 
				
			||||||
async def poll_imap():
 | 
					 | 
				
			||||||
        info("polling mailbox")
 | 
					        info("polling mailbox")
 | 
				
			||||||
    with IMAP4(IMAP_SERVER, IMAP_PORT, 30) as M:
 | 
					        with IMAP4(self._config.imap_server, self._config.imap_port, TIMEOUT) as M:
 | 
				
			||||||
 | 
					            if self._config.imap_starttls:
 | 
				
			||||||
                context = ssl.create_default_context()
 | 
					                context = ssl.create_default_context()
 | 
				
			||||||
                M.starttls(context)
 | 
					                M.starttls(context)
 | 
				
			||||||
        M.login(IMAP_USER, IMAP_PASS)
 | 
					            M.login(self._config.imap_user, self._config.imap_pass)
 | 
				
			||||||
        M.select(IMAP_MAILBOX)
 | 
					            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():
 | 
					            for msg_id in m_set[0].split():
 | 
				
			||||||
                debug("Retrieving msg id %s", msg_id)
 | 
					                debug("Retrieving msg id %s", msg_id)
 | 
				
			||||||
                status, msg = M.fetch(msg_id, "(RFC822)")
 | 
					                status, msg = M.fetch(msg_id, "(RFC822)")
 | 
				
			||||||
            await process_message(msg[0][1])
 | 
					                await self.process_message(msg[0][1])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def run(self):
 | 
				
			||||||
async def app():
 | 
					        async for tick in ticker(self._config.imap_interval):
 | 
				
			||||||
    async for tick in ticker(IMAP_INTERVAL):
 | 
					            await self.poll_imap()
 | 
				
			||||||
        await poll_imap()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == "__main__":
 | 
					if __name__ == "__main__":
 | 
				
			||||||
    logging.basicConfig(level=getenv("LOG_LEVEL", "INFO"))
 | 
					    logging.basicConfig(level=getenv("LOG_LEVEL", "INFO"))
 | 
				
			||||||
    debug("Parsers: %s", PARSERS)
 | 
					    app_config = AppConfig.from_environ()
 | 
				
			||||||
    asyncio.run(app())
 | 
					    app = App(app_config, CONFIG_PATH)
 | 
				
			||||||
 | 
					    asyncio.run(app.run())
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user