Improve upload account balance history#86
Improve upload account balance history#86CalvinChanCan wants to merge 39 commits intohammem:mainfrom
Conversation
hammem
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Please pull this branch off of your other commits in the other PR, so it doesn't accidentally introduce those changes simultaneously.
| variables=variables, | ||
| ) | ||
|
|
||
| async def delete_account( |
There was a problem hiding this comment.
same as above, please stack these commits atop origin/main, instead of the other branch you've created for #85
monarchmoney/monarchmoney.py
Outdated
|
|
||
| :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. |
There was a problem hiding this comment.
nit: move this description into the body of the docblock, instead of here, where it's challenging to fit.
monarchmoney/monarchmoney.py
Outdated
| response = await resp.json() | ||
| session_key = response["session_key"] | ||
| return session_key |
There was a problem hiding this comment.
return the entire JSON block, for consistency with the rest of the API.
There was a problem hiding this comment.
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.
monarchmoney/monarchmoney.py
Outdated
| 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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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!
monarchmoney/monarchmoney.py
Outdated
| async def upload_and_parse_balance_history( | ||
| self, | ||
| account_id: str, | ||
| csv_content, |
There was a problem hiding this comment.
To keep upload_account_balance_history, I've removed upload_and_parse_balance_history so this isn't necessary anymore.
monarchmoney/monarchmoney.py
Outdated
| session_key = response["session_key"] | ||
| return session_key | ||
|
|
||
| async def parse_upload_balance_history_session(self, session_key: str) -> dict: |
There was a problem hiding this comment.
nit
| async def parse_upload_balance_history_session(self, session_key: str) -> dict: | |
| async def initiate_upload_balance_history_session(self, session_key: str) -> dict: |
monarchmoney/monarchmoney.py
Outdated
| timeout: int = 300, | ||
| delay: int = 10, |
There was a problem hiding this comment.
define these as constants, so it can be consistent across the API
monarchmoney/monarchmoney.py
Outdated
| "Web_ParseUploadBalanceHistorySession", query, variables | ||
| ) | ||
|
|
||
| async def get_upload_balance_history_session(self, session_key: str): |
There was a problem hiding this comment.
for consistency with similar methods in this API
| async def get_upload_balance_history_session(self, session_key: str): | |
| async def is_upload_balance_history_complete(self, session_key: str) -> bool: |
monarchmoney/monarchmoney.py
Outdated
|
|
||
| is_completed = (await self.get_upload_balance_history_session(session_key))[ | ||
| "uploadBalanceHistorySession" | ||
| ]["status"] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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']
There was a problem hiding this comment.
@CalvinChanCan , after looking through the flow, I think you've picked the better option. the other can introduce more complicated handling for end-users.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Thanks @andrecloutier. That's a fair point. I did a bunch of refactoring to keep the existing method upload_account_balance_history.
…o feat-upload-balance-history
…o feat-upload-balance-history # Conflicts: # monarchmoney/monarchmoney.py
…ad_balance_history_session
…e_history_complete
andrecloutier
left a comment
There was a problem hiding this comment.
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.
monarchmoney/monarchmoney.py
Outdated
| csv_content: str, | ||
| timeout: int = TIMEOUT, | ||
| delay: int = DELAY, | ||
| ) -> str: |
There was a problem hiding this comment.
fixed. thanks for catching this!
|
Hi @hammem, just wondering if I could get another review. Happy to make any further changes/feedback. |
|
I pulled down this code to try and use it. can you please give example input? How do I use BalanceHistoryRow? |
|
Hi @CFarzaneh , You can import the class Example: |
|
@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. |
| """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: |
There was a problem hiding this comment.
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.
monarchmoney/monarchmoney.py
Outdated
| 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. |
There was a problem hiding this comment.
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!
monarchmoney/monarchmoney.py
Outdated
|
|
||
| is_completed = (await self.get_upload_balance_history_session(session_key))[ | ||
| "uploadBalanceHistorySession" | ||
| ]["status"] |
There was a problem hiding this comment.
@CalvinChanCan , after looking through the flow, I think you've picked the better option. the other can introduce more complicated handling for end-users.
|
@CalvinChanCan , lmk if you'd like this included in the next release to PyPI! |
This PR improves on the upload account balance history.
The method
upload_account_balance_historyonly uploads the CSV file but does not update the balance in Monarch Money.A new method
parse_upload_balance_history_sessionis added to parse the csv file which would cause an update to the account balance history.Another new method
get_upload_balance_history_sessionis added to check the status of whether the parsing is still processing or completed.Finally
upload_and_parse_balance_historyuses these 3 methods together to upload and parse the account balance history so that it shows up in Monarch Money.