diff --git a/mintapi/api.py b/mintapi/api.py
index 5c61ef37..a93134af 100644
--- a/mintapi/api.py
+++ b/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
@@ -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
@@ -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"
@@ -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,
@@ -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:
@@ -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 = "
" + response["accountHeader"] + "
"
- xml = xml.replace("–", "-")
- 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():
@@ -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)
diff --git a/mintapi/cli.py b/mintapi/cli.py
index 822bc399..ed245083 100644
--- a/mintapi/cli.py
+++ b/mintapi/cli.py
@@ -3,7 +3,6 @@
import os
import sys
import json
-from datetime import datetime
import getpass
import keyring
@@ -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",),
{
@@ -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
@@ -386,9 +358,6 @@ def main():
options.keyring,
)
- if options.accounts_ext:
- options.accounts = True
-
if not any(
[
options.accounts,
@@ -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
@@ -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:
diff --git a/tests/test_driver.py b/tests/test_driver.py
index 9ebed95b..4eae0aa6 100644
--- a/tests/test_driver.py
+++ b/tests/test_driver.py
@@ -14,15 +14,62 @@
from unittest.mock import patch, DEFAULT
-accounts_example = [
- {
- "accountName": "Chase Checking",
- "lastUpdated": 1401201492000,
- "lastUpdatedInString": "25 minutes",
- "accountType": "bank",
- "currentBalance": 100.12,
- }
-]
+accounts_example = {
+ "Account": [
+ {
+ "type": "CreditAccount",
+ "userCardType": "UNKNOWN",
+ "creditAccountType": "CREDIT_CARD",
+ "creditLimit": 2222.0,
+ "availableCredit": 1111.0,
+ "interestRate": 0.444,
+ "minPayment": 111.0,
+ "absoluteMinPayment": 111.0,
+ "statementMinPayment": 22.0,
+ "statementDueDate": "2022-04-19T07:00:00Z",
+ "statementDueAmount": 0.0,
+ "metaData": {
+ "createdDate": "2017-01-05T17:12:15Z",
+ "lastUpdatedDate": "2022-03-27T16:46:41Z",
+ "link": [
+ {
+ "otherAttributes": {},
+ "href": "/v1/accounts/id",
+ "rel": "self",
+ }
+ ],
+ },
+ "id": "id",
+ "name": "name",
+ "value": -555.55,
+ "isVisible": True,
+ "isDeleted": False,
+ "planningTrendsVisible": True,
+ "accountStatus": "ACTIVE",
+ "systemStatus": "ACTIVE",
+ "currency": "USD",
+ "fiLoginId": "fiLoginId",
+ "fiLoginStatus": "OK",
+ "currentBalance": 555.55,
+ "cpId": "cpId",
+ "cpAccountName": "cpAccountName",
+ "cpAccountNumberLast4": "cpAccountNumberLast4",
+ "hostAccount": False,
+ "fiName": "fiName",
+ "accountTypeInt": 0,
+ "isAccountClosedByMint": False,
+ "isAccountNotFound": False,
+ "isActive": True,
+ "isClosed": False,
+ "isError": False,
+ "isHiddenFromPlanningTrends": True,
+ "isTerminal": True,
+ "credentialSetId": "credentialSetId",
+ "ccAggrStatus": "0",
+ }
+ ]
+}
+
category_example = [
{
@@ -214,27 +261,6 @@ def request(a, b, **c):
class MintApiTests(unittest.TestCase):
- @patch.object(mintapi.api, "sign_in")
- @patch.object(mintapi.api, "_create_web_driver_at_mint_com")
- def test_accounts(self, mock_driver, mock_sign_in):
- mock_driver.return_value = TestMock()
- mock_sign_in.return_value = ("test", "token")
- accounts = mintapi.get_accounts("foo", "bar")
-
- self.assertFalse("lastUpdatedInDate" in accounts)
- self.assertNotEqual(accounts, accounts_example)
-
- accounts_annotated = copy.deepcopy(accounts_example)
- for account in accounts_annotated:
- account["lastUpdatedInDate"] = datetime.datetime.fromtimestamp(
- account["lastUpdated"] / 1000
- )
- self.assertEqual(accounts, accounts_annotated)
-
- # ensure everything is json serializable as this is the command-line
- # behavior.
- mintapi.cli.print_accounts(accounts)
-
def test_chrome_driver_links(self):
latest_version = mintapi.signIn.get_latest_chrome_driver_version()
for platform in mintapi.signIn.CHROME_ZIP_TYPES:
@@ -314,6 +340,14 @@ def test_build_bundledServiceController_url(self, mock_driver):
url = mintapi.Mint.build_bundledServiceController_url(mock_driver)
self.assertTrue(mintapi.api.MINT_ROOT_URL in url)
+ @patch.object(mintapi.Mint, "_Mint__call_accounts_endpoint")
+ def test_get_investment_data_new(self, mock_call_accounts_endpoint):
+ mock_call_accounts_endpoint.return_value = accounts_example
+ account_data = mintapi.Mint().get_account_data()[0]
+ self.assertFalse("metaData" in account_data)
+ self.assertTrue("createdDate" in account_data)
+ self.assertTrue("lastUpdatedDate" in account_data)
+
@patch.object(mintapi.Mint, "_Mint__call_transactions_endpoint")
def test_get_transaction_data(self, mock_call_transactions_endpoint):
mock_call_transactions_endpoint.return_value = transactions_example