Skip to content

Commit

Permalink
[FEATURE] - Support multiple data types from CLI (#450)
Browse files Browse the repository at this point in the history
* [FEATURE] - Support multiple data types from CLI

* PR suggestions

* One more PR Suggestion
  • Loading branch information
burkematthew committed Apr 23, 2022
1 parent 20f5249 commit bc6e78c
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 78 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
- Update the overview page url (#414)

BREAKING CHANGES:
- :warning: `mfa_method` is only required if your login flow presents you with the option to select which Multifactor Authentication Method you wish to use, typically as a result of your account configured to accept different methods. #392 provided a way to automatically detect the type of MFA requested by Mint, based on the prompts on the screen. Because of this, Mintapi now supports the use case where multiple MFA prompts appear.
- :warning: `mfa_method` is only required if your login flow presents you with the option to select which Multifactor Authentication Method you wish to use, typically as a result of your account configured to accept different methods. #392 provided a way to automatically detect the type of MFA requested by Mint, based on the prompts on the screen. Because of this, MintAPI now supports the use case where multiple MFA prompts appear.
- Because of the new Mint UI and the switchover to different API Endpoints, the data structure associated with each function may be different. Please verify that the data you are expecting against the new output of each endpoint.
- To help support the new CLI option for data format (`--format`), we have eliminated the need to specify a file extension in `--filename`. Be aware that if you do not specify `--format=csv`, then the extension\format will be `json`, which means that any filename you specify will include the extension of `.json`.

- In addition to the above, the CLI now supports receiving multiple types of data in one call to MintAPI. When exporting multiple data types, you can either send it directly to the `stdout` or you can export to a data file. What MintAPI will do with your specified filename is add a suffix based on the type of data you are exporting. For example, if you specify `current` as your filename and you export `account` and `transaction`, then you will receive two files: `current_account` and `current_transaction`.


1.64
Expand Down
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ An unofficial screen-scraping API for Mint.com.
Please [join us on Discord](https://discord.gg/YjJEuJRAu9) to get help or just chat with fellow mintapi users :)

## Installation

Ensure you have Python 3 and pip (`easy_install pip`) and then:

```shell
Expand All @@ -22,7 +23,7 @@ pip install mintapi

## Usage

### from the command line
### From the Command Line

From the command line, the most automated invocation will be:

Expand All @@ -43,16 +44,19 @@ If you're running mintapi in a server environment on an automatic schedule, cons
If you need to download the chromedriver manually, be sure to get the version that matches your chrome version and make the chromedriver available to your python interpreter either by putting the chromedriver in your python working directory or inside your `PATH` as described in the [python selenium documentation](https://www.selenium.dev/selenium/docs/api/py/index.html#drivers).

### General Automation Scenarios

When running this inside of a cron job or other long-term automation scripts, it might be helpful to specify chrome and chromedriver executables so as not to conflict with other chrome versions you may have. Selenium by default just gets these from your `PATH` environment variable, so customizing your environment can force a deterministic behavior from mintapi. To use a different browser besides Chrome or Chromium, see the [python api](#from-python). Below are two examples.

#### Unix Environment

If I wanted to make sure that mintapi used the chromium executable in my /usr/bin directory when executing a cron job, I could write the following cron line:
```cron
0 7 * * FRI PATH=/usr/bin:$PATH mintapi --headless john@example.com my_password
```
where prepending the /usr/bin path to path will make those binaries found first. This will only affect the cron job and will not change the environment for any other process.

#### Docker Image

You can also use the docker image to help manage your environment so you don't have to worry about chrome or chromedriver versions. There are a few caveats:
1. Headless mode is recommended. GUI works but introduces the need to configure an X11 server which varies with setup. Google is your friend.
2. Almost always use the flag `--use-chromedriver-on-path` as the chrome and chromedriver built into the docker image already match and getting the latest will break the image.
Expand All @@ -64,7 +68,9 @@ docker run --rm --shm-size=2g ghcr.io/mintapi/mintapi mintapi john@example.com m
```

#### Windows Environment

You can do a similar thing in windows by executing the following in Powershell.

```powershell
$ENV:PATH = "C:\Program Files\Google\Chrome;$ENV:PATH"
mintapi --headless john@example.com my_password
Expand All @@ -80,7 +86,24 @@ If `mfa-method` is soft-token then you must also pass your `mfa-token`. The `mfa

While Mint supports authentication via Voice, `mintapi` does not currently support this option. Compatability with this method will be added in a later version.

### from Python
### Multi-Data Support

As of v2.0, MintAPI supports returning multiple types of data in one call to MintAPI. When exporting multiple data types, you can either send it directly to `stdout` or you can export to a data file. What MintAPI will do with your specified filename is add a suffix based on the type of data you are exporting. The following table outlines the option selected and its corresponding suffix:

| Option | Suffix |
| ----------- | ----------- |
| accounts | account |
| budgets | budget |
| transactions | transaction |
| categories | category |
| investments | investment |
| net-worth | net_worth |
| credit-score | credit_score |
| credit-report| credit_report|

For example, if you specify `current` as your filename, format as csv, and you export `account` and `transaction`, then you will receive two files: `current_account.csv` and `current_transaction.csv`.

### From Python

From python, instantiate the Mint class (from the mintapi package) and you can
make calls to retrieve account/budget information. We recommend using the
Expand Down
26 changes: 11 additions & 15 deletions mintapi/api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
from mintapi import constants
import logging
import os
import random
Expand All @@ -12,42 +13,37 @@

logger = logging.getLogger("mintapi")

ACCOUNT_KEY = "Account"
BUDGET_KEY = "Budget"
CATEGORY_KEY = "Category"
INVESTMENT_KEY = "Investment"
TRANSACTION_KEY = "Transaction"

ENDPOINTS = {
ACCOUNT_KEY: {
constants.ACCOUNT_KEY: {
"apiVersion": "pfm/v1",
"endpoint": "accounts",
"beginningDate": None,
"endingDate": None,
"includeCreatedDate": True,
},
BUDGET_KEY: {
constants.BUDGET_KEY: {
"apiVersion": "pfm/v1",
"endpoint": "budgets",
"beginningDate": "startDate",
"endingDate": "endDate",
"includeCreatedDate": True,
},
CATEGORY_KEY: {
constants.CATEGORY_KEY: {
"apiVersion": "pfm/v1",
"endpoint": "categories",
"beginningDate": None,
"endingDate": None,
"includeCreatedDate": False,
},
INVESTMENT_KEY: {
constants.INVESTMENT_KEY: {
"apiVersion": "pfm/v1",
"endpoint": "investments",
"beginningDate": None,
"endingDate": None,
"includeCreatedDate": False,
},
TRANSACTION_KEY: {
constants.TRANSACTION_KEY: {
"apiVersion": "pfm/v1",
"endpoint": "transactions",
"beginningDate": "fromDate",
Expand Down Expand Up @@ -236,20 +232,20 @@ def get_account_data(
self,
limit=5000,
):
return self.get_data(ACCOUNT_KEY, limit)
return self.get_data(constants.ACCOUNT_KEY, limit)

def get_categories(
self,
limit=5000,
):
return self.get_data(CATEGORY_KEY, limit)
return self.get_data(constants.CATEGORY_KEY, limit)

def get_budgets(
self,
limit=5000,
):
return self.get_data(
BUDGET_KEY,
constants.BUDGET_KEY,
limit,
None,
start_date=self.__x_months_ago(11),
Expand All @@ -261,7 +257,7 @@ def get_investment_data(
limit=5000,
):
return self.get_data(
INVESTMENT_KEY,
constants.INVESTMENT_KEY,
limit,
)

Expand Down Expand Up @@ -289,7 +285,7 @@ def get_transaction_data(
if include_investment:
id = 0
data = self.get_data(
TRANSACTION_KEY,
constants.TRANSACTION_KEY,
limit,
id,
convert_mmddyy_to_datetime(start_date),
Expand Down
92 changes: 41 additions & 51 deletions mintapi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import sys
import json
import getpass

from mintapi import constants
import keyring
import configargparse

Expand All @@ -14,9 +14,6 @@

logger = logging.getLogger("mintapi")

JSON_FORMAT = "json"
CSV_FORMAT = "csv"


def parse_arguments(args):
ARGUMENTS = [
Expand Down Expand Up @@ -152,8 +149,8 @@ def parse_arguments(args):
(
("--format",),
{
"choices": [JSON_FORMAT, CSV_FORMAT],
"default": JSON_FORMAT,
"choices": [constants.JSON_FORMAT, constants.CSV_FORMAT],
"default": constants.JSON_FORMAT,
"help": "The format used to return data.",
},
),
Expand Down Expand Up @@ -303,30 +300,30 @@ def handle_password(type, prompt, email, password, use_keyring=False):
return password


def format_filename(options):
def format_filename(options, type):
if options.filename is None:
filename = None
else:
filename = "{}.{}".format(options.filename, options.format)
filename = "{}_{}.{}".format(options.filename, type.lower(), options.format)
return filename


def output_data(options, data, attention_msg=None):
filename = format_filename(options)
def output_data(options, data, type, attention_msg=None):
filename = format_filename(options, type)
if filename is None:
if options.format == CSV_FORMAT:
if options.format == constants.CSV_FORMAT:
print(json_normalize(data).to_csv(index=False))
else:
print(json.dumps(data, indent=2))
# NOTE: While this logic is here, unless validate_file_extensions
# allows for other data types to export to CSV, this will
# only include investment data.
elif options.format == CSV_FORMAT:
elif options.format == constants.CSV_FORMAT:
# NOTE: Currently, investment_data, which is a flat JSON, is the only
# type of data that uses this section. So, if we open this up to
# other non-flat JSON data, we will need to revisit this.
json_normalize(data).to_csv(filename, index=False)
elif options.format == JSON_FORMAT:
elif options.format == constants.JSON_FORMAT:
with open(filename, "w+") as f:
json.dump(data, f, indent=2)

Expand Down Expand Up @@ -416,63 +413,56 @@ def main():
print("MFA CODE:", mfa_code)
sys.exit()

data = None
if options.accounts and options.budgets:
try:
data = mint.get_account_data(limit=options.limit)
except Exception:
accounts = None

try:
budgets = mint.get_budgets(limit=options.limit)
except Exception:
budgets = None

data = {"accounts": accounts, "budgets": budgets}
elif options.budgets:
try:
data = mint.get_budgets(limit=options.limit)
except Exception:
data = None
attention_msg = None
if options.attention:
attention_msg = mint.get_attention()

if options.accounts:
data = mint.get_account_data(limit=options.limit)
output_data(options, data, constants.ACCOUNT_KEY, attention_msg)

if options.budgets:
data = mint.get_budgets(limit=options.limit)
output_data(options, data, constants.BUDGET_KEY, attention_msg)
elif options.budget_hist:
try:
data = mint.get_budgets(limit=options.limit, hist=12)
except Exception:
data = None
elif options.accounts:
try:
data = mint.get_account_data(limit=options.limit)
except Exception:
data = None
elif options.transactions:
data = mint.get_budgets(limit=options.limit, hist=12)
output_data(options, data, constants.BUDGET_KEY, attention_msg)

if options.transactions:
data = mint.get_transaction_data(
limit=options.limit,
start_date=options.start_date,
end_date=options.end_date,
include_investment=options.include_investment,
remove_pending=options.show_pending,
)
elif options.categories:
output_data(options, data, constants.TRANSACTION_KEY, attention_msg)

if options.categories:
data = mint.get_categories(
limit=options.limit,
)
elif options.investments:
output_data(options, data, constants.CATEGORY_KEY, attention_msg)

if options.investments:
data = mint.get_investment_data(
limit=options.limit,
)
elif options.net_worth:
output_data(options, data, constants.INVESTMENT_KEY, attention_msg)

if options.net_worth:
data = mint.get_net_worth()
elif options.credit_score:
output_data(options, data, constants.NET_WORTH_KEY, attention_msg)

if options.credit_score:
data = mint.get_credit_score()
elif options.credit_report:
output_data(options, data, constants.CREDIT_SCORE_KEY, attention_msg)

if options.credit_report:
data = mint.get_credit_report(
details=True,
exclude_inquiries=options.exclude_inquiries,
exclude_accounts=options.exclude_accounts,
exclude_utilization=options.exclude_utilization,
)

attention_msg = None
if options.attention:
attention_msg = mint.get_attention()
output_data(options, data, attention_msg)
output_data(options, data, constants.CREDIT_REPORT_KEY, attention_msg)
11 changes: 11 additions & 0 deletions mintapi/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
JSON_FORMAT = "json"
CSV_FORMAT = "csv"

ACCOUNT_KEY = "Account"
BUDGET_KEY = "Budget"
CATEGORY_KEY = "Category"
INVESTMENT_KEY = "Investment"
TRANSACTION_KEY = "Transaction"
NET_WORTH_KEY = "Net_Worth"
CREDIT_SCORE_KEY = "Credit_Score"
CREDIT_REPORT_KEY = "Credit_Report"

0 comments on commit bc6e78c

Please sign in to comment.