Skip to content

Commit

Permalink
🔨 - Update Accounts Endpoint (#430)
Browse files Browse the repository at this point in the history
* [CHORE] - Update Accounts Endpoint

* [CHORE] - Part 2 of Accounts Endpoint

* Final Cleanup
  • Loading branch information
burkematthew committed Mar 29, 2022
1 parent 3d875fe commit 5f99939
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 205 deletions.
167 changes: 29 additions & 138 deletions mintapi/api.py
@@ -1,7 +1,5 @@
from datetime import date, datetime, timedelta
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
import io
import json
import logging
import os
import random
Expand All @@ -10,9 +8,6 @@
import time
import warnings

import xmltodict
import pandas as pd

from mintapi.signIn import sign_in, _create_web_driver_at_mint_com


Expand Down Expand Up @@ -58,27 +53,6 @@ def parse_float(str_number):
return None


DATE_FIELDS = [
"addAccountDate",
"closeDate",
"fiLastUpdated",
"lastUpdated",
]


def convert_account_dates_to_datetime(account):
for df in DATE_FIELDS:
if df in account:
# Convert from javascript timestamp to unix timestamp
# http://stackoverflow.com/a/9744811/5026
try:
ts = account[df] / 1e3
except TypeError:
# returned data is not a number, don't parse
continue
account[df + "InDate"] = datetime.fromtimestamp(ts)


MINT_ROOT_URL = "https://mint.intuit.com"
MINT_ACCOUNTS_URL = "https://accounts.intuit.com"
MINT_CREDIT_URL = "https://credit.finance.intuit.com"
Expand Down Expand Up @@ -300,6 +274,23 @@ def get_investment_data(self):
raise MintException("Cannot find investment data")
return investments["Investment"]

def get_account_data(self):
accounts = self.__call_accounts_endpoint()
if "Account" in accounts.keys():
for i in accounts["Account"]:
i["createdDate"] = i["metaData"]["createdDate"]
i["lastUpdatedDate"] = i["metaData"]["lastUpdatedDate"]
i.pop("metaData", None)
else:
raise MintException("Cannot find account data")
return accounts["Account"]

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

def get_transaction_data(
self,
include_investment=False,
Expand Down Expand Up @@ -348,7 +339,7 @@ def __call_transactions_endpoint(
if include_investment:
id = 0
if start_date is None:
start_date = self.x_months_ago(2)
start_date = self.__x_months_ago(2)
else:
start_date = convert_mmddyy_to_datetime(start_date)
if end_date is None:
Expand All @@ -374,126 +365,26 @@ def get_categories(self):
headers=self._get_api_key_header(),
).json()["Category"]

def get_accounts(self, get_detail=False): # {{{
# Issue service request.
req_id = self.get_request_id_str()

input = {
"args": {
"types": [
"BANK",
"CREDIT",
"INVESTMENT",
"LOAN",
"MORTGAGE",
"OTHER_PROPERTY",
"REAL_ESTATE",
"VEHICLE",
"UNCLASSIFIED",
]
},
"id": req_id,
"service": "MintAccountService",
"task": "getAccountsSorted"
# 'task': 'getAccountsSortedByBalanceDescending'
}

data = {"input": json.dumps([input])}
response = self.make_post_request(
url=self.build_bundledServiceController_url(),
data=data,
convert_to_text=True,
)
if req_id not in response:
raise MintException("Could not parse account data: " + response)

# Parse the request
response = json.loads(response)
accounts = response["response"][req_id]["response"]

for account in accounts:
convert_account_dates_to_datetime(account)

if get_detail:
accounts = self.populate_extended_account_detail(accounts)

return accounts
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:
account_data = self.get_accounts()
account_data = self.get_account_data()

# account types in this list will be subtracted
invert = set(["loan", "loans", "credit"])
invert = set(["LoanAccount", "CreditAccount"])
return sum(
[
-a["currentBalance"]
if a["accountType"] in invert
else a["currentBalance"]
-a["currentBalance"] if a["type"] in invert else a["currentBalance"]
for a in account_data
if a["isActive"]
]
)

def populate_extended_account_detail(self, accounts): # {{{
# I can't find any way to retrieve this information other than by
# doing this stupid one-call-per-account to listTransactions.xevent
# and parsing the HTML snippet :(
for account in accounts:
headers = dict(JSON_HEADER)
headers["Referer"] = "{}/transaction.event?accountId={}".format(
MINT_ROOT_URL, account["id"]
)

list_txn_url = "{}/listTransaction.xevent".format(MINT_ROOT_URL)
params = {
"accountId": str(account["id"]),
"queryNew": "",
"offset": 0,
"comparableType": 8,
"acctChanged": "T",
"rnd": Mint.get_rnd(),
}

response = json.loads(
self.get(list_txn_url, params=params, headers=headers).text
)
xml = "<div>" + response["accountHeader"] + "</div>"
xml = xml.replace("&#8211;", "-")
xml = xmltodict.parse(xml)

account["availableMoney"] = None
account["totalFees"] = None
account["totalCredit"] = None
account["nextPaymentAmount"] = None
account["nextPaymentDate"] = None

xml = xml["div"]["div"][1]["table"]
if "tbody" not in xml:
continue
xml = xml["tbody"]
table_type = xml["@id"]
xml = xml["tr"][1]["td"]

if table_type == "account-table-bank":
account["availableMoney"] = parse_float(xml[1]["#text"])
account["totalFees"] = parse_float(xml[3]["a"]["#text"])
if account["interestRate"] is None:
account["interestRate"] = parse_float(xml[2]["#text"]) / 100.0
elif table_type == "account-table-credit":
account["availableMoney"] = parse_float(xml[1]["#text"])
account["totalCredit"] = parse_float(xml[2]["#text"])
account["totalFees"] = parse_float(xml[4]["a"]["#text"])
if account["interestRate"] is None:
account["interestRate"] = parse_float(xml[3]["#text"]) / 100.0
elif table_type == "account-table-loan":
account["nextPaymentAmount"] = parse_float(xml[1]["#text"])
account["nextPaymentDate"] = xml[2].get("#text", None)
elif table_type == "account-type-investment":
account["totalFees"] = parse_float(xml[2]["a"]["#text"])

return accounts

def get_budgets(self):
budgets = self.__call_budgets_endpoint()
if "Budget" in budgets.keys():
Expand Down Expand Up @@ -640,12 +531,12 @@ def __x_months_ago(self, months=2):

def get_accounts(email, password, get_detail=False):
mint = Mint(email, password)
return mint.get_accounts(get_detail=get_detail)
return mint.get_account_data(get_detail=get_detail)


def get_net_worth(email, password):
mint = Mint(email, password)
account_data = mint.get_accounts()
account_data = mint.get_account_data()
return mint.get_net_worth(account_data)


Expand Down
39 changes: 2 additions & 37 deletions mintapi/cli.py
Expand Up @@ -3,7 +3,6 @@
import os
import sys
import json
from datetime import datetime
import getpass

import keyring
Expand Down Expand Up @@ -117,15 +116,6 @@ def parse_arguments(args):
"help": "Latest date for transactions to be retrieved from. Used with --transactions. Format: mm/dd/yy",
},
),
(
("--extended-accounts",),
{
"action": "store_true",
"dest": "accounts_ext",
"default": False,
"help": "Retrieve extended account information (slower, implies --accounts)",
},
),
(
("--exclude-accounts",),
{
Expand Down Expand Up @@ -278,24 +268,6 @@ def parse_arguments(args):
return cmdline.parse_args(args)


def make_accounts_presentable(accounts, presentable_format="EXCEL"):
formatter = {
"DATE": "%Y-%m-%d",
"ISO8601": "%Y-%m-%dT%H:%M:%SZ",
"EXCEL": "%Y-%m-%d %H:%M:%S",
}[presentable_format]

for account in accounts:
for k, v in account.items():
if isinstance(v, datetime):
account[k] = v.strftime(formatter)
return accounts


def print_accounts(accounts):
print(json.dumps(make_accounts_presentable(accounts), indent=2))


def handle_password(type, prompt, email, password, use_keyring=False):
if use_keyring and not password:
# If we don't yet have a password, try prompting for it
Expand Down Expand Up @@ -386,9 +358,6 @@ def main():
options.keyring,
)

if options.accounts_ext:
options.accounts = True

if not any(
[
options.accounts,
Expand Down Expand Up @@ -441,9 +410,7 @@ def main():
data = None
if options.accounts and options.budgets:
try:
accounts = make_accounts_presentable(
mint.get_accounts(get_detail=options.accounts_ext)
)
data = mint.get_account_data()
except Exception:
accounts = None

Expand All @@ -465,9 +432,7 @@ def main():
data = None
elif options.accounts:
try:
data = make_accounts_presentable(
mint.get_accounts(get_detail=options.accounts_ext)
)
data = mint.get_account_data()
except Exception:
data = None
elif options.transactions:
Expand Down

0 comments on commit 5f99939

Please sign in to comment.