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
     LiabilitiesManual 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 reQuery 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)