Skip to content

Conversation

@CalvinChanCan
Copy link
Contributor

This PR improves on the upload account balance history.

The method upload_account_balance_history only uploads the CSV file but does not update the balance in Monarch Money.

A new method parse_upload_balance_history_session is added to parse the csv file which would cause an update to the account balance history.

Another new method get_upload_balance_history_session is added to check the status of whether the parsing is still processing or completed.

Finally upload_and_parse_balance_history uses these 3 methods together to upload and parse the account balance history so that it shows up in Monarch Money.

Copy link
Owner

@hammem hammem left a comment

Choose a reason for hiding this comment

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

Thanks, @CalvinChanCan ! What's the difference between using this and uploading transactions to an account? I don't understand how this will appear different to a person in the UX once it's done.

I have a bunch of inline comments about the naming of methods, ensuring it's consistent with patterns already set in the API, and ensuring you don't accidentally include your changes from #85

- `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
- `upload_account_balance_history` - uploads account history csv file for a given 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!

variables=variables,
)

async def delete_account(
Copy link
Owner

Choose a reason for hiding this comment

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

same as above, please stack these commits atop origin/main, instead of the other branch you've created for #85

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!


:param account_id: The account ID to apply the history to.
:param csv_content: CSV representation of the balance history.
:param csv_content: CSV representation of the balance history. Headers: Date, Amount, and Account Name.
Copy link
Owner

Choose a reason for hiding this comment

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

nit: move this description into the body of the docblock, instead of here, where it's challenging to fit.

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

Comment on lines 2460 to 2462
response = await resp.json()
session_key = response["session_key"]
return session_key
Copy link
Owner

Choose a reason for hiding this comment

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

return the entire JSON block, for consistency with the rest of the API.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I refactor this quite a bit since the initial review so I've added _upload_form_data as a way to generalize the uploading form data (I'm hoping to use this later to bulk add transactions from a csv). Anyway, this should now return the whole response.

Uploads and parses the balance history, updating the account balance on monarch money

:param account_id: The account ID to apply the history to.
:param csv_content: CSV representation of the balance history. Headers are Date, Amount, and Account Name.
Copy link
Owner

Choose a reason for hiding this comment

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

This should probably be something more structured than a str? If there are specific columns and column types/formats needed, perhaps using a List[Dict[str, Any]]? Alternatively, use either Python's built-in CSV parsing library or, maybe, Pandas.

nit: move the description of the CSV format required to the body of the docblock

Copy link
Contributor

Choose a reason for hiding this comment

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

If we're going this far to normalize the input, I'd vote for making the list value a class (or DataClass/NamedTuple). This way the caller doesn't need to worry about what the magic key names are.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree that a dataclass would help the caller with the proper input. I've refactor it to use a dataclass. A second review over this would be appreciated!

Copy link
Contributor

Choose a reason for hiding this comment

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

Personally, I'm fine with providing the CSV as a string. This gives the user the most flexibility to use the tools of their choice to build the CSV. If we require a normalized input, then they'll have to use a CSV parser to parse the input, only for it to generate a CSV once again.

Optionally, we could provide users with a CSV builder kind of class if we want to help them along. IMO this could be an improvement made outside of this PR. That's @hammem 's call :)

Copy link
Owner

Choose a reason for hiding this comment

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

Thanks for the good discussion on this one! Yeah, it's not ideal no matter which route you take. If only we knew why @monarchmoney decided to not use a GraphQL endpoint for uploading this data...

The option to go with a dataclass is great and probably something to do elsewhere.

My main concern with a raw CSV string is it makes debugging a huge pain for end users, as we can't guide or hint where things might be off.

But, I've been holding this up for a while, no need to keep holding it up for that.

Appreciate everyone's patience!

async def upload_and_parse_balance_history(
self,
account_id: str,
csv_content,
Copy link
Owner

Choose a reason for hiding this comment

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

needs a typehint

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To keep upload_account_balance_history, I've removed upload_and_parse_balance_history so this isn't necessary anymore.

session_key = response["session_key"]
return session_key

async def parse_upload_balance_history_session(self, session_key: str) -> 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

Suggested change
async def parse_upload_balance_history_session(self, session_key: str) -> dict:
async def initiate_upload_balance_history_session(self, session_key: str) -> dict:

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!

Comment on lines 2399 to 2400
timeout: int = 300,
delay: int = 10,
Copy link
Owner

Choose a reason for hiding this comment

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

define these as constants, so it can be consistent across the API

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!

"Web_ParseUploadBalanceHistorySession", query, variables
)

async def get_upload_balance_history_session(self, session_key: str):
Copy link
Owner

Choose a reason for hiding this comment

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

for consistency with similar methods in this API

Suggested change
async def get_upload_balance_history_session(self, session_key: str):
async def is_upload_balance_history_complete(self, session_key: str) -> 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.

done!


is_completed = (await self.get_upload_balance_history_session(session_key))[
"uploadBalanceHistorySession"
]["status"]
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe this is missing a == "completed" check.

Though, I believe a better approach would be to check whether the processing is still "pending" so that this code short circuits if the upload fails.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it is missing the check! Thanks for catching that!

The possible statuses that I've found so far are: "created", "started", and "completed". After calling _initiate_upload_balance_history_session, it usually returns the "created" status. Then _is_upload_balance_history_complete can either return the "started" or "completed" statuses.

If _is_upload_balance_history_complete can return "created", it may lead to a premature completed check. Though I don't think that's likely.

@hammem , thoughts on whether we should check for "started" vs "completed"? I could see the better case being checking for "started" but worried about possible side effects that may cause.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah I was expecting a failed state if an invalid csv is provided. In that case, looks like the following error is thrown. Which I think is sufficient. Please disregard the suggestion!

gql.transport.exceptions.TransportQueryError: {'message': "Something went wrong while processing: ['parseBalanceHistory']

Copy link
Owner

Choose a reason for hiding this comment

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

@CalvinChanCan , after looking through the flow, I think you've picked the better option. the other can introduce more complicated handling for end-users.

@andrecloutier
Copy link
Contributor

What's the difference between using this and uploading transactions to an account?

This API allows you to set the balance history on an account irrespective of the transactions. For my use case, I use this API to sync nightly the value of my brokerage accounts with Monarch. There's no transactions to report, the value just fluctuates every day.

@CalvinChanCan - Thanks for putting this PR together! I've been meaning to contribute back some local hacks I had to account for the changes made to the API... but this PR is much more complete!

README.md Outdated
- `create_manual_account` - creates a new manual account
- `upload_account_balance_history` - uploads account history csv file for a given account
- `delete_account` - deletes an account by the provided account id
- `upload_and_parse_balance_history` - uploads and parses account history csv file for a given account
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd vote for keeping the existing method rather than make a breaking change. The fact that the API now requires us to poll for the result is an implementation detail.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @andrecloutier. That's a fair point. I did a bunch of refactoring to keep the existing method upload_account_balance_history.

Copy link
Contributor

@andrecloutier andrecloutier left a comment

Choose a reason for hiding this comment

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

Looks good to me! I took these changes for a test drive locally and was able to use it to update some balances in my account.

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!

@CalvinChanCan
Copy link
Contributor Author

Hi @hammem, just wondering if I could get another review. Happy to make any further changes/feedback.

@CFarzaneh
Copy link

I pulled down this code to try and use it. can you please give example input? How do I use BalanceHistoryRow?

@CalvinChanCan
Copy link
Contributor Author

CalvinChanCan commented Mar 23, 2024

Hi @CFarzaneh ,

You can import the class BalanceHistoryRow. With this class, you only need to provide the date (as a datetime object), an amount as a float, and optionally, can you include the account_name.

Example:

rows_list = []
row = BalanceHistoryRow(
    date=datetime.datetime(2020, 1, 1),
    amount=17.00,
    account_name='Chase Bank Account',
)
rows_list.append(row)

mm.upload_account_balance_history(account_id='1700000000000', csv_content=rows_list)

@CalvinChanCan CalvinChanCan requested a review from hammem March 26, 2024 03:10
@hammem
Copy link
Owner

hammem commented Apr 14, 2024

@CalvinChanCan , apologies for the delay on this. Just a couple of minor things and this is ready to go. Once you are ready, I'll merge and do a release with this as well.

@hammem hammem linked an issue Apr 14, 2024 that may be closed by this pull request
"""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.

Uploads and parses the balance history, updating the account balance on monarch money

:param account_id: The account ID to apply the history to.
:param csv_content: CSV representation of the balance history. Headers are Date, Amount, and Account Name.
Copy link
Owner

Choose a reason for hiding this comment

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

Thanks for the good discussion on this one! Yeah, it's not ideal no matter which route you take. If only we knew why @monarchmoney decided to not use a GraphQL endpoint for uploading this data...

The option to go with a dataclass is great and probably something to do elsewhere.

My main concern with a raw CSV string is it makes debugging a huge pain for end users, as we can't guide or hint where things might be off.

But, I've been holding this up for a while, no need to keep holding it up for that.

Appreciate everyone's patience!


is_completed = (await self.get_upload_balance_history_session(session_key))[
"uploadBalanceHistorySession"
]["status"]
Copy link
Owner

Choose a reason for hiding this comment

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

@CalvinChanCan , after looking through the flow, I think you've picked the better option. the other can introduce more complicated handling for end-users.

@hammem
Copy link
Owner

hammem commented Apr 20, 2024

@CalvinChanCan , lmk if you'd like this included in the next release to PyPI!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

upload_account_balance_history endpoint is not working as expected

6 participants