mega-commit
This commit is contained in:
12
get_some_transactions_v1.py
Normal file
12
get_some_transactions_v1.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
# twilio.rest has a Client too, so let's avoid a namespace collision
|
||||
from plaid import Client as PlaidClient
|
||||
|
||||
plaid_client = PlaidClient(client_id=os.getenv('PLAID_CLIENT_ID'), secret=os.getenv('PLAID_SECRET'),
|
||||
public_key=os.getenv('PLAID_PUBLIC_KEY'), environment=os.getenv('PLAID_ENV'))
|
||||
|
||||
|
||||
def get_some_transactions(access_token: str, start_date: str, end_date: str) -> List[dict]:
|
||||
return plaid_client.Transactions.get(access_token, start_date, end_date)
|
||||
36
get_some_transactions_v2.py
Normal file
36
get_some_transactions_v2.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import math
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
# twilio.rest has a Client too, so let's avoid a namespace collision
|
||||
from plaid import Client as PlaidClient
|
||||
|
||||
plaid_client = PlaidClient(client_id=os.getenv('PLAID_CLIENT_ID'), secret=os.getenv('PLAID_SECRET'),
|
||||
public_key=os.getenv('PLAID_PUBLIC_KEY'), environment=os.getenv('PLAID_ENV'))
|
||||
|
||||
|
||||
# https://plaid.com/docs/api/#transactions
|
||||
MAX_TRANSACTIONS_PER_PAGE = 500
|
||||
OMIT_CATEGORIES = ["Transfer", "Credit Card", "Deposit"]
|
||||
|
||||
|
||||
def get_some_transactions(access_token: str, start_date: str, end_date: str) -> List[dict]:
|
||||
account_ids = [account['account_id'] for account in plaid_client.Accounts.get(access_token)['accounts']
|
||||
if account['subtype'] not in ['cd', 'savings']]
|
||||
|
||||
num_available_transactions = plaid_client.Transactions.get(access_token, start_date, end_date,
|
||||
account_ids=account_ids)['total_transactions']
|
||||
num_pages = math.ceil(num_available_transactions / MAX_TRANSACTIONS_PER_PAGE)
|
||||
transactions = []
|
||||
|
||||
for page_num in range(num_pages):
|
||||
transactions += [transaction
|
||||
for transaction in plaid_client.Transactions.get(access_token, start_date, end_date,
|
||||
account_ids=account_ids,
|
||||
offset=page_num * MAX_TRANSACTIONS_PER_PAGE,
|
||||
count=MAX_TRANSACTIONS_PER_PAGE)['transactions']
|
||||
if transaction['category'] is None
|
||||
or not any(category in OMIT_CATEGORIES
|
||||
for category in transaction['category'])]
|
||||
|
||||
return transactions
|
||||
17
get_yesterdays.py
Normal file
17
get_yesterdays.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import datetime
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from get_some_transactions_v2 import get_some_transactions
|
||||
|
||||
|
||||
def get_yesterdays_transactions() -> List[dict]:
|
||||
yesterday = ('2017-05-16' if os.getenv('PLAID_ENV') == 'sandbox'
|
||||
else (datetime.date.today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d'))
|
||||
|
||||
transactions = []
|
||||
|
||||
for access_id in [os.getenv('CHASE_ACCESS_TOKEN'), os.getenv('BOFA_ACCESS_TOKEN')]:
|
||||
transactions += get_some_transactions(access_id, yesterday, yesterday)
|
||||
|
||||
return transactions
|
||||
5
run.py
Normal file
5
run.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from get_yesterdays import get_yesterdays_transactions
|
||||
from send_summary import send_summary
|
||||
|
||||
|
||||
send_summary(get_yesterdays_transactions())
|
||||
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
14
send_summary.py
Normal file
14
send_summary.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from twilio.rest import Client as TwilioClient
|
||||
|
||||
twilio_client = TwilioClient(os.getenv('TWILIO_SID'), os.getenv('TWILIO_TOKEN'))
|
||||
|
||||
|
||||
def send_summary(transactions: List[dict]) -> None:
|
||||
total_spent = sum(transaction['amount'] for transaction in transactions)
|
||||
|
||||
message = f'You spent ${total_spent} yesterday. 💸'
|
||||
|
||||
twilio_client.api.account.messages.create(to=os.getenv('MY_CELL'), from_=os.getenv('MY_TWILIO_NUM'), body=message)
|
||||
232
spending_summary.md
Normal file
232
spending_summary.md
Normal file
@@ -0,0 +1,232 @@
|
||||
Your bank lets you set up SMS alerts for various triggers; it might even give you the option of receiving periodic spending summaries (mine doesn't!).
|
||||
But what about a daily SMS summary of your spending across *all* your accounts? This is harder to come by, so let's roll our own:
|
||||
|
||||
## Pre-work
|
||||
|
||||
1. [Nab a sandbox account](https://dashboard.plaid.com/signup) from Plaid, and put those credentials into `PLAID_CLIENT_ID`, `PLAID_SECRET`,
|
||||
and `PLAID_PUBLIC_KEY` environment variables. While you're at it,
|
||||
[ask for access](https://dashboard.plaid.com/overview/request-development) the development API (it'll take a few days).
|
||||
For now, though, create an env var `PLAID_ENV` and set it to 'sandbox'.
|
||||
|
||||
```bash
|
||||
export PLAID_CLIENT_ID='somechars1234'
|
||||
export PLAID_PUBLIC_KEY='somemorechars1234'
|
||||
export PLAID_SECRET='somesecretchars1234'
|
||||
export PLAID_ENV='sandbox'
|
||||
```
|
||||
|
||||
1. Clone the [quickstart for Plaid](https://github.com/plaid/quickstart) and reference those environment variables in the
|
||||
Python quickstart's `server.py`.
|
||||
|
||||
```python
|
||||
PLAID_CLIENT_ID = os.getenv('PLAID_CLIENT_ID')
|
||||
PLAID_SECRET = os.getenv('PLAID_SECRET')
|
||||
PLAID_PUBLIC_KEY = os.getenv('PLAID_PUBLIC_KEY')
|
||||
PLAID_ENV = os.getenv('PLAID_ENV')
|
||||
```
|
||||
|
||||
1. Run `server.py` (Python 2 only 😦) and log into Chase with the test credentials ("user_good" and
|
||||
"pass_good" as of 5/24/2017). It prints the access token to your terminal: Grab that and put it into a
|
||||
`CHASE_ACCESS_TOKEN` environment variable. Repeat this for Bank of America and put that access token into `BOFA_ACCESS_TOKEN`.
|
||||
|
||||
1. [Grab your Twilio credentials](https://www.twilio.com/console/account/settings) and
|
||||
[Twilio incoming phone number](https://www.twilio.com/console/phone-numbers/incoming) make sure those are available as
|
||||
environment variables too (see below).
|
||||
|
||||
1. Put your cell phone number in an environment variable as `MY_CELL`.
|
||||
|
||||
```bash
|
||||
export CHASE_ACCESS_TOKEN='access-sandbox-someprettysecretchars1234'
|
||||
export BOFA_ACCESS_TOKEN='access-sandbox-somemoreprettysecretchars1234'
|
||||
export TWILIO_SID='somechars1234'
|
||||
export TWILIO_TOKEN='somesecretchars1234'
|
||||
export MY_TWILIO_NUM='+11111111111'
|
||||
export MY_CELL='+12222222222'
|
||||
```
|
||||
|
||||
1. Install just a couple of dependencies:
|
||||
|
||||
```bash
|
||||
$ pip install python-plaid twilio
|
||||
```
|
||||
|
||||
## Get Some Transactions
|
||||
|
||||
Make yourself a Plaid client instance and grab some transactions from Chase:
|
||||
|
||||
[get_some_transactions_v1.py](https://github.com/zevaverbach/spending_summary/blob/master/get_some_transactions_v1.py)
|
||||
|
||||
```python
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
# twilio.rest has a Client too, so let's avoid a namespace collision
|
||||
from plaid import Client as PlaidClient
|
||||
|
||||
plaid_client = PlaidClient(client_id=os.getenv('PLAID_CLIENT_ID'), secret=os.getenv('PLAID_SECRET'),
|
||||
public_key=os.getenv('PLAID_PUBLIC_KEY'), environment=os.getenv('PLAID_ENV'))
|
||||
|
||||
|
||||
def get_some_transactions(access_token: str, start_date: str, end_date: str) -> List[dict]:
|
||||
return plaid_client.Transactions.get(access_token, start_date, end_date)
|
||||
|
||||
```
|
||||
|
||||
Inspecting the output of `get_some_transactions`, we see that there are multiple accounts, 337 transactions among them, but only
|
||||
100 transactions returned from this API call. Two of these accounts are for savings, so presumably they're only
|
||||
going to have transfers rather than purchases.
|
||||
|
||||
```python
|
||||
>>> from get_some_transactions_v1 import get_some_transactions
|
||||
>>> import os
|
||||
>>> some_transactions = get_some_transactions(os.getenv('CHASE_ACCESS_TOKEN'), '1972-01-01', '2017-05-26'))
|
||||
>>> some_transactions['total_transactions']
|
||||
337
|
||||
>>> from pprint import pprint
|
||||
>>> pprint(some_transactions['accounts'])
|
||||
[{'account_id': 'qwp96Z11b5IBKVMl8XvLSkJXjgj6ZxIXX3o79',
|
||||
'name': 'Plaid Checking',
|
||||
'subtype': 'checking'
|
||||
...},
|
||||
{'account_id': 'Kk9ZL7NN4wSX3lR9evV8f9P4GVGk3BF33QnAM',
|
||||
'name': 'Plaid Saving',
|
||||
'subtype': 'savings'
|
||||
...},
|
||||
{'account_id': 'rEy96MWWgXukrnBW4yVphv7yl3lznosBBzo6n',
|
||||
'name': 'Plaid CD',
|
||||
'subtype': 'cd'
|
||||
...}
|
||||
{'account_id': '9rNKomMMdWTvVL4X9RP6UKb4qEqng1uJJ6nQw',
|
||||
'name': 'Plaid Credit Card',
|
||||
'subtype': 'credit'
|
||||
...}]
|
||||
>>> len(some_transactions['transactions'])
|
||||
100
|
||||
```
|
||||
## Get **The Right** Transactions
|
||||
|
||||
Looking at the transactions themselves, we see that there is a `category` field which sometimes has a list of values,
|
||||
sometimes `None`. Among the categories there are "Transfer", "Credit Card", and "Deposit": These aren't going to be
|
||||
useful in gleaning spending activity, so we'll refactor our `get_some_transactions` function to 1) skip transactions
|
||||
with those categories and 2) skip accounts with a subtype of "savings" or "cd". Let's also 3) make sure to get all
|
||||
available transactions by using pagination and 4) just return transactions.
|
||||
|
||||
```python
|
||||
>>> some_transactions['transactions'].keys()
|
||||
dict_keys(['account_id', 'account_owner', 'amount', 'category', 'category_id', 'date', 'location', 'name', 'payment_meta', 'pending', 'pending_transaction_id', 'transaction_id', 'transaction_type'])
|
||||
>>> {category for transaction in some_transactions['transactions'] if some_transactions['category'] for category in trans['category']}
|
||||
{'Airlines and Aviation Services',
|
||||
'Coffee Shop',
|
||||
'Credit Card',
|
||||
'Deposit',
|
||||
'Fast Food',
|
||||
'Food and Drink',
|
||||
'Payment',
|
||||
'Restaurants',
|
||||
'Transfer',
|
||||
'Travel'}
|
||||
```
|
||||
|
||||
[get_some_transactions_v2.py](https://github.com/zevaverbach/spending_summary/blob/master/get_some_transactions_v2.py)
|
||||
|
||||
```python
|
||||
import math
|
||||
...
|
||||
# https://plaid.com/docs/api/#transactions
|
||||
MAX_TRANSACTIONS_PER_PAGE = 500
|
||||
OMIT_CATEGORIES = ["Transfer", "Credit Card", "Deposit"]
|
||||
|
||||
|
||||
def get_some_transactions(access_token: str, start_date: str, end_date: str) -> List[dict]:
|
||||
account_ids = [account['account_id'] for account in plaid_client.Accounts.get(access_token)['accounts']
|
||||
if account['subtype'] not in ['cd', 'savings']]
|
||||
|
||||
num_available_transactions = plaid_client.Transactions.get(access_token, start_date, end_date,
|
||||
account_ids=account_ids)['total_transactions']
|
||||
num_pages = math.ceil(num_available_transactions / MAX_TRANSACTIONS_PER_PAGE)
|
||||
transactions = []
|
||||
|
||||
for page_num in range(num_pages):
|
||||
transactions += [transaction
|
||||
for transaction in plaid_client.Transactions.get(access_token, start_date, end_date,
|
||||
account_ids=account_ids,
|
||||
offset=page_num * MAX_TRANSACTIONS_PER_PAGE,
|
||||
count=MAX_TRANSACTIONS_PER_PAGE)['transactions']
|
||||
if transaction['category'] is None
|
||||
or not any(category in OMIT_CATEGORIES
|
||||
for category in transaction['category'])]
|
||||
|
||||
return transactions
|
||||
```
|
||||
|
||||
Now there are just 265 transactions. Are any of them negative?
|
||||
|
||||
```python
|
||||
>>> pprint([transaction for transaction in some_transactions if transaction['amount'] < 0])
|
||||
[{'amount': -500,
|
||||
'category': ['Travel', 'Airlines and Aviation Services'],
|
||||
'name': 'United Airlines',
|
||||
'transaction_type': 'special',
|
||||
...},
|
||||
...]
|
||||
```
|
||||
|
||||
Okay, that seems legit -- airfare refund, I guess. Let's keep negative items in.
|
||||
|
||||
## Pulling it All Together
|
||||
|
||||
Now let's get all the transactions from yesterday, making sure to pull them from both accounts.
|
||||
|
||||
[get_yesterdays.py](https://github.com/zevaverbach/spending_summary/blob/master/get_yesterdays.py)
|
||||
|
||||
```python
|
||||
...
|
||||
def get_yesterdays_transactions() -> List[dict]:
|
||||
yesterday = ('2017-05-16' if os.getenv('PLAID_ENV') == 'sandbox'
|
||||
else (datetime.date.today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d'))
|
||||
|
||||
transactions = []
|
||||
|
||||
for access_id in [os.getenv('CHASE_ACCESS_TOKEN'), os.getenv('BOFA_ACCESS_TOKEN')]:
|
||||
transactions += get_transactions_from_multiple_accounts(access_id, yesterday, yesterday)
|
||||
|
||||
return transactions
|
||||
```
|
||||
|
||||
As of 5/24/2017, the most recent transactions available in these sandbox accounts are from 5/16/17: Hence, the hardcoded
|
||||
`yesterday` value above.
|
||||
|
||||
Let's send an SMS to ourselves with the total spent yesterday!
|
||||
|
||||
[send_summary.py](https://github.com/zevaverbach/spending_summary/blob/master/send_summary.py)
|
||||
|
||||
```python
|
||||
...
|
||||
from twilio.rest import Client as TwilioClient
|
||||
|
||||
twilio_client = TwilioClient(os.getenv('TWILIO_SID'), os.getenv('TWILIO_TOKEN'))
|
||||
|
||||
|
||||
def send_summary(transactions: List[dict]) -> None:
|
||||
total_spent = sum(transaction['amount'] for transaction in transactions)
|
||||
|
||||
message = f'You spent ${total_spent} yesterday. 💸'
|
||||
|
||||
twilio_client.api.account.messages.create(to=os.getenv('MY_CELL'), from_=os.getenv('MY_TWILIO_NUM'), body=message)
|
||||
|
||||
|
||||
```
|
||||
Voila!
|
||||
|
||||

|
||||
|
||||
## Representing Money as a Float?
|
||||
|
||||
You're gonna have a bad time, 'mkay? If you build anything significant (budget tracker bot?) with the Plaid API,
|
||||
be sure to convert those transaction amounts to Decimals:
|
||||
|
||||
```python
|
||||
import decimal
|
||||
def convert_transaction_amount(transaction_amount):
|
||||
return decimal.Decimal(transaction_amount)
|
||||
```
|
||||
60
spending_summary.py
Normal file
60
spending_summary.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import datetime
|
||||
import math
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from twilio.rest import Client as TwilioClient
|
||||
|
||||
from plaid import Client as PlaidClient
|
||||
|
||||
# these are access IDs from Plaid for a specific bank/credit account
|
||||
ACCOUNT_ACCESS_IDS: List[str] = [os.getenv('PLAID_ACCESS_ID'), os.getenv('OTHER_PLAID_ACCESS_ID')]
|
||||
MAX_TRANSACTIONS_PER_PAGE = 500
|
||||
|
||||
plaid_client = PlaidClient(client_id=os.getenv('PLAID_CLIENT_ID'), secret=os.getenv('PLAID_SECRET'),
|
||||
public_key=os.getenv('PLAID_PUBLIC_KEY'), environment=os.getenv('PLAID_ENV'))
|
||||
|
||||
twilio_client = TwilioClient(os.getenv('TWILIO_SID'), os.getenv('TWILIO_TOKEN'))
|
||||
|
||||
|
||||
def get_num_available_transactions(access_id, start: str, end: str) -> int:
|
||||
return plaid_client.Transactions.get(access_id, start, end)['total_transactions']
|
||||
|
||||
|
||||
def get_transactions_from_account(access_id: str, start: str, end: str) -> List[dict]:
|
||||
transactions = []
|
||||
num_available_transactions = get_num_available_transactions(access_id, start, end)
|
||||
num_pages = math.ceil(num_available_transactions / MAX_TRANSACTIONS_PER_PAGE)
|
||||
|
||||
for page_num in range(num_pages):
|
||||
transactions += plaid_client.Transactions.get(access_id, start, end,
|
||||
offset=page_num * MAX_TRANSACTIONS_PER_PAGE,
|
||||
count=MAX_TRANSACTIONS_PER_PAGE)['transactions']
|
||||
|
||||
return transactions
|
||||
|
||||
|
||||
def get_transactions_from_multiple_accounts(access_ids: List[str], start: str, end: str) -> List[dict]:
|
||||
transactions = []
|
||||
for access_id in access_ids:
|
||||
transactions += get_transactions_from_account(access_id, start, end)
|
||||
return transactions
|
||||
|
||||
|
||||
def get_yesterdays_transactions() -> List[dict]:
|
||||
yesterday: str = (datetime.date.today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d')
|
||||
return get_transactions_from_multiple_accounts(ACCOUNT_ACCESS_IDS, yesterday, yesterday)
|
||||
|
||||
|
||||
def send_summary(transactions: List[dict]) -> None:
|
||||
total_spent = sum(transaction['amount']
|
||||
for transaction in transactions
|
||||
if transaction['amount'] > 0)
|
||||
|
||||
message = f'You spent ${total_spent} yesterday. 💸'
|
||||
|
||||
twilio_client.api.account.messages.create(to=os.getenv('MY_CELL'), from_=os.getenv('MY_TWILIO_NUM'), body=message)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
send_summary(get_yesterdays_transactions())
|
||||
@@ -1,5 +0,0 @@
|
||||
Budgeting. You do it, then you stop doing it. You wake up in a cold sweat and you start tracking your spending again. It sucks: You stop.
|
||||
|
||||
There are tools! Horrible, awful tools that are glorified spreadsheets which think all your Amazon purchases go under `books`. Much data entered, many hairs pulled out.
|
||||
|
||||
Surely this is fodder for a bot.
|
||||
@@ -1,71 +0,0 @@
|
||||
import datetime
|
||||
import os
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import math
|
||||
from twilio.rest import Client as TwilioClient
|
||||
|
||||
from plaid import Client as PlaidClient
|
||||
|
||||
CHECK_FOR_NEW_TRANSACTIONS_EVERY_X_MINUTES = 5
|
||||
ALERT_FOR_TRANSACTIONS_GTE = 500
|
||||
MY_CELL = os.getenv('MY_CELL')
|
||||
MY_TWILIO_NUM = os.getenv('MY_TWILIO_NUM')
|
||||
PLAID_ENV = os.getenv('PLAID_ENV') or 'sandbox'
|
||||
|
||||
# this is an access ID from Plaid for a specific user and bank/credit account
|
||||
PLAID_ACCESS_ID = os.getenv('PLAID_ACCESS_ID')
|
||||
|
||||
plaid_client = PlaidClient(client_id=os.getenv('PLAID_CLIENT_ID'), secret=os.getenv('PLAID_SECRET'),
|
||||
public_key=os.getenv('PLAID_PUBLIC_KEY'), environment=PLAID_ENV)
|
||||
|
||||
twilio_client = TwilioClient(os.getenv('TWILIO_SID'), os.getenv('TWILIO_TOKEN'))
|
||||
|
||||
transaction_ids = set()
|
||||
|
||||
|
||||
def get_all_transactions() -> List[dict]:
|
||||
today = datetime.date.today().strftime('%Y-%m-%d')
|
||||
total_transactions: int = plaid_client.Transactions.get(PLAID_ACCESS_ID, '1972-01-01', today)['total_transactions']
|
||||
all_transactions = []
|
||||
for page in range(math.ceil(total_transactions / 500)):
|
||||
all_transactions += plaid_client.Transactions.get(PLAID_ACCESS_ID, '1972-01-01', today,
|
||||
offset=page * 500, count=500)['transactions']
|
||||
return all_transactions
|
||||
|
||||
|
||||
def get_latest_transactions() -> List[dict]:
|
||||
"""get 100 most recent transactions"""
|
||||
today = datetime.date.today().strftime('%Y-%m-%d')
|
||||
return [transaction
|
||||
for transaction in
|
||||
plaid_client.Transactions.get(PLAID_ACCESS_ID, '1972-01-01', today)['transactions']
|
||||
if transaction['transaction_id'] not in transaction_ids]
|
||||
|
||||
|
||||
def alert(transaction: dict) -> None:
|
||||
message = (f'hey, a transaction hit your account that exceeds ${ALERT_FOR_TRANSACTIONS_GTE}: '
|
||||
f'{transaction["date"]} {transaction["name"]} ${transaction["amount"]}')
|
||||
|
||||
twilio_client.api.account.messages.create(to=MY_CELL, from_=MY_TWILIO_NUM, body=message)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(transaction_ids) == 0:
|
||||
transaction_ids.update(transaction['transaction_id'] for transaction in get_all_transactions())
|
||||
|
||||
while True:
|
||||
|
||||
for transaction in get_latest_transactions():
|
||||
|
||||
transaction_ids.add(transaction['transaction_id'])
|
||||
|
||||
if transaction['amount'] >= ALERT_FOR_TRANSACTIONS_GTE:
|
||||
alert(transaction)
|
||||
|
||||
time.sleep(CHECK_FOR_NEW_TRANSACTIONS_EVERY_X_MINUTES * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user