errands
small jobs the runtime does for the studio.
git clone https://owenrusk.dev/errands.git
commit 35a8fcecb23a553e4b270ef7399006aa8c71cc50 parent c2491a364fe2c71f63a54397c9fd5cc3a7ff280d author Owen Rusk <owen@papermothgames.com> date 2025-03-04 15:15:39 -0600
invoices: a ledger in state, no duplicates
| errands/invoices.py | +41 | -2 |
| tests/test_invoices.py | +20 | -1 |
diff --git a/errands/invoices.py b/errands/invoices.py index 626cd52..e6d6220 100644 --- a/errands/invoices.py +++ b/errands/invoices.py @@ -1,7 +1,10 @@-# bills that come in to the support inbox.+# bills that come in to the support inbox, kept in a ledger. +import json import re-from dataclasses import dataclass+from dataclasses import asdict, dataclass +from pathlib import Path +from . import mail, settings from .mail import Message PAYEES = { @@ -25,3 +28,39 @@ def read(message: Message) -> Invoice | None: if payee is None or found is None: return None return Invoice(payee, message.sender, message.subject, found.group(1).replace(",", "")) + + +def ledger() -> Path: + return settings.STATE / "invoices.json" + + +def load() -> list[Invoice]: + path = ledger() + if not path.exists(): + return [] + return [Invoice(**row) for row in json.loads(path.read_text())] + + +def save(invoices: list[Invoice]) -> None: + path = ledger() + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps([asdict(invoice) for invoice in invoices], indent=2) + "\n") + + +def record(invoice: Invoice) -> bool: + invoices = load() + key = (invoice.sender, invoice.subject, invoice.amount) + if any((i.sender, i.subject, i.amount) == key for i in invoices): + return False + invoices.append(invoice) + save(invoices) + return True + + +def from_inbox(unread=mail.unread) -> int: + added = 0 + for message in unread(): + invoice = read(message) + if invoice is not None and record(invoice): + added += 1 + return added diff --git a/tests/test_invoices.py b/tests/test_invoices.py index f6ada3c..b6de9bd 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -1,6 +1,8 @@ +import tempfile import unittest +from pathlib import Path-from errands import invoices+from errands import invoices, settings from errands.mail import Message @@ -19,3 +21,20 @@ class ReadTest(unittest.TestCase): def test_no_amount(self): self.assertIsNone(invoices.read(message(body="the heating is on from monday.\n"))) + + +class LedgerTest(unittest.TestCase): + def setUp(self): + self.dir = tempfile.TemporaryDirectory() + self.state = settings.STATE + settings.STATE = Path(self.dir.name) + + def tearDown(self): + settings.STATE = self.state + self.dir.cleanup() + + def test_from_inbox_records_bills_once(self): + inbox = [message(), message(sender="someone@example.com")] + self.assertEqual(invoices.from_inbox(lambda: inbox), 1) + self.assertEqual(invoices.from_inbox(lambda: inbox), 0) + self.assertEqual([i.payee for i in invoices.load()], ["Calder & Webb"])