Finalized the this month script, which will be sent weekly via text to me and necessary parties. The last month and new_month_check transactions are also cleaned up and working. The next tasks are to sunset the import of moneyed as reading the actual-py docs, I found that I can do transactions.get_amount() which will return a decimal formatted number instead of needing to change the strings, etc. Will also need to clean up the files so they are more organized for public release.

This commit is contained in:
norm
2025-11-11 09:47:47 -05:00
parent b1d6aab9e3
commit b70fcbb185
10 changed files with 119 additions and 152 deletions

0
README.md Normal file
View File

Binary file not shown.

View File

@ -13,9 +13,14 @@ import pprint
pp=pprint.PrettyPrinter(indent=4, sort_dicts=False)
load_dotenv()
THISMONTH = datetime.now()
# datestring= "2025-08-06 13:57:57"
# format = '%Y-%m-%d %H:%M:%S'
# THISMONTH = datetime.strptime(datestring, format)
MONTHMINUSONE = THISMONTH - relativedelta(months=1)
MONTHMINUSTWO = THISMONTH - relativedelta(months=2)
ONBUDGETACCOUNTS = os.getenv('ONBUDGETACCOUNTS')
LASTM_STRING = datetime.strftime(MONTHMINUSONE, '%B')
TWOM_STRING = datetime.strftime(MONTHMINUSTWO, '%B')
ONBUDGETACCOUNTS = list(os.getenv('ONBUDGETACCOUNTS'))
def compare_months(actual):
budget_one = main(actual, MONTHMINUSONE)
@ -23,7 +28,18 @@ def compare_months(actual):
x = find_percent_diff(budget_one, budget_two)
first_sort = dict(sorted(budget_one.items(), key=lambda item: item[1]['amount_spent']))
reduced_sort = dict(list(first_sort.items())[:5])
print(x)
text_list = []
for i, k in reduced_sort.items():
items = (i, format_money(k['amount_spent']))
text_list.append(items)
formatted = str(text_list)[1:-1]
replacements = [('\\(', ''), ('\\)',''), ("'", ''), ('"',''), ('(\\d)\\,\\s\\b', '\\1\\n'), ('\\b, (\\-)', ': \\1')]
for old, new in replacements:
formatted = re.sub(fr"{old}", fr"{new}", formatted)
x = x.replace('"','')
print(f"""**This is an automated message!**\nHere's your budget status. Here are your top 5 spending categories from last month:\n\n{formatted}\n\nAnd here are the top 5 categories with the biggest increase in spending between last month and the previous month:\n\n{x}""")
def find_percent_diff(lastmonth, twomonths):
@ -34,10 +50,23 @@ def find_percent_diff(lastmonth, twomonths):
absolute = v['amount_spent'] - x['amount_spent']
average = (v['amount_spent'] + x['amount_spent'])/2
diff = round(absolute/average, 2)
diffs.append(( k, diff) )
diffs.append((k, diff, (LASTM_STRING, v['amount_spent']), (TWOM_STRING, x['amount_spent'])))
sort = sorted(diffs, reverse=True, key=lambda item: item[1])
return sort
first = sort[:5]
second = []
for t in first:
t1 = str(f"{float(t[1])}%")
t2 = (t[2][0], format_money(t[2][1]))
t3 = (t[3][0], format_money(t[3][1]))
newlist = f"{t[0]}, {t1}, {t2}, {t3}"
second.append(newlist)
formatted = str(second)[1:-1]
replacements = [('\\(', ''), ('\\)',''), ("'", ''), ('", ','\\n'), ('(\\%), ','\\1 --> '), ('(\\d)\\,\\s\\b', '\\1 --> '), ('([a-z]),', '\\1:')]
for old, new in replacements:
formatted = re.sub(fr"{old}", fr"{new}", formatted)
return formatted
def simple_track(actual):

15
min_test.py Normal file
View File

@ -0,0 +1,15 @@
from dotenv import load_dotenv
from actual import Actual
import os
load_dotenv()
print("hold onto your butts")
with Actual(
base_url=os.getenv('BASEURL'),
password=os.getenv('PASSWORD'),
encryption_password=None, # Optional: Password for the file encryption. Will not use it if set to None.
file=os.getenv('FILE')
) as actual:
print(actual)

View File

@ -0,0 +1,49 @@
from actual import Actual, Transactions
import os
from dotenv import load_dotenv
from actual.queries import get_transactions
from datetime import datetime, timedelta
from moneyed import Money, USD
from moneyed.l10n import format_money
from last_month import compare_months
import last_month
from dateutil.relativedelta import relativedelta
import json
import re
import pprint
pp=pprint.PrettyPrinter(indent=4, sort_dicts=False)
load_dotenv()
THISMONTH = datetime.now()
# datestring= "2025-08-06 13:57:57"
# format = '%Y-%m-%d %H:%M:%S'
# THISMONTH = datetime.strptime(datestring, format)
MONTHMINUSONE = THISMONTH - relativedelta(months=1)
MONTHMINUSTWO = THISMONTH - relativedelta(months=2)
ONBUDGETACCOUNTS = os.getenv('ONBUDGETACCOUNTS').split()
def main(actual, month):
accounts_with_curr_month = 0
monthdaydate = datetime.strftime(month, "%Y%m")
print(monthdaydate)
first_day_month = month.replace(day=1)
for account in ONBUDGETACCOUNTS:
latest_trans = get_transactions(actual.session, account=account, start_date=first_day_month)
# for acctrans in latest_trans:
if f"date={monthdaydate}" in str(latest_trans):# and not run_compare_months:
accounts_with_curr_month += 1
if accounts_with_curr_month == 3:
print("Hold onto your butts! All 3 accounts have this month's data!")
last_month.compare_months(actual)
else:
print(f"No current month transactions for {account}")
if __name__ == "__main__":
with Actual(
base_url=os.getenv('BASEURL'),
password=os.getenv('PASSWORD'),
encryption_password=None,
file=os.getenv('FILE')
) as actual:
main(actual, THISMONTH)

7
pyproject.toml Normal file
View File

@ -0,0 +1,7 @@
[project]
name = "actual-py-proj"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []

View File

@ -7,7 +7,6 @@ charset-normalizer==3.4.4
cryptography==46.0.3
idna==3.11
numpy==2.3.4
pandas==2.3.3
proto-plus==1.26.1
protobuf==6.33.0
py-moneyed==3.0

View File

@ -1,128 +0,0 @@
"""
The output of this script should be in a format that can be sent as a text or added to a note.
In Apple Shortcuts, do the following:
* Create new shortcut
* Add Shell Script action
* In the text box put:
source ~/Documents/tmp/.actualpy/bin/activate
python ~/Documents/tmp/main.py
Select the following options:
Shell: Bash
Input: Input
Pass Input: to stdin
Run as Admin: unchecked
Then add the other actions that you need. In my case, I did "Send {Shell Script Result} to {Contact}"
"""
from actual import Actual
from actual.queries import get_budgets, get_categories, get_category, get_transactions
from dotenv import load_dotenv
from datetime import datetime
from moneyed import Money, USD
from moneyed.l10n import format_money
import json
import re
import pprint
pp=pprint.PrettyPrinter(indent=4)
load_dotenv()
TODAY = datetime.now()
MONTH_DAY = TODAY.strftime("%Y%m")
MONTHSTR = TODAY.strftime("%B")
MONTHDAYDATE = datetime.strptime(MONTH_DAY, "%Y%m")
def main():
main_dict = {}
main_list = []
with Actual(
base_url=os.getenv('BASEURL')
password=os.getenv('PASSWORD')
encryption_password=None, # Optional: Password for the file encryption. Will not use it if set to None.
file=os.getenv('FILE')
) as actual:
budget = get_budgets(actual.session)
for b in budget:
if b.month == int(MONTH_DAY):
if b.amount > 0:
cats = get_categories(actual.session)
for cat in cats:
if cat.id == b.category_id:
# if cat.name == "Kid's Activities":
formatted_amount = str(b.amount)[:-2] + "." + str(b.amount)[-2:]
budgeted_amount = Money(formatted_amount, USD)
main_dict[cat.name] = {"budgeted_amount": budgeted_amount, "amount_spent": "", "amount_left": ""}
# main_dict = {"category": cat.name, "month": MONTHSTR, "budgeted_amount": budgeted_amount, "amount_spent": "", "amount_left": ""}
category_transcations(sesh=actual.session, main_list=main_dict)
def category_transcations(sesh, main_list):
for keys, vals in main_list.items():
this_month_trans = get_transactions(sesh, category=keys, start_date=MONTHDAYDATE)
curr_sum = Money(0, USD)
for ta in this_month_trans:
tamount = str(ta.amount)[:-2] + "." + str(ta.amount)[-2:]
total_sum = Money(tamount, USD)
curr_sum += total_sum
# print(f"curr_sum: {curr_sum} PLUS total_sum: {total_sum}")
amount_list = curr_sum + vals["budgeted_amount"]
vals["budgeted_amount"] = vals['budgeted_amount']
vals["amount_spent"] = curr_sum
vals["amount_left"] = amount_list
# sorted_items = sorted(main_list.items(), key=lambda item: item[1]['amount_left'])
# sorted_nested_dict = dict(sorted_items)
# pp.pprint(sorted_nested_dict)
# pp.pprint(sorted_key_val)
# sorted_data = sorted(main_list, key=lambda x: main_list[x]['amount_left'])
# pp.pprint(sorted_data)
# sort_budgets_for_notification(sorted_data)
sort_budgets_for_notification(main_list)
def sort_budgets_for_notification(sorted_data):
negative_budget = {}
no_budget_left = {}
some_budget_left = {}
final_tuple = ()
for categories, values in sorted_data.items():
# for budget_type, amount in values.items():
intmoney = values['amount_left'].amount
if intmoney == 0:
# no_budget_left.append((categories, values) )
no_budget_left[categories] = values
elif intmoney < 0:
# negative_budget.append((categories, values))
negative_budget[categories] = values
else:
# some_budget_left.append(( categories, values) )
some_budget_left[categories] = values
final_tuple = (format_dicts(some_budget_left), format_dicts(negative_budget), format_dicts(no_budget_left))
print(f"""**This is an automated message!** Here's your spending status so far for this month.\nFirst, here are the categories where you've overspent:\n{final_tuple[1]}.\n\nHere is where you still have some budget left:\n{final_tuple[0]}.\n\nAnd finally, here is where you're exactly where you need to be. $0 left.\n {final_tuple[2]}""")
def format_dicts(budgets_sorted):
x = str(budgets_sorted)
# y = re.search(r"(\'\w*\':)|(\'\w{1,9}\s\w{1,9})|(\d*.\d{2})", x).group()
y = (x
.replace("{'", "")
.replace("}", "")
.replace("Money('", "")
.replace("'", "")
.replace(", USD", "")
.replace(")","")
.replace("(","")
.replace('"', '')
)
z = re.sub(r"((([A-Z][a-z]*( | & )){1,2}[A-Z][a-z]*:)|([A-Z][a-z]*)|529 Contrib)", '\n\\1', y)
t = re.sub(r", \n", '\n', z)
h = re.sub(r'([a-z]*_[a-z]*)',lambda x: x[1].replace('_', ' ').capitalize(), t)
p = re.sub(r'(\d{1,5}.\d{2})', '$\\1', h)
o = re.sub(r'(\d{4})', lambda x: x[1][:1]+','+x[1][1:], p)
return o
if __name__ == "__main__":
main()

View File

@ -29,7 +29,7 @@ def simple_track(actual):
if cat.name == "Income":
formatted_amount = str(b.amount)[:-2] + "." + str(b.amount)[-2:]
expected_income = Money(formatted_amount, USD)
main_dict[cat.name] = {"Expected Income": expected_income, "Actual Income": Money(0, USD), "Total Spent": Money(0, USD), "Amount Left Against Budget": Money(0, USD)}
main_dict[cat.name] = {"Expected Income": expected_income, "Actual Income": Money(0, USD), "Total Spent": Money(0, USD), "Left Against Budget": Money(0, USD), "Left Against Actual": Money(0, USD)}
category_transcations(sesh=actual.session, main_list=main_dict, origin="simple")
def main(actual):
@ -72,10 +72,14 @@ def category_transcations(sesh, main_list, origin):
amount_left = curr_sum + main_list['Income']['Expected Income']
main_list['Income']['Expected Income'] = main_list['Income']['Expected Income']
main_list['Income']['Actual Income'] = income_sum
main_list['Income']['Amount Left Against Budget'] = amount_left
main_list['Income']['Left Against Budget'] = amount_left
main_list['Income']['Total Spent'] = curr_sum
xx = curr_sum.amount
if xx.is_zero():
main_list['Income']['Left Against Actual'] = income_sum+curr_sum
else:
main_list['Income']['Left Against Actual'] = income_sum+curr_sum
# print(f"curr_sum: {curr_sum} PLUS total_sum: {total_sum}")
else:
for keys, vals in main_list.items():
this_month_trans = get_transactions(sesh, category=keys, start_date=MONTHDAYDATE)
@ -84,20 +88,11 @@ def category_transcations(sesh, main_list, origin):
tamount = str(ta.amount)[:-2] + "." + str(ta.amount)[-2:]
total_sum = Money(tamount, USD)
curr_sum += total_sum
# print(f"curr_sum: {curr_sum} PLUS total_sum: {total_sum}")
amount_list = curr_sum + vals["budgeted_amount"]
vals["budgeted_amount"] = vals['budgeted_amount']
vals["amount_spent"] = curr_sum
vals["amount_left"] = amount_list
# sorted_items = sorted(main_list.items(), key=lambda item: item[1]['amount_left'])
# sorted_nested_dict = dict(sorted_items)
# pp.pprint(sorted_nested_dict)
# pp.pprint(sorted_key_val)
# sorted_data = sorted(main_list, key=lambda x: main_list[x]['amount_left'])
# pp.pprint(sorted_data)
# sort_budgets_for_notification(sorted_data)
if origin == "simple":
x = format_dicts(main_list)
simple_notifications(x)
@ -150,13 +145,6 @@ def format_dicts(budgets_sorted):
return o
# categies = str([x['category'] for x in negative_budget ])[1:-1].replace('"','').replace("'",'')
# categies2 = str([x['category'] for x in some_budget_left ])[1:-1].replace('"','').replace("'",'')
# categies3 = str([x['category'] for x in no_budget_left ])[1:-1].replace('"','').replace("'",'')
# print(f"""Here's your spending status so far for this month. First, here are the categories where you've overspent:\n- {str(categies)}.\n\nHere is where you still have some budget left:\n- {str(categies2)}.\n\nAnd finally, here is where you're exactly where you need to be. $0 left.\n- {str(categies3)}""")
if __name__ == "__main__":
with Actual(
base_url=os.getenv('BASEURL'),

8
uv.lock generated Normal file
View File

@ -0,0 +1,8 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "actual-py-proj"
version = "0.1.0"
source = { virtual = "." }