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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add method to get budgets #52

Merged
merged 3 commits into from
Jan 4, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ As of writing this README, the following methods are supported:

- `get_accounts` - gets all the accounts linked to Monarch Money
- `get_account_holdings` - gets all of the securities in a brokerage or similar type of account
- `get_budgets` — all the budgets and the corresponding actual amounts
- `get_subscription_details` - gets the Monarch Money account's status (e.g. paid or trial)
- `get_transactions` - gets transaction data, defaults to returning the last 100 transactions; can also be searched by date range
- `get_transaction_categories` - gets all of the categories configured in the account
Expand Down
5 changes: 5 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ def main() -> None:
with open("data.json", "w") as outfile:
json.dump(accounts, outfile)

# Budgets
budgets = asyncio.run(mm.get_budgets())
with open("budgets.json", "w") as outfile:
json.dump(budgets, outfile)

# # Transaction categories
categories = asyncio.run(mm.get_transaction_categories())
with open("categories.json", "w") as outfile:
Expand Down
237 changes: 237 additions & 0 deletions monarchmoney/monarchmoney.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,243 @@ async def get_account_holdings(self, account_id: int) -> Dict[str, Any]:
variables=variables,
)

async def get_budgets(
self,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
use_legacy_goals: Optional[bool] = False,
use_v2_goals: Optional[bool] = True,
Comment on lines +397 to +398
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will it still work if someone sets both of these to True?

Comment on lines +397 to +398
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if you both of these are set to True or False? Are those valid parameter combinations?

If a condition for validity of the API call is that use_legacy_goals != use_v2_goals, I wonder if we can just simplify this to on use_legacy_goals parameter that determines the values of both the parameters in the API call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@grablair This is the result from the query:

(use_legacy_goals=true, use_v2_goals=true): two lists returned. One list under dictionary key "goals" and another list under dictionary key "goalsV2". If you have no goals, the list will be empty.
(use_legacy_goals=false, use_v2_goals=false): no lists returned. No dictionary key called "goals" and "goalsV2" respectively.

So all combinations could be valid

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thank you!

) -> Dict[str, Any]:
"""
Get your budgets and corresponding actual amounts from the account.

When no date arguments given:
| `start_date` will default to last month based on todays date
| `end_date` will default to next month based on todays date

:param start_date:
the earliest date to get budget data, in "yyyy-mm-dd" format (default: last month)
:param end_date:
the latest date to get budget data, in "yyyy-mm-dd" format (default: next month)
:param use_legacy_goals:
Set True to return a list of monthly budget set aside for goals (default: no list)
:param use_v2_goals:
Set True to return a list of monthly budget set aside for version 2 goals (default list)
"""
query = gql(
"""
query GetJointPlanningData($startDate: Date!, $endDate: Date!, $useLegacyGoals: Boolean!, $useV2Goals: Boolean!) {
budgetData(startMonth: $startDate, endMonth: $endDate) {
monthlyAmountsByCategory {
category {
id
__typename
}
monthlyAmounts {
month
plannedCashFlowAmount
plannedSetAsideAmount
actualAmount
remainingAmount
previousMonthRolloverAmount
rolloverType
__typename
}
__typename
}
monthlyAmountsByCategoryGroup {
categoryGroup {
id
__typename
}
monthlyAmounts {
month
plannedCashFlowAmount
actualAmount
remainingAmount
previousMonthRolloverAmount
rolloverType
__typename
}
__typename
}
monthlyAmountsForFlexExpense {
budgetVariability
monthlyAmounts {
month
plannedCashFlowAmount
actualAmount
remainingAmount
previousMonthRolloverAmount
rolloverType
__typename
}
__typename
}
totalsByMonth {
month
totalIncome {
plannedAmount
actualAmount
remainingAmount
previousMonthRolloverAmount
__typename
}
totalExpenses {
plannedAmount
actualAmount
remainingAmount
previousMonthRolloverAmount
__typename
}
totalFixedExpenses {
plannedAmount
actualAmount
remainingAmount
previousMonthRolloverAmount
__typename
}
totalNonMonthlyExpenses {
plannedAmount
actualAmount
remainingAmount
previousMonthRolloverAmount
__typename
}
totalFlexibleExpenses {
plannedAmount
actualAmount
remainingAmount
previousMonthRolloverAmount
__typename
}
__typename
}
__typename
}
categoryGroups {
id
name
order
groupLevelBudgetingEnabled
budgetVariability
rolloverPeriod {
id
startMonth
endMonth
__typename
}
categories {
id
name
icon
order
budgetVariability
rolloverPeriod {
id
startMonth
endMonth
__typename
}
__typename
}
type
__typename
}
goals @include(if: $useLegacyGoals) {
id
name
icon
completedAt
targetDate
__typename
}
goalMonthlyContributions(startDate: $startDate, endDate: $endDate) @include(if: $useLegacyGoals) {
mount: monthlyContribution
startDate
goalId
__typename
}
goalPlannedContributions(startDate: $startDate, endDate: $endDate) @include(if: $useLegacyGoals) {
id
amount
startDate
goal {
id
__typename
}
__typename
}
goalsV2 @include(if: $useV2Goals) {
id
name
archivedAt
completedAt
priority
imageStorageProvider
imageStorageProviderId
plannedContributions(startMonth: $startDate, endMonth: $endDate) {
id
month
amount
__typename
}
monthlyContributionSummaries(startMonth: $startDate, endMonth: $endDate) {
month
sum
__typename
}
__typename
}
budgetSystem
}
"""
)

variables = {
"startDate": start_date,
"endDate": end_date,
"useLegacyGoals": use_legacy_goals,
"useV2Goals": use_v2_goals,
}

if not start_date and not end_date:
# Default start_date to last month and end_date to next month
today = datetime.today()

# Get the first day of last month
last_month = today.month - 1
last_month_year = today.year
first_day_of_last_month = 1
if last_month < 1:
last_month_year -= 1
last_month = 12
variables["startDate"] = datetime(
last_month_year, last_month, first_day_of_last_month
).strftime("%Y-%m-%d")

# Get the last day of next month
next_month = today.month + 1
next_month_year = today.year
if next_month > 12:
next_month_year += 1
next_month = 1
last_day_of_next_month = calendar.monthrange(next_month_year, next_month)[1]
variables["endDate"] = datetime(
next_month_year, next_month, last_day_of_next_month
).strftime("%Y-%m-%d")
Comment on lines +598 to +617
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use Python's timedelta instead of computing these yourself:

# Get first day of last month
last_month_start = today - datetime.timedelta(weeks=4)
last_month.replace(day=1)
variables["startDate"] = last_month_start.strftime("%Y-%m-%d")

# Get the last day of the next month
next_month_end = today + datetime.timedelta(weeks=4)
next_month_end.replace(
  day=calendar.monthrange(
    next_month_end.year, 
    next_month_end.month
  )[1]
)
variables["endDate"] = next_month_end.strftime("%Y-%m-%d")


elif bool(start_date) != bool(end_date):
raise Exception(
"You must specify both a startDate and endDate, not just one of them."
)

return await self.gql_call(
operation="GetJointPlanningData",
graphql_query=query,
variables=variables,
)

async def get_subscription_details(self) -> Dict[str, Any]:
"""
The type of subscription for the Monarch Money account.
Expand Down
Loading