From b1d6aab9e306511ca5797b4a43f2baa87b554e6d Mon Sep 17 00:00:00 2001 From: norm Date: Wed, 5 Nov 2025 14:43:11 -0500 Subject: [PATCH] Initial commit. Lots of messy functions but eventually each file will do different things for apple shortcuts to then send me a text. --- actual_bank_sync.py | 30 +++++++ last_month.py | 195 ++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 27 ++++++ scrubbed_main.py | 128 +++++++++++++++++++++++++++++ this_month.py | 168 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 548 insertions(+) create mode 100644 actual_bank_sync.py create mode 100644 last_month.py create mode 100644 requirements.txt create mode 100644 scrubbed_main.py create mode 100644 this_month.py diff --git a/actual_bank_sync.py b/actual_bank_sync.py new file mode 100644 index 0000000..a2d2d98 --- /dev/null +++ b/actual_bank_sync.py @@ -0,0 +1,30 @@ +from actual import Actual +import os +from dotenv import load_dotenv + +load_dotenv() +def main(): + synced_accounts = os.getenv('SYNCED_ACCOUNTS') + 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'), + cert=False + ) as actual: + for account in synced_accounts: + try: + sync_test = actual.run_bank_sync(account=account, run_rules=True) + except Exception as e: + print("**************") + print(f"An exception occurred! \n {e}") + print("**************") + else: + for transaction in sync_test: + print(f"Added of modified {transaction}") + finally: + actual.commit() + + +if __name__ == "__main__": + main() diff --git a/last_month.py b/last_month.py new file mode 100644 index 0000000..37f50e7 --- /dev/null +++ b/last_month.py @@ -0,0 +1,195 @@ +from actual import Actual +import os +from dotenv import load_dotenv +from actual.queries import get_budgets, get_categories, get_category, get_transactions +from datetime import datetime, timedelta +from moneyed import Money, USD +from moneyed.l10n import format_money +from dateutil.relativedelta import relativedelta +import json +import re +import pprint + +pp=pprint.PrettyPrinter(indent=4, sort_dicts=False) +load_dotenv() +THISMONTH = datetime.now() +MONTHMINUSONE = THISMONTH - relativedelta(months=1) +MONTHMINUSTWO = THISMONTH - relativedelta(months=2) +ONBUDGETACCOUNTS = os.getenv('ONBUDGETACCOUNTS') + +def compare_months(actual): + budget_one = main(actual, MONTHMINUSONE) + budget_two = main(actual, MONTHMINUSTWO) + 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) + + +def find_percent_diff(lastmonth, twomonths): + diffs = [] + for k, v in lastmonth.items(): + for y, x in twomonths.items(): + if k == y: + absolute = v['amount_spent'] - x['amount_spent'] + average = (v['amount_spent'] + x['amount_spent'])/2 + diff = round(absolute/average, 2) + diffs.append(( k, diff) ) + + sort = sorted(diffs, reverse=True, key=lambda item: item[1]) + return sort + + +def simple_track(actual): + main_dict = {} + main_list = [] + budget = get_budgets(actual.session) + for b in budget: + if b.month == int(MONTH_DAY_ONE) and b.amount > 0: + cats = get_categories(actual.session) + for cat in cats: + if cat.id == b.category_id: + 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)} + category_transcations(sesh=actual.session, main_list=main_dict, origin="simple") + +def main(actual, month): + intmonth = month.strftime("%Y%m") + main_dict = {} + main_list = [] + budget = get_budgets(actual.session) + for b in budget: + if b.month == int(intmonth) and 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": ""} + returns = category_transcations(sesh=actual.session, main_list=main_dict, origin="all_cats", month=month) + return returns + + +def category_transcations(sesh, main_list, origin, month): + first_day_month = month.replace(day=1) + first_day_next_month = (month.replace(day=1) + timedelta(days=32)).replace(day=1) + if origin == "simple": + this_monhth_income = get_transactions(sesh, category='Income', start_date=MONTHMINUSONE.replace(day=1), end_date=THISMONTH.replace(day=1)) + income_sum = Money(0, USD) + for tit in this_monhth_income: + print(tit.amount, tit.payee.name, tit.date) + income = str(tit.amount)[:-2] + "." + str(tit.amount)[-2:] + total_income = Money(income, USD) + income_sum += total_income + + this_month_trans = get_transactions(sesh, account='onbudget', start_date=first_day_month, end_date=first_day_next_month) + curr_sum = Money(0, USD) + count = 0 + for ta in this_month_trans: + if ta.category_id != '3c1699a5-522a-435e-86dc-93d900a14f0e' and ta.account.id in ONBUDGETACCOUNTS: + tamount = str(ta.amount)[:-2] + "." + str(ta.amount)[-2:] + total_sum = Money(tamount, USD) + curr_sum += total_sum + count += 1 + + 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']['Total Spent'] = curr_sum + + else: + for keys, vals in main_list.items(): + this_month_trans = get_transactions(sesh, category=keys, start_date=first_day_month, end_date=first_day_next_month) + 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) + + if origin == "simple": + x = format_dicts(main_list) + simple_notifications(x) + else: + return main_list + # all_budgets_for_notifications(main_list) + +def simple_notifications(x): + print(f"""Here's where you stand last month!\n {x}""") + +def all_budgets_for_notifications(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)[10:] + # 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,5})', lambda x: x[1][:-3]+','+x[1][-3:], p) + 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'), + 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: + # main(actual) + compare_months(actual) + # simple_track(actual) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..da4d58e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,27 @@ +actualpy==0.16.0 +annotated-types==0.7.0 +babel==2.17.0 +certifi==2025.10.5 +cffi==2.0.0 +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 +pycparser==2.23 +pydantic==2.12.3 +pydantic-core==2.41.4 +python-dateutil==2.9.0.post0 +python-dotenv==1.2.1 +pytz==2025.2 +requests==2.32.5 +six==1.17.0 +sqlalchemy==2.0.44 +sqlmodel==0.0.27 +typing-extensions==4.15.0 +typing-inspection==0.4.2 +tzdata==2025.2 +urllib3==2.5.0 diff --git a/scrubbed_main.py b/scrubbed_main.py new file mode 100644 index 0000000..fe603b0 --- /dev/null +++ b/scrubbed_main.py @@ -0,0 +1,128 @@ +""" +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() diff --git a/this_month.py b/this_month.py new file mode 100644 index 0000000..f3cc7de --- /dev/null +++ b/this_month.py @@ -0,0 +1,168 @@ +from actual import Actual +import os +from dotenv import load_dotenv +from actual.queries import get_budgets, get_categories, get_category, get_transactions +from datetime import datetime +from moneyed import Money, USD +from moneyed.l10n import format_money +import json +import re +import pprint + +load_dotenv() +pp=pprint.PrettyPrinter(indent=4) +TODAY = datetime.now() +MONTH_DAY = TODAY.strftime("%Y%m") +MONTHSTR = TODAY.strftime("%B") +MONTHDAYDATE = datetime.strptime(MONTH_DAY, "%Y%m") +ONBUDGETACCOUNTS = os.getenv('ONBUDGETACCOUNTS') + +def simple_track(actual): + main_dict = {} + main_list = [] + budget = get_budgets(actual.session) + for b in budget: + if b.month == int(MONTH_DAY) and b.amount > 0: + cats = get_categories(actual.session) + for cat in cats: + if cat.id == b.category_id: + 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)} + category_transcations(sesh=actual.session, main_list=main_dict, origin="simple") + +def main(actual): + main_dict = {} + main_list = [] + budget = get_budgets(actual.session) + for b in budget: + if b.month == int(MONTH_DAY) and 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": ""} + print(main_dict) + category_transcations(sesh=actual.session, main_list=main_dict, origin="all_cats") + + +def category_transcations(sesh, main_list, origin): + if origin == "simple": + this_monhth_income = get_transactions(sesh, category='Income', start_date=MONTHDAYDATE) + income_sum = Money(0, USD) + for ti in this_monhth_income: + income = str(ti.amount)[:-2] + "." + str(ti.amount)[-2:] + total_income = Money(income, USD) + income_sum += total_income + + this_month_trans = get_transactions(sesh, account='onbudget', start_date=MONTHDAYDATE) + curr_sum = Money(0, USD) + count = 0 + for ta in this_month_trans: + if ta.category_id != '3c1699a5-522a-435e-86dc-93d900a14f0e' and ta.account.id in ONBUDGETACCOUNTS: + tamount = str(ta.amount)[:-2] + "." + str(ta.amount)[-2:] + total_sum = Money(tamount, USD) + curr_sum += total_sum + count += 1 + + 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']['Total Spent'] = 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) + 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) + + if origin == "simple": + x = format_dicts(main_list) + simple_notifications(x) + else: + all_budgets_for_notifications(main_list) + +def simple_notifications(x): + print(f"""Here's where you stand this month!\n {x}""") + +def all_budgets_for_notifications(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)[10:] + # 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,5})', lambda x: x[1][:-3]+','+x[1][-3:], p) + 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'), + 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: + # main(actual) + simple_track(actual)