Skip to content

Commit

Permalink
Merge branch 'main' into chore/update-accounts-endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
burkematthew committed Mar 28, 2022
2 parents 369e489 + 3d875fe commit 6747c58
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 302 deletions.
26 changes: 10 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ an MFA prompt, you'll be prompted on the command line for your code, which by de
goes to SMS unless you specify `--mfa-method=email`. This will also persist a browser
session in $HOME/.mintapi/session to avoid an MFA in the future, unless you specify `--session-path=None`.

If you wish to simplify the number of arguments passed in the command line, you can use a configuration file by specifying `--config-file`. For arguments such as `--extended-transactions`, you can add a line in your config file that says `extended-transactions`. For other arguments that have input, such as `--start-date`, you would add a line such as `start-date=10/01/21`. There are two exceptions to what you can add to the config file: email and password. Since these arguments do not include `--`, you cannot add them to the config file.
If you wish to simplify the number of arguments passed in the command line, you can use a configuration file by specifying `--config-file`. For arguments such as `--transactions`, you can add a line in your config file that says `transactions`. For other arguments that have input, such as `--start-date`, you would add a line such as `start-date=10/01/21`. There are two exceptions to what you can add to the config file: email and password. Since these arguments do not include `--`, you cannot add them to the config file.

### Linux Distributions (including Raspberry Pi OS)

Expand Down Expand Up @@ -130,15 +130,12 @@ make calls to retrieve account/budget information. We recommend using the
mint.get_budgets()

# Get transactions
mint.get_transactions() # as pandas dataframe
mint.get_transactions_csv(include_investment=False) # as raw csv data
mint.get_transactions_json(include_investment=False)
mint.get_transaction_data() # as pandas dataframe

# Get transactions for a specific account
accounts = mint.get_accounts(True)
for account in accounts:
mint.get_transactions_csv(id=account["id"])
mint.get_transactions_json(id=account["id"])
mint.get_transaction_data(id=account["id"])

# Get net worth
mint.get_net_worth()
Expand Down Expand Up @@ -175,16 +172,16 @@ make calls to retrieve account/budget information. We recommend using the
)
# now you can do all the normal api calls
# ex:
mint.get_transactions()
mint.get_transaction_data()
```

---
Run it as a sub-process from your favorite language; `pip install mintapi` creates a binary in your $PATH. From the command-line, the output is JSON:

```shell
usage: mintapi [-h] [--session-path [SESSION_PATH]] [--accounts] [--investment]
[--budgets | --budget_hist] [--net-worth] [--extended-accounts] [--transactions]
[--extended-transactions] [--credit-score] [--credit-report]
[--budgets | --budget_hist] [--net-worth] [--extended-accounts]
[--transactions] [--credit-score] [--credit-report]
[--exclude-inquiries] [--exclude-accounts] [--exclude-utilization]
[--start-date [START_DATE]] [--end-date [END_DATE]]
[--include-investment] [--show-pending]
Expand Down Expand Up @@ -217,19 +214,16 @@ Run it as a sub-process from your favorite language; `pip install mintapi` creat
--net-worth Retrieve net worth information
--extended-accounts Retrieve extended account information (slower, implies --accounts)
--transactions, -t Retrieve transactions
--extended-transactions
Retrieve transactions with extra information and
arguments
--start-date [START_DATE]
Earliest date for which to retrieve transactions.
Used with --transactions or --extended-transactions. Format: mm/dd/yy
Used with --transactions. Format: mm/dd/yy
--end-date [END_DATE]
Latest date for which to retrieve transactions.
Used with --transactions or --extended-transactions. Format: mm/dd/yy
Used with --transactions. Format: mm/dd/yy
--investments Retrieve data related to your investments, whether they be retirement or personal stock purchases
--include-investment Used with --extended-transactions
--include-investment Used with --transactions
--show-pending Exclude pending transactions from being retrieved.
Used with --extended-transactions
Used with --transactions
--filename FILENAME, -f FILENAME
write results to file. can be {csv,json} format.
default is to write to stdout.
Expand Down
237 changes: 61 additions & 176 deletions mintapi/api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import date, datetime, timedelta
from dateutil.relativedelta import relativedelta
import io
import json
import logging
Expand Down Expand Up @@ -295,91 +296,15 @@ def __call_accounts_endpoint(self):
headers=self._get_api_key_header(),
).json()

def __call_investments_endpoint(self):
return self.get(
"{}/pfm/v1/investments".format(MINT_ROOT_URL),
headers=self._get_api_key_header(),
).json()

def get_categories(self):
return self.get(
"{}/pfm/v1/categories".format(MINT_ROOT_URL),
headers=self._get_api_key_header(),
).json()["Category"]

def __call_accounts_endpoint(self):
return self.get(
"{}/pfm/v1/accounts".format(MINT_ROOT_URL),
headers=self._get_api_key_header(),
).json()

def get_transactions_json(
def get_transaction_data(
self,
include_investment=False,
start_date=None,
end_date=None,
remove_pending=True,
id=0,
):
"""Returns the raw JSON transaction data as downloaded from Mint. The JSON
transaction data includes some additional information missing from the
CSV data, such as whether the transaction is pending or completed, but
leaves off the year for current year transactions.
"""

# Converts the start date into datetime format - input must be mm/dd/yy
start_date = convert_mmddyy_to_datetime(start_date)
# Converts the end date into datetime format - input must be mm/dd/yy
end_date = convert_mmddyy_to_datetime(end_date)

all_txns = []
offset = 0
# Mint only returns some of the transactions at once. To get all of
# them, we have to keep asking for more until we reach the end.
while 1:
url = MINT_ROOT_URL + "/getJsonData.xevent"
params = {
"queryNew": "",
"offset": offset,
"comparableType": "8",
"startDate": convert_date_to_string(start_date),
"endDate": convert_date_to_string(end_date),
"rnd": Mint.get_rnd(),
}
# Specifying accountId=0 causes Mint to return investment
# transactions as well. Otherwise they are skipped by
# default.
if self._include_investments_with_transactions(id, include_investment):
params["accountId"] = id
if include_investment:
params["task"] = "transactions"
else:
params["task"] = "transactions,txnfilters"
params["filterType"] = "cash"
result = self.request_and_check(
url,
headers=JSON_HEADER,
params=params,
expected_content_type="text/json|application/json",
)
data = json.loads(result.text)
txns = data["set"][0].get("data", [])
if not txns:
break
all_txns.extend(txns)
offset += len(txns)
return all_txns

def get_detailed_transactions(
self,
include_investment=False,
remove_pending=True,
start_date=None,
end_date=None,
):
"""Returns the JSON transaction data as a DataFrame, and converts
current year dates and prior year dates into consistent datetime
format, and reverses credit activity.
Note: start_date and end_date must be in format mm/dd/yy.
If pulls take too long, consider a narrower range of start and end
date. See json explanation of include_investment.
Expand All @@ -388,63 +313,68 @@ def get_detailed_transactions(
change dates/amounts after the transactions post. They have been
removed by default in this pull, but can be included by changing
remove_pending to False
"""
result = self.get_transactions_json(include_investment, start_date, end_date)

df = pd.DataFrame(self.add_parent_category_to_result(result))
df["odate"] = df["odate"].apply(json_date_to_datetime)

if remove_pending:
df = df[~df.isPending]
df.reset_index(drop=True, inplace=True)

df.amount = df.apply(reverse_credit_amount, axis=1)

return df

def add_parent_category_to_result(self, result):
# Finds the parent category name from the categories object based on
# the transaction category ID
categories = self.get_categories()
for transaction in result:
category = self.get_category_object_from_id(
transaction["categoryId"], categories
)
parent = self._find_parent_from_category(category, categories)
transaction["parentCategoryId"] = self.__format_category_id(parent["id"])
transaction["parentCategoryName"] = parent["name"]
result = self.__call_transactions_endpoint(
include_investment, start_date, end_date, id
)
if "Transaction" in result.keys():
if remove_pending:
filtered = filter(
lambda transaction: transaction["isPending"] == False,
result["Transaction"],
)
transactions = list(filtered)
else:
transactions = result["Transaction"]
for i in transactions:
i["lastUpdatedDate"] = i["metaData"]["lastUpdatedDate"]
i.pop("metaData", None)

return result
else:
raise MintException("Cannot find transaction data")
return transactions

def get_transactions_csv(
self, include_investment=False, start_date=None, end_date=None, acct=0
def __call_transactions_endpoint(
self, include_investment=False, start_date=None, end_date=None, id=0
):
"""Returns the raw CSV transaction data as downloaded from Mint.
If include_investment == True, also includes transactions that Mint
classifies as investment-related. You may find that the investment
transaction data is not sufficiently detailed to actually be useful,
however.
"""

# Specifying accountId=0 causes Mint to return investment
# transactions as well. Otherwise they are skipped by
# default.
if include_investment:
id = 0
if start_date is None:
start_date = self.x_months_ago(2)
else:
start_date = convert_mmddyy_to_datetime(start_date)
if end_date is None:
end_date = date.today()
else:
end_date = convert_mmddyy_to_datetime(end_date)
return self.get(
"{}/pfm/v1/transactions?id={}&fromDate={}&toDate={}".format(
MINT_ROOT_URL, id, start_date, end_date
),
headers=self._get_api_key_header(),
).json()

params = {
"accountId": acct
if self._include_investments_with_transactions(acct, include_investment)
else None,
"startDate": convert_date_to_string(convert_mmddyy_to_datetime(start_date)),
"endDate": convert_date_to_string(convert_mmddyy_to_datetime(end_date)),
}
result = self.request_and_check(
"{}/transactionDownload.event".format(MINT_ROOT_URL),
params=params,
expected_content_type="text/csv",
)
return result.content
def __call_investments_endpoint(self):
return self.get(
"{}/pfm/v1/investments".format(MINT_ROOT_URL),
headers=self._get_api_key_header(),
).json()

def get_categories(self):
return self.get(
"{}/pfm/v1/categories".format(MINT_ROOT_URL),
headers=self._get_api_key_header(),
).json()["Category"]

def __call_accounts_endpoint(self):
return self.get(
"{}/pfm/v1/accounts".format(MINT_ROOT_URL),
headers=self._get_api_key_header(),
).json()

def get_net_worth(self, account_data=None):
if account_data is None:
Expand All @@ -462,23 +392,6 @@ def get_net_worth(self, account_data=None):
]
)

def get_transactions(
self, include_investment=False, start_date=None, end_date=None
):
"""Returns the transaction data as a Pandas DataFrame."""
s = io.BytesIO(
self.get_transactions_csv(
start_date=start_date,
end_date=end_date,
include_investment=include_investment,
)
)
s.seek(0)
df = pd.read_csv(s, parse_dates=["Date"])
df.columns = [c.lower().replace(" ", "_") for c in df.columns]
df.category = df.category.str.lower().replace("uncategorized", pd.NA)
return df

def get_budgets(self):
budgets = self.__call_budgets_endpoint()
if "Budget" in budgets.keys():
Expand All @@ -492,38 +405,11 @@ def get_budgets(self):
def __call_budgets_endpoint(self):
return self.get(
"{}/pfm/v1/budgets?startDate={}&endDate={}".format(
MINT_ROOT_URL, self.__eleven_months_ago(), self.__first_of_this_month()
MINT_ROOT_URL, self.__x_months_ago(11), self.__first_of_this_month()
),
headers=self._get_api_key_header(),
).json()

def get_category_object_from_id(self, cid, categories):
if cid == 0:
return {"parent": "Uncategorized", "depth": 1, "name": "Uncategorized"}

result = filter(
lambda category: self.__format_category_id(category["id"]) == str(cid),
categories,
)
category = list(result)
return (
category[0]
if len(category) > 0
else {"parent": "Unknown", "depth": 1, "name": "Unknown"}
)

def __format_category_id(self, cid):
return cid if str(cid).find("_") == "-1" else str(cid)[str(cid).find("_") + 1 :]

def _find_parent_from_category(self, category, categories):
if category["depth"] == 1:
return {"id": "", "name": ""}

parent = self.get_category_object_from_id(
self.__format_category_id(category["parentId"]), categories
)
return {"id": parent["id"], "name": parent["name"]}

def initiate_account_refresh(self):
self.make_post_request(url="{}/refreshFILogins.xevent".format(MINT_ROOT_URL))

Expand Down Expand Up @@ -641,14 +527,13 @@ def _flatten_utilization(self, data):
)
return utilization

def _include_investments_with_transactions(self, id, include_investment):
return id > 0 or include_investment

def __first_of_this_month(self):
return date.today().replace(day=1)

def __eleven_months_ago(self):
return (self.__first_of_this_month() - timedelta(days=330)).replace(day=1)
def __x_months_ago(self, months=2):
return (self.__first_of_this_month() - relativedelta(months=months)).replace(
day=1
)


def get_accounts(email, password, get_detail=False):
Expand Down

0 comments on commit 6747c58

Please sign in to comment.