Update your financial ledger by email in python
Abstract
Each time I buy something and pay it later, my expenses and liabilities increase.
In this blog post I’m using the example of the reservation of a tennis court.
Once a year, the club sends an invoice for every reservation made. In order to reflect this in my finances, I note each reservation as a transaction into my ledger. The final goal is to create such a transaction into my ledger file completely automatic.
Goal
2021/3/27 Tennis
Expenses CHF 10
Liabilities
Manual process briefly explained
In order to keep my ledger up, a couple of steps are necessary. For a better understanding of what’s happening in the python script below, I briefly go over the manual process:
Query mu for emails
mu find from:no-reply@t.gotcourts.com subject:Reservation --fields 'd s l'
Read the email headers
Backslash and line-break added manually for better reading
Sat 27 Mar 2021 06:54:07 PM CET \
GotCourts - Reservation confirmation \
/home/ra/Maildir/AnyOtherProvider/INBOX/new/1616867898.105333_1.x,U=3935:2,
Extract the html content if plain-text is not provided
mu extract --parts=1 /home/ra/Maildir/AnyOtherProvider/INBOX/new/1616867898.105333_1.x,U=3935:2,
Read html content
</p><p>Partner: </p><p> </p><p>Price: 10 CHF<p>
Write the ledger post
Parse and fetch all information. Write the post as shown in Goal.
Script - Step by step
We start by importing the necessary modules
import subprocess
from datetime import datetime
import re
Query mu and return the fields (d)ate
, (s)ubject
and fi(l)e
MU_FIND_QUERY = "mu find " + myemailAlias + " --fields 'd s l'"
Run the current mu query
def mu_command(MU_FIND_QUERY):
return subprocess.check_output((MU_FIND_QUERY), shell=True).splitlines()
Create a list of results returned by mu
EMAILS = [mystr.decode("utf-8").split(" ") for mystr in mu_command(MU_FIND_QUERY)]
In our case, the sender’s unable to provide plain-text. We tackle this obstacle by extracting the html part.
MU_EXTRACT_QUERY = "mu extract --target-dir=/tmp/ --overwrite --parts=1 "
We subsequently store it temporarily /tmp/1.msgpart
. Finally we read and
return it’s content.
def html_extract(file):
mu_command(MU_EXTRACT_QUERY + file)
with open("/tmp/1.msgpart") as f:
return f.read()
We parse the content for the price. A match contains at least one positive
integer followed by another zero based integer followed by CHF
. Thus e.g. 10 CHF
is a match.
PRICE_REG = r"[1-9]+[0-9]*(?= CHF)"
If found, we return the price match
def search_price(file):
text = html_extract(file)
if text:
match = re.search(PRICE_REG, text)
if match:
return match.group(0)
mu
returns the date’s month name abbreviated in English e.g. JAN
. Thankfully python’s
strptime method cover’s this format.
def month(text):
return str(datetime.strptime(text, "%b").month)
Now we’re all set regarding the necessary information to create a proper ledger posting.
For each parsed email we write/print a posting to our ledger file. But only if the email contains a price.
{email[3]}/{month(email[2])}/{email[1]}
prints the year/month/day e.g.
2021/12/31
.
email[-1]
prints the emails file path, we pass it as argument to get the
price by {search_price(email[-1])}
.
def parse(query_file):
with open(query_file, "w") as f:
[
print(f'''\
{email[3]}/{month(email[2])}/{email[1]} Tennis
Expenses:Tennis CHF {search_price(email[-1])}
Liabilities:Tennis
''',
file=f) for email in EMAILS if search_price(email[-1])
]
Catch and print any exception
try:
parse(query_file)
except BaseException as e:
print(e)
Complete script
import subprocess
from datetime import datetime
import re
MU_FIND_QUERY = "mu find " + myemailAlias + " --fields 'd s l'"
def mu_command(MU_FIND_QUERY):
return subprocess.check_output((MU_FIND_QUERY), shell=True).splitlines()
EMAILS = [mystr.decode("utf-8").split(" ") for mystr in mu_command(MU_FIND_QUERY)]
MU_EXTRACT_QUERY = "mu extract --target-dir=/tmp/ --overwrite --parts=1 "
def html_extract(file):
mu_command(MU_EXTRACT_QUERY + file)
with open("/tmp/1.msgpart") as f:
return f.read()
PRICE_REG = r"[1-9]+[0-9]*(?= CHF)"
def search_price(file):
text = html_extract(file)
if text:
match = re.search(PRICE_REG, text)
if match:
return match.group(0)
def month(text):
return str(datetime.strptime(text, "%b").month)
def parse(query_file):
with open(query_file, "w") as f:
[
print(f'''\
{email[3]}/{month(email[2])}/{email[1]} Tennis
Expenses:Tennis CHF {search_price(email[-1])}
Liabilities:Tennis
''',
file=f) for email in EMAILS if search_price(email[-1])
]
try:
parse(query_file)
except BaseException as e:
print(e)