add bmo parser, some refactors
This commit is contained in:
73
parsers.py
73
parsers.py
@@ -1,12 +1,14 @@
|
||||
import re
|
||||
from abc import ABC
|
||||
from abc import abstractmethod
|
||||
from datetime import datetime
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from email.message import EmailMessage
|
||||
from email.message import Message
|
||||
from email.message import EmailMessage, Message
|
||||
from logging import info
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from model import Transaction
|
||||
|
||||
|
||||
@@ -25,7 +27,7 @@ class TransactionParser(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def extract(self, msg: EmailMessage) -> Transaction:
|
||||
def extract(self, msg: EmailMessage) -> Optional[Transaction]:
|
||||
"""
|
||||
Extracts transaction details from the given email message.
|
||||
|
||||
@@ -59,11 +61,11 @@ class RogersBankParser(TransactionParser):
|
||||
and msg["Subject"] == "Purchase amount alert"
|
||||
)
|
||||
|
||||
def extract(self, msg: EmailMessage) -> Transaction:
|
||||
body = msg.get_body()
|
||||
def extract(self, msg: EmailMessage) -> Optional[Transaction]:
|
||||
body = msg.get_content()
|
||||
if body is None:
|
||||
raise TransactionParsingFailed("No body of message found")
|
||||
matches = self.EXTRACT_RE.search(body.as_string())
|
||||
matches = self.EXTRACT_RE.search(body)
|
||||
if matches is None:
|
||||
raise TransactionParsingFailed("No matches for extraction RE")
|
||||
amount = Decimal(matches[1])
|
||||
@@ -88,27 +90,68 @@ class MBNAParser(TransactionParser):
|
||||
def __init__(self, account_id: UUID):
|
||||
self.account_id = account_id
|
||||
|
||||
def match(self, msg: Message):
|
||||
def match(self, msg: Message) -> bool:
|
||||
return (
|
||||
msg["From"] == "MBNA Notifications <noreply@mbna.ca>"
|
||||
and msg["Subject"] == "MBNA - Transaction Alert"
|
||||
)
|
||||
|
||||
def extract(self, msg: EmailMessage) -> Transaction:
|
||||
body = msg.get_body()
|
||||
def extract(self, msg: EmailMessage) -> Optional[Transaction]:
|
||||
body = msg.get_content()
|
||||
if body is None:
|
||||
raise TransactionParsingFailed("No body of message found")
|
||||
matches = self.EXTRACT_RE.search(body.as_string())
|
||||
matches = self.EXTRACT_RE.search(body)
|
||||
if matches is None:
|
||||
raise TransactionParsingFailed("No matches for extraction RE")
|
||||
amount = Decimal(matches[1])
|
||||
payee = matches[2]
|
||||
date = matches[5]
|
||||
date_raw = matches[5]
|
||||
return Transaction(
|
||||
account=self.account_id,
|
||||
date=date,
|
||||
date=date.fromisoformat(date_raw),
|
||||
amount=amount,
|
||||
payee=payee,
|
||||
notes="via email",
|
||||
imported_id=msg["Message-ID"],
|
||||
)
|
||||
|
||||
|
||||
class BMOParser(TransactionParser):
|
||||
EXTRACT_RE = re.compile(
|
||||
r"We want to let you know that a (withdrawal|deposit) of\s+\$(\d+\.\d{2})\s+has been made (?:to|from) your account ending\s+in\s+(\d{3})", # noqa: E501
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
def __init__(self, account_map: dict[int, UUID]):
|
||||
self._account_map = account_map
|
||||
|
||||
def match(self, msg: Message) -> bool:
|
||||
return msg["From"] == "bmoalerts@bmo.com"
|
||||
|
||||
def extract(self, msg: EmailMessage) -> Optional[Transaction]:
|
||||
body = msg.get_content()
|
||||
if body is None:
|
||||
raise TransactionParsingFailed("No body of message found")
|
||||
soup = BeautifulSoup(body, "html.parser")
|
||||
matches = self.EXTRACT_RE.search(soup.get_text())
|
||||
if matches is None:
|
||||
raise TransactionParsingFailed("No matches for extraction RE")
|
||||
|
||||
amount = Decimal(matches[2])
|
||||
if matches[1] == "withdrawal":
|
||||
amount = amount * -1
|
||||
date_raw = msg["Date"]
|
||||
date = datetime.strptime(date_raw, "%a, %d %b %Y %H:%M:%S %z").date()
|
||||
account_ref = int(matches[3])
|
||||
if account_ref not in self._account_map:
|
||||
info("Account %s not in account map", account_ref)
|
||||
return None
|
||||
account_id = self._account_map[account_ref]
|
||||
return Transaction(
|
||||
account=account_id,
|
||||
date=date,
|
||||
amount=amount,
|
||||
payee="",
|
||||
notes="via email",
|
||||
imported_id=msg["Message-ID"],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user