Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9b8c69b
feat: upload and parse account balance
CalvinChanCan Mar 2, 2024
813adca
chore: fix formatting
CalvinChanCan Mar 2, 2024
1593b90
chore: update README.md
CalvinChanCan Mar 2, 2024
9bf0010
feat: add delete_account
CalvinChanCan Feb 14, 2024
cbff400
feat: upload and parse account balance
CalvinChanCan Mar 2, 2024
1d2eb40
chore: fix formatting
CalvinChanCan Mar 2, 2024
a4c614c
chore: update README.md
CalvinChanCan Mar 2, 2024
76ebcde
Merge remote-tracking branch 'origin/feat-upload-balance-history' int…
CalvinChanCan Mar 7, 2024
00c6760
remove delete_account from other branch
CalvinChanCan Mar 7, 2024
7ad7961
feat: add delete_account
CalvinChanCan Feb 14, 2024
967447b
fix merge conflicts
CalvinChanCan Mar 8, 2024
ad421a8
chore: update README.md
CalvinChanCan Mar 2, 2024
d6f5180
remove delete_account from other branch
CalvinChanCan Mar 7, 2024
14beaf3
Merge remote-tracking branch 'origin/feat-upload-balance-history' int…
CalvinChanCan Mar 8, 2024
1d07ae0
add back delete_account
CalvinChanCan Mar 8, 2024
8b37717
refactor: keep existing upload_account_balance_history
CalvinChanCan Mar 9, 2024
76d25b2
fix: add back delete_account to readme
CalvinChanCan Mar 9, 2024
e7acd66
chore: rename _parse_upload_balance_history_session to _initiate_uplo…
CalvinChanCan Mar 9, 2024
1fe7987
chore: rename get_upload_balance_history_session to _is_upload_balanc…
CalvinChanCan Mar 9, 2024
ee959a3
refactor: define constants for delay and timeout to use consistently …
CalvinChanCan Mar 9, 2024
7f68c9c
chore: move description into body of docblock
CalvinChanCan Mar 9, 2024
44711ad
chore: lint
CalvinChanCan Mar 9, 2024
4e2d624
fix: update return type from str to bool for upload_account_balance_h…
CalvinChanCan Mar 10, 2024
53b6abc
refactor: replace csv_content str with dataclass
CalvinChanCan Mar 10, 2024
86a0311
chore: add docstring
CalvinChanCan Mar 10, 2024
ee41dc6
Update monarchmoney/monarchmoney.py
hammem Jan 19, 2025
9394a97
Update monarchmoney/monarchmoney.py
hammem Jan 19, 2025
508f9bf
fix: persist long-lived session tokens (Fixes #139)
Oct 10, 2025
2d4f1b4
Update README.md
bradleyseanf Jan 3, 2026
89a9ce9
Create assets folder
bradleyseanf Jan 3, 2026
b6bbf50
Update readme.Md with auth correction notice
Jan 3, 2026
d97de7f
Merge branch 'hammem:main' into fix/session-persistence-#139
bradleyseanf Jan 11, 2026
34e440d
Fix get_budgets query for legacy goals removal
bradleyseanf Jan 11, 2026
b6732d6
Merge remote-tracking branch 'origin/fix/session-persistence-#139' in…
bradleyseanf Jan 11, 2026
a1703a3
Merge remote-tracking branch 'origin/fix/get_budgets()-query' into ma…
bradleyseanf Jan 11, 2026
d317b9a
updating gql endpoint
bradleyf-2025 Jan 15, 2026
3989f3e
Release v1.0.0
Jan 17, 2026
147b1aa
Update README
Jan 17, 2026
88e3dc7
Merge branch 'community-dev' into feat-upload-balance-history
CalvinChanCan Jan 17, 2026
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ As of writing this README, the following methods are supported:
- `set_budget_amount` - sets a budget's value to the given amount (date allowed, will only apply to month specified by default). A zero amount value will "unset" or "clear" the budget for the given category.
- `create_manual_account` - creates a new manual account
- `delete_account` - deletes an account by the provided account id
Copy link
Owner

Choose a reason for hiding this comment

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

Please pull this branch off of your other commits in the other PR, so it doesn't accidentally introduce those changes simultaneously.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done!

- `upload_account_balance_history` - uploads account history csv file for a given account
- `upload_account_balance_history` - uploads and parses account history csv file for a given account

# Contributing

Expand Down
125 changes: 113 additions & 12 deletions monarchmoney/monarchmoney.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
AUTH_HEADER_KEY = "authorization"
CSRF_KEY = "csrftoken"
DEFAULT_RECORD_LIMIT = 100
DELAY = 10
ERRORS_KEY = "error_code"
SESSION_DIR = ".mm"
SESSION_FILE = f"{SESSION_DIR}/mm_session.pickle"
TIMEOUT = 300


class MonarchMoneyEndpoints(object):
Expand Down Expand Up @@ -127,6 +129,17 @@ async def multi_factor_authenticate(
"""Performs multi-factor authentication to access a Monarch Money account."""
await self._multi_factor_authenticate(email, password, code)

async def _upload_form_data(self, url: str, data: FormData) -> dict:
Copy link
Owner

Choose a reason for hiding this comment

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

nit: please move methods like this down to the bottom for folks that happen to read the code as an end-user, as _gql_call() and others are.

"""
Retrieves the response from the server for a given URL and form data.
"""
async with ClientSession(headers=self._headers) as session:
resp = await session.post(url, data=data)
if resp.status != 200:
raise RequestFailedException(f"HTTP Code {resp.status}: {resp.reason}")

return await resp.json()

async def get_accounts(self) -> Dict[str, Any]:
"""
Gets the list of accounts configured in the Monarch Money account.
Expand Down Expand Up @@ -434,8 +447,8 @@ async def is_accounts_refresh_complete(self) -> bool:
async def request_accounts_refresh_and_wait(
self,
account_ids: Optional[List[str]] = None,
timeout: int = 300,
delay: int = 10,
timeout: int = TIMEOUT,
delay: int = DELAY,
) -> bool:
"""
Convenience method for forcing an accounts refresh on Monarch, as well
Expand Down Expand Up @@ -2394,13 +2407,20 @@ async def set_budget_amount(
)

async def upload_account_balance_history(
self, account_id: str, csv_content: str
) -> None:
self,
account_id: str,
csv_content: str,
timeout: int = TIMEOUT,
delay: int = DELAY,
) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

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

Response is a bool.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed. thanks for catching this!

"""
Uploads the account balance history csv for a given account.
Uploads the account balance history CSV for a specified account.

:param account_id: The account ID to apply the history to.
:param csv_content: CSV representation of the balance history.
Headers: Date, Amount, and Account Name.
:param timeout: The number of seconds to wait before timing out
:param delay: The number of seconds to wait for each check on whether parsing is completed
"""
if not account_id or not csv_content:
raise RequestFailedException("account_id and csv_content cannot be empty")
Expand All @@ -2410,13 +2430,94 @@ async def upload_account_balance_history(
form.add_field("files", csv_content, filename=filename, content_type="text/csv")
form.add_field("account_files_mapping", json.dumps({filename: account_id}))

async with ClientSession(headers=self._headers) as session:
resp = await session.post(
MonarchMoneyEndpoints.getAccountBalanceHistoryUploadEndpoint(),
data=form,
)
if resp.status != 200:
raise RequestFailedException(f"HTTP Code {resp.status}: {resp.reason}")
upload_response = await self._upload_form_data(
url=MonarchMoneyEndpoints.getAccountBalanceHistoryUploadEndpoint(),
data=form,
)

session_key = upload_response["session_key"]

parse_response = await self._initiate_upload_balance_history_session(
session_key=session_key
)

is_completed = (
parse_response["parseBalanceHistory"]["uploadBalanceHistorySession"][
"status"
]
== "completed"
)

start = time.time()
while not is_completed and (time.time() <= (start + timeout)):
await asyncio.sleep(delay)

is_completed = (
await self._is_upload_balance_history_complete(session_key)
)["uploadBalanceHistorySession"]["status"] == "completed"

return is_completed

async def _initiate_upload_balance_history_session(self, session_key: str) -> dict:
"""
Triggers parsing of the uploaded balance history CSV file.

:param session_key: The session key for the uploaded file.
"""

query = gql(
"""
mutation Web_ParseUploadBalanceHistorySession($input: ParseBalanceHistoryInput!) {
parseBalanceHistory(input: $input) {
uploadBalanceHistorySession {
...UploadBalanceHistorySessionFields
__typename
}
__typename
}
}
fragment UploadBalanceHistorySessionFields on UploadBalanceHistorySession {
sessionKey
status
__typename
}
"""
)

variables = {"input": {"sessionKey": session_key}}

return await self.gql_call(
"Web_ParseUploadBalanceHistorySession", query, variables
)

async def _is_upload_balance_history_complete(self, session_key: str):
"""
Retrieves the status of the upload balance history session.

:param session_key: The session key for the uploaded file.
"""

query = gql(
"""
query Web_GetUploadBalanceHistorySession($sessionKey: String!) {
uploadBalanceHistorySession(sessionKey: $sessionKey) {
...UploadBalanceHistorySessionFields
__typename
}
}
fragment UploadBalanceHistorySessionFields on UploadBalanceHistorySession {
sessionKey
status
__typename
}
"""
)

variables = {"sessionKey": session_key}

return await self.gql_call(
"Web_GetUploadBalanceHistorySession", query, variables
)

async def get_recurring_transactions(
self,
Expand Down