Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

馃敤 - Update Accounts Endpoint #430

Merged
merged 5 commits into from Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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