add neo parser

This commit is contained in:
2026-08-24 09:26:32 -07:00
parent a15970d292
commit d414d0cf09
2 changed files with 43 additions and 3 deletions
+42 -2
View File
@@ -1,10 +1,11 @@
import re import re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Sequence
from datetime import date, datetime from datetime import date, datetime
from decimal import Decimal from decimal import Decimal
from email.message import EmailMessage, Message from email.message import EmailMessage, Message
from logging import info, warning from logging import debug, info, warning
from typing import Any, Optional, Sequence from typing import Any, Optional
from uuid import UUID from uuid import UUID
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
@@ -308,3 +309,42 @@ class ScotiaBankParser(TransactionParser):
return self.extract_transaction(msg) return self.extract_transaction(msg)
info("Subject `%s` didn't match any extractors", msg["Subject"]) info("Subject `%s` didn't match any extractors", msg["Subject"])
return None return None
class NeoFinancialParser(TransactionParser):
PURCHASE_EXTRACT_RE = re.compile(
r"your purchase of \$(?P<amount>[0-9]+\.[0-9]{2}) at (?P<payee>.+) on (?P<date>(January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}(st|nd|rd|th)?, \d{4})", # noqa: E501
re.MULTILINE,
)
def __init__(self, account_id: UUID):
self.account_id = account_id
def match(self, msg: Message) -> bool:
return msg["From"] == "Neo Financial <info@neofinancial.com>" and msg[
"Subject"
].startswith("You earned")
def extract_purchase(self, msg: EmailMessage) -> Optional[Transaction]:
content = self.get_content(msg, preferencelist=["plain"])
matches = self.PURCHASE_EXTRACT_RE.search(content)
if matches is None:
debug(f"No match on\n\n{content}")
raise TransactionParsingFailed("no matches for purchase extraction RE")
amount = Decimal(matches["amount"].replace(",", "")) * -1
# Use date from email header, it seems to match the body and includes TZ info
date = parse_email_time(msg["Date"]).date()
return Transaction(
account=self.account_id,
date=date,
amount=amount,
payee=matches["payee"],
notes="via email",
imported_id=msg["Message-ID"],
)
def extract(self, msg: EmailMessage) -> Optional[Transaction]:
if msg["Subject"].startswith("You earned"):
return self.extract_purchase(msg)
return None
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "actual-imap-poll" name = "actual-imap-poll"
version = "0.1.3" version = "0.2.0"
description = "Poll an IMAP mailbox looking for transactions to submit to Actual Budget" description = "Poll an IMAP mailbox looking for transactions to submit to Actual Budget"
authors = [ authors = [
{name = "Keenan Tims",email = "ktims@gotroot.ca"} {name = "Keenan Tims",email = "ktims@gotroot.ca"}