diff --git a/Makefile b/Makefile index 52220a6..ed7ca42 100644 --- a/Makefile +++ b/Makefile @@ -13,4 +13,7 @@ uninstall: pip uninstall monarchmoney clean: - rm -fR build dist monarchmoney.egg-info monarchmoney/__pycache__ *.json \ No newline at end of file + rm -fR build dist monarchmoney.egg-info monarchmoney/__pycache__ *.json + + +models: diff --git a/monarchmoney/const.py b/monarchmoney/const.py new file mode 100644 index 0000000..a08b871 --- /dev/null +++ b/monarchmoney/const.py @@ -0,0 +1,1104 @@ +QUERY_GET_ACCOUNTS = """ + query GetAccounts { + accounts { + ...AccountFields + __typename + } + householdPreferences { + id + accountGroupOrder + __typename + } + } + + fragment AccountFields on Account { + id + displayName + syncDisabled + deactivatedAt + isHidden + isAsset + mask + createdAt + updatedAt + displayLastUpdatedAt + currentBalance + displayBalance + includeInNetWorth + hideFromList + hideTransactionsFromReports + includeBalanceInNetWorth + includeInGoalBalance + dataProvider + dataProviderAccountId + isManual + transactionsCount + holdingsCount + manualInvestmentsTrackingMethod + order + logoUrl + type { + name + display + __typename + } + subtype { + name + display + __typename + } + credential { + id + updateRequired + disconnectedFromDataProviderAt + dataProvider + institution { + id + plaidInstitutionId + name + status + __typename + } + __typename + } + institution { + id + name + primaryColor + url + __typename + } + __typename + } +""" + +QUERY_GET_ACCOUNT_HISTORY = """ + query AccountDetails_getAccount($id: UUID!, $filters: TransactionFilterInput) { + account(id: $id) { + id + ...AccountFields + ...EditAccountFormFields + isLiability + credential { + id + hasSyncInProgress + canBeForceRefreshed + disconnectedFromDataProviderAt + dataProvider + institution { + id + plaidInstitutionId + url + ...InstitutionStatusFields + __typename + } + __typename + } + institution { + id + plaidInstitutionId + url + ...InstitutionStatusFields + __typename + } + __typename + } + transactions: allTransactions(filters: $filters) { + totalCount + results(limit: 20) { + id + ...TransactionsListFields + __typename + } + __typename + } + snapshots: snapshotsForAccount(accountId: $id) { + date + signedBalance + __typename + } + } + + fragment AccountFields on Account { + id + displayName + syncDisabled + deactivatedAt + isHidden + isAsset + mask + createdAt + updatedAt + displayLastUpdatedAt + currentBalance + displayBalance + includeInNetWorth + hideFromList + hideTransactionsFromReports + includeBalanceInNetWorth + includeInGoalBalance + dataProvider + dataProviderAccountId + isManual + transactionsCount + holdingsCount + manualInvestmentsTrackingMethod + order + logoUrl + type { + name + display + group + __typename + } + subtype { + name + display + __typename + } + credential { + id + updateRequired + disconnectedFromDataProviderAt + dataProvider + institution { + id + plaidInstitutionId + name + status + __typename + } + __typename + } + institution { + id + name + primaryColor + url + __typename + } + __typename + } + + fragment EditAccountFormFields on Account { + id + displayName + deactivatedAt + displayBalance + includeInNetWorth + hideFromList + hideTransactionsFromReports + dataProvider + dataProviderAccountId + isManual + manualInvestmentsTrackingMethod + isAsset + invertSyncedBalance + canInvertBalance + type { + name + display + __typename + } + subtype { + name + display + __typename + } + __typename + } + + fragment InstitutionStatusFields on Institution { + id + hasIssuesReported + hasIssuesReportedMessage + plaidStatus + status + balanceStatus + transactionsStatus + __typename + } + + fragment TransactionsListFields on Transaction { + id + ...TransactionOverviewFields + __typename + } + + fragment TransactionOverviewFields on Transaction { + id + amount + pending + date + hideFromReports + plaidName + notes + isRecurring + reviewStatus + needsReview + dataProviderDescription + attachments { + id + __typename + } + isSplitTransaction + category { + id + name + group { + id + type + __typename + } + __typename + } + merchant { + name + id + transactionsCount + __typename + } + tags { + id + name + color + order + __typename + } + __typename + } + """ + +QUERY_GET_ACCOUNT_HOLDINGS = """ + query Web_GetHoldings($input: PortfolioInput) { + portfolio(input: $input) { + aggregateHoldings { + edges { + node { + id + quantity + basis + totalValue + securityPriceChangeDollars + securityPriceChangePercent + lastSyncedAt + holdings { + id + type + typeDisplay + name + ticker + closingPrice + isManual + closingPriceUpdatedAt + __typename + } + security { + id + name + type + ticker + typeDisplay + currentPrice + currentPriceUpdatedAt + closingPrice + closingPriceUpdatedAt + oneDayChangePercent + oneDayChangeDollars + __typename + } + __typename + } + __typename + } + __typename + } + __typename + } + } + """ + +QUERY_GET_ACCOUNT_TYPE_OPTIONS = """ + query GetAccountTypeOptions { + accountTypeOptions { + type { + name + display + group + possibleSubtypes { + display + name + __typename + } + __typename + } + subtype { + name + display + __typename + } + __typename + } + } + """ + + +QUERY_GET_ACCOUNT_SNAPSHOTS_BY_TYPE = """ + query GetSnapshotsByAccountType($startDate: Date!, $timeframe: Timeframe!) { + snapshotsByAccountType(startDate: $startDate, timeframe: $timeframe) { + accountType + month + balance + __typename + } + accountTypes { + name + group + __typename + } + } + """ + +QUERY_GET_ACCOUNT_RECENT_BALANCES = """ + query GetAccountRecentBalances($startDate: Date!) { + accounts { + id + recentBalances(startDate: $startDate) + __typename + } + } + """ + +QUERY_GET_AGGREGATE_SNAPSHOTS = """ + query GetAggregateSnapshots($filters: AggregateSnapshotFilters) { + aggregateSnapshots(filters: $filters) { + date + balance + __typename + } + } + """ + + + + +QUERY_GET_SUBSCRIPTION_DETAILS = """ +query GetSubscriptionDetails { + subscription { + id + paymentSource + referralCode + isOnFreeTrial + hasPremiumEntitlement + __typename + } + } +""" + +QUERY_GET_TRANSACTIONS_SUMMARY = """ + query GetTransactionsPage($filters: TransactionFilterInput) { + aggregates(filters: $filters) { + summary { + ...TransactionsSummaryFields + __typename + } + __typename + } + } + + fragment TransactionsSummaryFields on TransactionsSummary { + avg + count + max + maxExpense + sum + sumIncome + sumExpense + first + last + __typename + } + """ + + +QUERY_GET_TRANSACTIONS_LIST = """ +query GetTransactionsList($offset: Int, $limit: Int, $filters: TransactionFilterInput, $orderBy: TransactionOrdering) { +allTransactions(filters: $filters) { + totalCount + results(offset: $offset, limit: $limit, orderBy: $orderBy) { + id + ...TransactionOverviewFields + __typename + } + __typename +} +transactionRules { + id + __typename +} +} + +fragment TransactionOverviewFields on Transaction { +id +amount +pending +date +hideFromReports +plaidName +notes +isRecurring +reviewStatus +needsReview +attachments { + id + extension + filename + originalAssetUrl + publicId + sizeBytes + __typename +} +isSplitTransaction +createdAt +updatedAt +category { + id + name + __typename +} +merchant { + name + id + transactionsCount + __typename +} +account { + id + displayName + __typename +} +tags { + id + name + color + order + __typename +} +__typename +} +""" + + +QUERY_GET_TRANSACTION_CATEGORIES = """ + query GetCategories { + categories { + ...CategoryFields + __typename + } + } + + fragment CategoryFields on Category { + id + order + name + systemCategory + isSystemCategory + isDisabled + updatedAt + createdAt + group { + id + name + type + __typename + } + __typename + } + """ + + +QUERY_GET_TRANSACTION_CATEGORY_GROUPS = """ + query ManageGetCategoryGroups { + categoryGroups { + id + name + order + type + updatedAt + createdAt + __typename + } + } + """ + +QUERY_GET_TRANSACTION_TAGS = """ + query GetHouseholdTransactionTags($search: String, $limit: Int, $bulkParams: BulkTransactionDataParams) { + householdTransactionTags( + search: $search + limit: $limit + bulkParams: $bulkParams + ) { + id + name + color + order + transactionCount + __typename + } + } + """ + +QUERY_GET_TRANSACTION_DETAILS = """ + query GetTransactionDrawer($id: UUID!, $redirectPosted: Boolean) { + getTransaction(id: $id, redirectPosted: $redirectPosted) { + id + amount + pending + isRecurring + date + originalDate + hideFromReports + needsReview + reviewedAt + reviewedByUser { + id + name + __typename + } + plaidName + notes + hasSplitTransactions + isSplitTransaction + isManual + splitTransactions { + id + ...TransactionDrawerSplitMessageFields + __typename + } + originalTransaction { + id + ...OriginalTransactionFields + __typename + } + attachments { + id + publicId + extension + sizeBytes + filename + originalAssetUrl + __typename + } + account { + id + ...TransactionDrawerAccountSectionFields + __typename + } + category { + id + __typename + } + goal { + id + __typename + } + merchant { + id + name + transactionCount + logoUrl + recurringTransactionStream { + id + __typename + } + __typename + } + tags { + id + name + color + order + __typename + } + needsReviewByUser { + id + __typename + } + __typename + } + myHousehold { + users { + id + name + __typename + } + __typename + } + } + + fragment TransactionDrawerSplitMessageFields on Transaction { + id + amount + merchant { + id + name + __typename + } + category { + id + name + __typename + } + __typename + } + + fragment OriginalTransactionFields on Transaction { + id + date + amount + merchant { + id + name + __typename + } + __typename + } + + fragment TransactionDrawerAccountSectionFields on Account { + id + displayName + logoUrl + id + mask + subtype { + display + __typename + } + __typename + } + """ + +QUERY_GET_TRANSACTION_SPLITS = """ + query TransactionSplitQuery($id: UUID!) { + getTransaction(id: $id) { + id + amount + category { + id + name + __typename + } + merchant { + id + name + __typename + } + splitTransactions { + id + merchant { + id + name + __typename + } + category { + id + name + __typename + } + amount + notes + __typename + } + __typename + } + } + """ + +QUERY_GET_CASHFLOW = """ + query Web_GetCashFlowPage($filters: TransactionFilterInput) { + byCategory: aggregates(filters: $filters, groupBy: ["category"]) { + groupBy { + category { + id + name + group { + id + type + __typename + } + __typename + } + __typename + } + summary { + sum + __typename + } + __typename + } + byCategoryGroup: aggregates(filters: $filters, groupBy: ["categoryGroup"]) { + groupBy { + categoryGroup { + id + name + type + __typename + } + __typename + } + summary { + sum + __typename + } + __typename + } + byMerchant: aggregates(filters: $filters, groupBy: ["merchant"]) { + groupBy { + merchant { + id + name + logoUrl + __typename + } + __typename + } + summary { + sumIncome + sumExpense + __typename + } + __typename + } + summary: aggregates(filters: $filters, fillEmptyValues: true) { + summary { + sumIncome + sumExpense + savings + savingsRate + __typename + } + __typename + } + } + """ + +QUERY_GET_CASHFLOW_SUMMARY = """ + query Web_GetCashFlowPage($filters: TransactionFilterInput) { + summary: aggregates(filters: $filters, fillEmptyValues: true) { + summary { + sumIncome + sumExpense + savings + savingsRate + __typename + } + __typename + } + } + """ + + +QUERY_GET_RECURRING_TRANSACTIONS = """ + query Web_GetUpcomingRecurringTransactionItems($startDate: Date!, $endDate: Date!, $filters: RecurringTransactionFilter) { + recurringTransactionItems( + startDate: $startDate + endDate: $endDate + filters: $filters + ) { + stream { + id + frequency + amount + isApproximate + merchant { + id + name + logoUrl + __typename + } + __typename + } + date + isPast + transactionId + amount + amountDiff + category { + id + name + __typename + } + account { + id + displayName + logoUrl + __typename + } + __typename + } + } + """ + + + +QUERY_GET_INSTITUTIONS = """ + query Web_GetInstitutionSettings { + credentials { + id + ...CredentialSettingsCardFields + __typename + } + accounts(filters: {includeDeleted: true}) { + id + displayName + subtype { + display + __typename + } + mask + credential { + id + __typename + } + deletedAt + __typename + } + subscription { + isOnFreeTrial + hasPremiumEntitlement + __typename + } + } + + fragment CredentialSettingsCardFields on Credential { + id + updateRequired + disconnectedFromDataProviderAt + ...InstitutionInfoFields + institution { + id + name + url + __typename + } + __typename + } + + fragment InstitutionInfoFields on Credential { + id + displayLastUpdatedAt + dataProvider + updateRequired + disconnectedFromDataProviderAt + ...InstitutionLogoWithStatusFields + institution { + id + name + hasIssuesReported + hasIssuesReportedMessage + __typename + } + __typename + } + + fragment InstitutionLogoWithStatusFields on Credential { + dataProvider + updateRequired + institution { + hasIssuesReported + status + balanceStatus + transactionsStatus + __typename + } + __typename + } + """ + +QUERY_GET_BUDGETS = """ + 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 + order + budgetVariability + rolloverPeriod { + id + startMonth + endMonth + __typename + } + __typename + } + type + __typename + } + goals @include(if: $useLegacyGoals) { + id + name + 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 + } + """ + +def main(): + print(QUERY_GET_ACCOUNTS) + # print("QUERY_GET_ACCOUNT_TYPE_OPTIONS:", QUERY_GET_ACCOUNT_TYPE_OPTIONS) + # print("QUERY_GET_ACCOUNT_SNAPSHOTS_BY_TYPE:", QUERY_GET_ACCOUNT_SNAPSHOTS_BY_TYPE) + # print("QUERY_GET_ACCOUNT_RECENT_BALANCES:", QUERY_GET_ACCOUNT_RECENT_BALANCES) + # print("QUERY_GET_AGGREGATE_SNAPSHOTS:", QUERY_GET_AGGREGATE_SNAPSHOTS) + # print("QUERY_GET_SUBSCRIPTION_DETAILS:", QUERY_GET_SUBSCRIPTION_DETAILS) + # print("QUERY_GET_TRANSACTIONS_SUMMARY:", QUERY_GET_TRANSACTIONS_SUMMARY) + # print("QUERY_GET_TRANSACTIONS_LIST:", QUERY_GET_TRANSACTIONS_LIST) + # print("QUERY_GET_TRANSACTION_CATEGORIES:", QUERY_GET_TRANSACTION_CATEGORIES) + # print("QUERY_GET_TRANSACTION_CATEGORY_GROUPS:", QUERY_GET_TRANSACTION_CATEGORY_GROUPS) + # print("QUERY_GET_TRANSACTION_TAGS:", QUERY_GET_TRANSACTION_TAGS) + # print("QUERY_GET_TRANSACTION_DETAILS:", QUERY_GET_TRANSACTION_DETAILS) + # print("QUERY_GET_TRANSACTION_SPLITS:", QUERY_GET_TRANSACTION_SPLITS) + # print("QUERY_GET_CASHFLOW:", QUERY_GET_CASHFLOW) + # print("QUERY_GET_CASHFLOW_SUMMARY:", QUERY_GET_CASHFLOW_SUMMARY) + # print("QUERY_GET_RECURRING_TRANSACTIONS:", QUERY_GET_RECURRING_TRANSACTIONS) + +if __name__ == "__main__": + main() + diff --git a/monarchmoney/create_update.py b/monarchmoney/create_update.py new file mode 100644 index 0000000..e69de29 diff --git a/monarchmoney/get.py b/monarchmoney/get.py new file mode 100644 index 0000000..e69de29 diff --git a/monarchmoney/monarchmoney.py b/monarchmoney/monarchmoney.py index d5ee5d1..6e95dd6 100644 --- a/monarchmoney/monarchmoney.py +++ b/monarchmoney/monarchmoney.py @@ -15,6 +15,39 @@ from gql.transport.aiohttp import AIOHTTPTransport from graphql import DocumentNode +from monarchmoney.const import ( + QUERY_GET_ACCOUNTS, + QUERY_GET_ACCOUNT_TYPE_OPTIONS, + QUERY_GET_ACCOUNT_RECENT_BALANCES, + QUERY_GET_ACCOUNT_SNAPSHOTS_BY_TYPE, + QUERY_GET_AGGREGATE_SNAPSHOTS, + QUERY_GET_SUBSCRIPTION_DETAILS, + QUERY_GET_TRANSACTIONS_SUMMARY, + QUERY_GET_TRANSACTIONS_LIST, + QUERY_GET_TRANSACTION_CATEGORIES, + QUERY_GET_TRANSACTION_CATEGORY_GROUPS, + QUERY_GET_TRANSACTION_TAGS, + QUERY_GET_TRANSACTION_DETAILS, + QUERY_GET_TRANSACTION_SPLITS, + QUERY_GET_CASHFLOW, + QUERY_GET_CASHFLOW_SUMMARY, + QUERY_GET_RECURRING_TRANSACTIONS, QUERY_GET_ACCOUNT_HOLDINGS, QUERY_GET_ACCOUNT_HISTORY, QUERY_GET_INSTITUTIONS, + QUERY_GET_BUDGETS, +) + + + +from .models import ( + GetAccountsResponse, GetAccountTypeOptionsResponse, GetAccountSnapshotsResponse, + GetAggregateSnapshotsResponse, GetSubscriptionDetailsResponse, + GetTransactionsSummaryResponse, GetTransactionsResponse, + GetTransactionCategoryGroupsResponse, GetTransactionTagsResponse, + GetTransactionDetailsResponse, GetCashFlowResponse, GetRecurringTransactionsResponse, + GetRecentAccountBalancesResponse, GetAccountSnapshotsByTypeResponse, GetAccountHoldingsResponse, + GetAccountHistoryResponse, GetInstitutionsResponse, GetBudgetsResponse, GetTransactionCategoriesResponse, + GetTransactionSplitsResponse +) + AUTH_HEADER_KEY = "authorization" CSRF_KEY = "csrftoken" DEFAULT_RECORD_LIMIT = 100 @@ -127,127 +160,32 @@ 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 get_accounts(self) -> Dict[str, Any]: + async def get_accounts(self) -> GetAccountsResponse: """ Gets the list of accounts configured in the Monarch Money account. """ - query = gql( - """ - query GetAccounts { - accounts { - ...AccountFields - __typename - } - householdPreferences { - id - accountGroupOrder - __typename - } - } + query = gql(QUERY_GET_ACCOUNTS) - fragment AccountFields on Account { - id - displayName - syncDisabled - deactivatedAt - isHidden - isAsset - mask - createdAt - updatedAt - displayLastUpdatedAt - currentBalance - displayBalance - includeInNetWorth - hideFromList - hideTransactionsFromReports - includeBalanceInNetWorth - includeInGoalBalance - dataProvider - dataProviderAccountId - isManual - transactionsCount - holdingsCount - manualInvestmentsTrackingMethod - order - logoUrl - type { - name - display - __typename - } - subtype { - name - display - __typename - } - credential { - id - updateRequired - disconnectedFromDataProviderAt - dataProvider - institution { - id - plaidInstitutionId - name - status - __typename - } - __typename - } - institution { - id - name - primaryColor - url - __typename - } - __typename - } - """ - ) - return await self.gql_call( + result = await self.gql_call( operation="GetAccounts", graphql_query=query, ) + return GetAccountsResponse(**result) - async def get_account_type_options(self) -> Dict[str, Any]: + async def get_account_type_options(self) -> GetAccountTypeOptionsResponse: """ Retrieves a list of available account types and their subtypes. """ - query = gql( - """ - query GetAccountTypeOptions { - accountTypeOptions { - type { - name - display - group - possibleSubtypes { - display - name - __typename - } - __typename - } - subtype { - name - display - __typename - } - __typename - } - } - """ - ) - return await self.gql_call( + query = gql(QUERY_GET_ACCOUNT_TYPE_OPTIONS) + result = await self.gql_call( operation="GetAccountTypeOptions", graphql_query=query, ) + return GetAccountTypeOptionsResponse(**result) async def get_recent_account_balances( self, start_date: Optional[str] = None - ) -> Dict[str, Any]: + ) -> GetRecentAccountBalancesResponse: """ Retrieves the daily balance for all accounts starting from `start_date`. `start_date` is an ISO formatted datestring, e.g. YYYY-MM-DD. @@ -256,24 +194,16 @@ async def get_recent_account_balances( if start_date is None: start_date = (date.today() - timedelta(days=31)).isoformat() - query = gql( - """ - query GetAccountRecentBalances($startDate: Date!) { - accounts { - id - recentBalances(startDate: $startDate) - __typename - } - } - """ - ) - return await self.gql_call( + query = gql(QUERY_GET_ACCOUNT_RECENT_BALANCES) + result = await self.gql_call( operation="GetAccountRecentBalances", graphql_query=query, variables={"startDate": start_date}, ) + return GetRecentAccountBalancesResponse(**result) - async def get_account_snapshots_by_type(self, start_date: str, timeframe: str): + + async def get_account_snapshots_by_type(self, start_date: str, timeframe: str) -> GetAccountSnapshotsByTypeResponse: """ Retrieves snapshots of the net values of all accounts of a given type, with either a yearly monthly granularity. @@ -287,51 +217,29 @@ async def get_account_snapshots_by_type(self, start_date: str, timeframe: str): if timeframe not in ("year", "month"): raise Exception(f'Unknown timeframe "{timeframe}"') - query = gql( - """ - query GetSnapshotsByAccountType($startDate: Date!, $timeframe: Timeframe!) { - snapshotsByAccountType(startDate: $startDate, timeframe: $timeframe) { - accountType - month - balance - __typename - } - accountTypes { - name - group - __typename - } - } - """ - ) - return await self.gql_call( + query = gql(QUERY_GET_ACCOUNT_SNAPSHOTS_BY_TYPE) + result = await self.gql_call( operation="GetSnapshotsByAccountType", graphql_query=query, variables={"startDate": start_date, "timeframe": timeframe}, ) + return GetAccountSnapshotsByTypeResponse(**result) + + + async def get_aggregate_snapshots( self, start_date: Optional[date] = None, end_date: Optional[date] = None, account_type: Optional[str] = None, - ) -> dict: + ) -> GetAggregateSnapshotsResponse: """ Retrieves the daily net value of all accounts, optionally between `start_date` and `end_date`, and optionally only for accounts of type `account_type`. Both `start_date` and `end_date` are ISO datestrings, formatted as YYYY-MM-DD """ - query = gql( - """ - query GetAggregateSnapshots($filters: AggregateSnapshotFilters) { - aggregateSnapshots(filters: $filters) { - date - balance - __typename - } - } - """ - ) + query = gql(QUERY_GET_AGGREGATE_SNAPSHOTS) if start_date is None: # The mobile app defaults to 150 years ago today @@ -341,7 +249,7 @@ async def get_aggregate_snapshots( year=today.year - 150, month=today.month, day=1 ).isoformat() - return await self.gql_call( + result = await self.gql_call( operation="GetAggregateSnapshots", graphql_query=query, variables={ @@ -352,6 +260,7 @@ async def get_aggregate_snapshots( } }, ) + return GetAggregateSnapshotsResponse(**result) async def create_manual_account( self, @@ -722,59 +631,12 @@ async def request_accounts_refresh_and_wait( refreshed = await self.is_accounts_refresh_complete(account_ids) return refreshed - async def get_account_holdings(self, account_id: int) -> Dict[str, Any]: + async def get_account_holdings(self, account_id: int) -> GetAccountHoldingsResponse: """ Get the holdings information for a brokerage or similar type of account. """ - query = gql( - """ - query Web_GetHoldings($input: PortfolioInput) { - portfolio(input: $input) { - aggregateHoldings { - edges { - node { - id - quantity - basis - totalValue - securityPriceChangeDollars - securityPriceChangePercent - lastSyncedAt - holdings { - id - type - typeDisplay - name - ticker - closingPrice - isManual - closingPriceUpdatedAt - __typename - } - security { - id - name - type - ticker - typeDisplay - currentPrice - currentPriceUpdatedAt - closingPrice - closingPriceUpdatedAt - oneDayChangePercent - oneDayChangeDollars - __typename - } - __typename - } - __typename - } - __typename - } - __typename - } - } - """ + query = gql(QUERY_GET_ACCOUNT_HOLDINGS + ) variables = { @@ -786,13 +648,14 @@ async def get_account_holdings(self, account_id: int) -> Dict[str, Any]: }, } - return await self.gql_call( + result = await self.gql_call( operation="Web_GetHoldings", graphql_query=query, variables=variables, ) + return GetAccountHoldingsResponse(**result) - async def get_account_history(self, account_id: int) -> Dict[str, Any]: + async def get_account_history(self, account_id: int) -> GetAccountHistoryResponse: """ Gets historical account snapshot data for the requested account @@ -803,203 +666,8 @@ async def get_account_history(self, account_id: int) -> Dict[str, Any]: json object with all historical snapshots of requested account's balances """ - query = gql( - """ - query AccountDetails_getAccount($id: UUID!, $filters: TransactionFilterInput) { - account(id: $id) { - id - ...AccountFields - ...EditAccountFormFields - isLiability - credential { - id - hasSyncInProgress - canBeForceRefreshed - disconnectedFromDataProviderAt - dataProvider - institution { - id - plaidInstitutionId - url - ...InstitutionStatusFields - __typename - } - __typename - } - institution { - id - plaidInstitutionId - url - ...InstitutionStatusFields - __typename - } - __typename - } - transactions: allTransactions(filters: $filters) { - totalCount - results(limit: 20) { - id - ...TransactionsListFields - __typename - } - __typename - } - snapshots: snapshotsForAccount(accountId: $id) { - date - signedBalance - __typename - } - } + query = gql(QUERY_GET_ACCOUNT_HISTORY - fragment AccountFields on Account { - id - displayName - syncDisabled - deactivatedAt - isHidden - isAsset - mask - createdAt - updatedAt - displayLastUpdatedAt - currentBalance - displayBalance - includeInNetWorth - hideFromList - hideTransactionsFromReports - includeBalanceInNetWorth - includeInGoalBalance - dataProvider - dataProviderAccountId - isManual - transactionsCount - holdingsCount - manualInvestmentsTrackingMethod - order - logoUrl - type { - name - display - group - __typename - } - subtype { - name - display - __typename - } - credential { - id - updateRequired - disconnectedFromDataProviderAt - dataProvider - institution { - id - plaidInstitutionId - name - status - __typename - } - __typename - } - institution { - id - name - primaryColor - url - __typename - } - __typename - } - - fragment EditAccountFormFields on Account { - id - displayName - deactivatedAt - displayBalance - includeInNetWorth - hideFromList - hideTransactionsFromReports - dataProvider - dataProviderAccountId - isManual - manualInvestmentsTrackingMethod - isAsset - invertSyncedBalance - canInvertBalance - type { - name - display - __typename - } - subtype { - name - display - __typename - } - __typename - } - - fragment InstitutionStatusFields on Institution { - id - hasIssuesReported - hasIssuesReportedMessage - plaidStatus - status - balanceStatus - transactionsStatus - __typename - } - - fragment TransactionsListFields on Transaction { - id - ...TransactionOverviewFields - __typename - } - - fragment TransactionOverviewFields on Transaction { - id - amount - pending - date - hideFromReports - plaidName - notes - isRecurring - reviewStatus - needsReview - dataProviderDescription - attachments { - id - __typename - } - isSplitTransaction - category { - id - name - group { - id - type - __typename - } - __typename - } - merchant { - name - id - transactionsCount - __typename - } - tags { - id - name - color - order - __typename - } - __typename - } - """ ) variables = {"id": str(account_id)} @@ -1019,92 +687,20 @@ async def get_account_history(self, account_id: int) -> Dict[str, Any]: i.update(dict(accountId=str(account_id))) i.update(dict(accountName=account_name)) - return account_balance_history + return GetAccountHistoryResponse(**account_balance_history) - async def get_institutions(self) -> Dict[str, Any]: + async def get_institutions(self) -> GetInstitutionsResponse: """ Gets institution data from the account. """ - query = gql( - """ - query Web_GetInstitutionSettings { - credentials { - id - ...CredentialSettingsCardFields - __typename - } - accounts(filters: {includeDeleted: true}) { - id - displayName - subtype { - display - __typename - } - mask - credential { - id - __typename - } - deletedAt - __typename - } - subscription { - isOnFreeTrial - hasPremiumEntitlement - __typename - } - } - - fragment CredentialSettingsCardFields on Credential { - id - updateRequired - disconnectedFromDataProviderAt - ...InstitutionInfoFields - institution { - id - name - url - __typename - } - __typename - } - - fragment InstitutionInfoFields on Credential { - id - displayLastUpdatedAt - dataProvider - updateRequired - disconnectedFromDataProviderAt - ...InstitutionLogoWithStatusFields - institution { - id - name - hasIssuesReported - hasIssuesReportedMessage - __typename - } - __typename - } - - fragment InstitutionLogoWithStatusFields on Credential { - dataProvider - updateRequired - institution { - hasIssuesReported - status - balanceStatus - transactionsStatus - __typename - } - __typename - } - """ - ) - return await self.gql_call( + query = gql(QUERY_GET_INSTITUTIONS) + result = await self.gql_call( operation="Web_GetInstitutionSettings", graphql_query=query, ) + return GetInstitutionsResponse(**result) + async def get_budgets( self, @@ -1112,7 +708,7 @@ async def get_budgets( end_date: Optional[str] = None, use_legacy_goals: Optional[bool] = False, use_v2_goals: Optional[bool] = True, - ) -> Dict[str, Any]: + ) -> GetBudgetsResponse: """ Get your budgets and corresponding actual amounts from the account. @@ -1129,172 +725,8 @@ async def get_budgets( :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 - order - budgetVariability - rolloverPeriod { - id - startMonth - endMonth - __typename - } - __typename - } - type - __typename - } - goals @include(if: $useLegacyGoals) { - id - name - 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 - } - """ + query = gql(QUERY_GET_BUDGETS + ) variables = { @@ -1335,70 +767,35 @@ async def get_budgets( "You must specify both a startDate and endDate, not just one of them." ) - return await self.gql_call( + result = await self.gql_call( operation="GetJointPlanningData", graphql_query=query, variables=variables, ) + return GetBudgetsResponse(**result) - async def get_subscription_details(self) -> Dict[str, Any]: + async def get_subscription_details(self) -> GetSubscriptionDetailsResponse: """ The type of subscription for the Monarch Money account. """ - query = gql( - """ - query GetSubscriptionDetails { - subscription { - id - paymentSource - referralCode - isOnFreeTrial - hasPremiumEntitlement - __typename - } - } - """ - ) - return await self.gql_call( + query = gql(QUERY_GET_SUBSCRIPTION_DETAILS) + result = await self.gql_call( operation="GetSubscriptionDetails", graphql_query=query, ) + return GetSubscriptionDetailsResponse(**result) - async def get_transactions_summary(self) -> Dict[str, Any]: + async def get_transactions_summary(self) -> GetTransactionsSummaryResponse: """ Gets transactions summary from the account. """ - query = gql( - """ - query GetTransactionsPage($filters: TransactionFilterInput) { - aggregates(filters: $filters) { - summary { - ...TransactionsSummaryFields - __typename - } - __typename - } - } - - fragment TransactionsSummaryFields on TransactionsSummary { - avg - count - max - maxExpense - sum - sumIncome - sumExpense - first - last - __typename - } - """ - ) - return await self.gql_call( + query = gql(QUERY_GET_TRANSACTIONS_SUMMARY) + result = await self.gql_call( operation="GetTransactionsPage", graphql_query=query, ) + return GetTransactionsSummaryResponse(**result) async def get_transactions( self, @@ -1417,7 +814,7 @@ async def get_transactions( is_recurring: Optional[bool] = None, imported_from_mint: Optional[bool] = None, synced_from_institution: Optional[bool] = None, - ) -> Dict[str, Any]: + ) -> GetTransactionsResponse: """ Gets transaction data from the account. @@ -1438,74 +835,7 @@ async def get_transactions( :param synced_from_institution: a bool to filter for whether the transactions were synced from an institution. """ - query = gql( - """ - query GetTransactionsList($offset: Int, $limit: Int, $filters: TransactionFilterInput, $orderBy: TransactionOrdering) { - allTransactions(filters: $filters) { - totalCount - results(offset: $offset, limit: $limit, orderBy: $orderBy) { - id - ...TransactionOverviewFields - __typename - } - __typename - } - transactionRules { - id - __typename - } - } - - fragment TransactionOverviewFields on Transaction { - id - amount - pending - date - hideFromReports - plaidName - notes - isRecurring - reviewStatus - needsReview - attachments { - id - extension - filename - originalAssetUrl - publicId - sizeBytes - __typename - } - isSplitTransaction - createdAt - updatedAt - category { - id - name - __typename - } - merchant { - name - id - transactionsCount - __typename - } - account { - id - displayName - __typename - } - tags { - id - name - color - order - __typename - } - __typename - } - """ - ) + query = gql(QUERY_GET_TRANSACTIONS_LIST) variables = { "offset": offset, @@ -1549,9 +879,10 @@ async def get_transactions( "You must specify both a startDate and endDate, not just one of them." ) - return await self.gql_call( + result = await self.gql_call( operation="GetTransactionsList", graphql_query=query, variables=variables ) + return GetTransactionsResponse(**result) async def create_transaction( self, @@ -1661,39 +992,13 @@ async def delete_transaction(self, transaction_id: str) -> bool: return True - async def get_transaction_categories(self) -> Dict[str, Any]: + async def get_transaction_categories(self) -> GetTransactionCategoriesResponse: """ Gets all the categories configured in the account. """ - query = gql( - """ - query GetCategories { - categories { - ...CategoryFields - __typename - } - } - - fragment CategoryFields on Category { - id - order - name - systemCategory - isSystemCategory - isDisabled - updatedAt - createdAt - group { - id - name - type - __typename - } - __typename - } - """ - ) - return await self.gql_call(operation="GetCategories", graphql_query=query) + query = gql(QUERY_GET_TRANSACTION_CATEGORIES) + result = await self.gql_call(operation="GetCategories", graphql_query=query) + return GetTransactionCategoriesResponse(**result) async def delete_transaction_category(self, category_id: str) -> bool: query = gql( @@ -1746,28 +1051,15 @@ async def delete_transaction_categories( return_exceptions=True, ) - async def get_transaction_category_groups(self) -> Dict[str, Any]: + async def get_transaction_category_groups(self) -> GetTransactionCategoryGroupsResponse: """ Gets all the category groups configured in the account. """ - query = gql( - """ - query ManageGetCategoryGroups { - categoryGroups { - id - name - order - type - updatedAt - createdAt - __typename - } - } - """ - ) - return await self.gql_call( + query = gql(QUERY_GET_TRANSACTION_CATEGORY_GROUPS) + result= await self.gql_call( operation="ManageGetCategoryGroups", graphql_query=query ) + return GetTransactionCategoryGroupsResponse(**result) async def create_transaction_category( self, @@ -1895,31 +1187,15 @@ async def create_transaction_tag(self, name: str, color: str) -> Dict[str, Any]: variables=variables, ) - async def get_transaction_tags(self) -> Dict[str, Any]: + async def get_transaction_tags(self) -> GetTransactionTagsResponse: """ Gets all the tags configured in the account. """ - query = gql( - """ - query GetHouseholdTransactionTags($search: String, $limit: Int, $bulkParams: BulkTransactionDataParams) { - householdTransactionTags( - search: $search - limit: $limit - bulkParams: $bulkParams - ) { - id - name - color - order - transactionCount - __typename - } - } - """ - ) - return await self.gql_call( + query = gql(QUERY_GET_TRANSACTION_TAGS) + result = await self.gql_call( operation="GetHouseholdTransactionTags", graphql_query=query ) + return GetTransactionTagsResponse(**result) async def set_transaction_tags( self, @@ -1978,203 +1254,40 @@ async def set_transaction_tags( async def get_transaction_details( self, transaction_id: str, redirect_posted: bool = True - ) -> Dict[str, Any]: + ) -> GetTransactionDetailsResponse: """ Returns detailed information about a transaction. :param transaction_id: the transaction to fetch. :param redirect_posted: whether to redirect posted transactions. Defaults to True. """ - query = gql( - """ - query GetTransactionDrawer($id: UUID!, $redirectPosted: Boolean) { - getTransaction(id: $id, redirectPosted: $redirectPosted) { - id - amount - pending - isRecurring - date - originalDate - hideFromReports - needsReview - reviewedAt - reviewedByUser { - id - name - __typename - } - plaidName - notes - hasSplitTransactions - isSplitTransaction - isManual - splitTransactions { - id - ...TransactionDrawerSplitMessageFields - __typename - } - originalTransaction { - id - ...OriginalTransactionFields - __typename - } - attachments { - id - publicId - extension - sizeBytes - filename - originalAssetUrl - __typename - } - account { - id - ...TransactionDrawerAccountSectionFields - __typename - } - category { - id - __typename - } - goal { - id - __typename - } - merchant { - id - name - transactionCount - logoUrl - recurringTransactionStream { - id - __typename - } - __typename - } - tags { - id - name - color - order - __typename - } - needsReviewByUser { - id - __typename - } - __typename - } - myHousehold { - users { - id - name - __typename - } - __typename - } - } - - fragment TransactionDrawerSplitMessageFields on Transaction { - id - amount - merchant { - id - name - __typename - } - category { - id - name - __typename - } - __typename - } - - fragment OriginalTransactionFields on Transaction { - id - date - amount - merchant { - id - name - __typename - } - __typename - } - - fragment TransactionDrawerAccountSectionFields on Account { - id - displayName - logoUrl - id - mask - subtype { - display - __typename - } - __typename - } - """ - ) + query = gql(QUERY_GET_TRANSACTION_DETAILS) variables = { "id": transaction_id, "redirectPosted": redirect_posted, } - return await self.gql_call( + result = await self.gql_call( operation="GetTransactionDrawer", variables=variables, graphql_query=query ) + return GetTransactionDetailsResponse(**result) - async def get_transaction_splits(self, transaction_id: str) -> Dict[str, Any]: + async def get_transaction_splits(self, transaction_id: str) -> GetTransactionSplitsResponse: """ Returns the transaction split information for a transaction. :param transaction_id: the transaction to query. """ - query = gql( - """ - query TransactionSplitQuery($id: UUID!) { - getTransaction(id: $id) { - id - amount - category { - id - name - __typename - } - merchant { - id - name - __typename - } - splitTransactions { - id - merchant { - id - name - __typename - } - category { - id - name - __typename - } - amount - notes - __typename - } - __typename - } - } - """ - ) + query = gql(QUERY_GET_TRANSACTION_SPLITS) variables = {"id": transaction_id} - return await self.gql_call( + result = await self.gql_call( operation="TransactionSplitQuery", variables=variables, graphql_query=query ) + return GetTransactionSplitsResponse(**result) + async def update_transaction_splits( self, transaction_id: str, split_data: List[Dict[str, Any]] @@ -2259,75 +1372,7 @@ async def get_cashflow( """ Gets all the categories configured in the account. """ - query = gql( - """ - query Web_GetCashFlowPage($filters: TransactionFilterInput) { - byCategory: aggregates(filters: $filters, groupBy: ["category"]) { - groupBy { - category { - id - name - group { - id - type - __typename - } - __typename - } - __typename - } - summary { - sum - __typename - } - __typename - } - byCategoryGroup: aggregates(filters: $filters, groupBy: ["categoryGroup"]) { - groupBy { - categoryGroup { - id - name - type - __typename - } - __typename - } - summary { - sum - __typename - } - __typename - } - byMerchant: aggregates(filters: $filters, groupBy: ["merchant"]) { - groupBy { - merchant { - id - name - logoUrl - __typename - } - __typename - } - summary { - sumIncome - sumExpense - __typename - } - __typename - } - summary: aggregates(filters: $filters, fillEmptyValues: true) { - summary { - sumIncome - sumExpense - savings - savingsRate - __typename - } - __typename - } - } - """ - ) + query = gql(QUERY_GET_CASHFLOW) variables = { "limit": limit, @@ -2360,26 +1405,11 @@ async def get_cashflow_summary( limit: int = DEFAULT_RECORD_LIMIT, start_date: Optional[str] = None, end_date: Optional[str] = None, - ) -> Dict[str, Any]: + ) -> GetCashFlowResponse: """ Gets all the categories configured in the account. """ - query = gql( - """ - query Web_GetCashFlowPage($filters: TransactionFilterInput) { - summary: aggregates(filters: $filters, fillEmptyValues: true) { - summary { - sumIncome - sumExpense - savings - savingsRate - __typename - } - __typename - } - } - """ - ) + query = gql(QUERY_GET_CASHFLOW_SUMMARY) variables = { "limit": limit, @@ -2403,9 +1433,10 @@ async def get_cashflow_summary( variables["filters"]["startDate"] = self._get_start_of_current_month() variables["filters"]["endDate"] = self._get_end_of_current_month() - return await self.gql_call( + result = await self.gql_call( operation="Web_GetCashFlowPage", variables=variables, graphql_query=query ) + return GetCashFlowResponse(**result) async def update_transaction( self, @@ -2668,53 +1699,12 @@ async def get_recurring_transactions( self, start_date: Optional[str] = None, end_date: Optional[str] = None, - ) -> Dict[str, Any]: + ) -> GetRecurringTransactionsResponse: """ Fetches upcoming recurring transactions from Monarch Money's API. This includes all merchant data, as well as the accounts where the charge will take place. """ - query = gql( - """ - query Web_GetUpcomingRecurringTransactionItems($startDate: Date!, $endDate: Date!, $filters: RecurringTransactionFilter) { - recurringTransactionItems( - startDate: $startDate - endDate: $endDate - filters: $filters - ) { - stream { - id - frequency - amount - isApproximate - merchant { - id - name - logoUrl - __typename - } - __typename - } - date - isPast - transactionId - amount - amountDiff - category { - id - name - __typename - } - account { - id - displayName - logoUrl - __typename - } - __typename - } - } - """ - ) + query = gql(QUERY_GET_RECURRING_TRANSACTIONS) variables = {"startDate": start_date, "endDate": end_date} @@ -2726,9 +1716,10 @@ async def get_recurring_transactions( variables["startDate"] = self._get_start_of_current_month() variables["endDate"] = self._get_end_of_current_month() - return await self.gql_call( + result = await self.gql_call( "Web_GetUpcomingRecurringTransactionItems", query, variables ) + return GetRecurringTransactionsResponse(**result) def _get_current_date(self) -> str: """ diff --git a/tests/test_model.py b/tests/test_model.py new file mode 100644 index 0000000..1278e31 --- /dev/null +++ b/tests/test_model.py @@ -0,0 +1,623 @@ +import unittest +from datetime import datetime, date +from decimal import Decimal +from monarchmoney.models import ( + Account, AccountType, AccountSubtype, Institution, Credential, + HouseholdPreferences, GetAccountsResponse, AccountTypeOption, + GetAccountTypeOptionsResponse, AccountSnapshot, GetAccountSnapshotsResponse, + AggregateSnapshot, GetAggregateSnapshotsResponse, Subscription, + GetSubscriptionDetailsResponse, TransactionsSummary, GetTransactionsSummaryResponse, + Tag, Category, Merchant, Transaction, GetTransactionsResponse, + CategoryGroup, GetTransactionCategoryGroupsResponse, GetTransactionTagsResponse, + SplitTransaction, GetTransactionDetailsResponse, CashFlowSummary, + GetCashFlowResponse, RecurringTransactionStream, RecurringTransactionItem, + GetRecurringTransactionsResponse, RecentAccountBalance, + GetRecentAccountBalancesResponse, AccountSnapshotByType, AccountType, + GetAccountSnapshotsByTypeResponse, AggregateHolding, Holding, Security, + GetAccountHoldingsResponse, AccountHistorySnapshot, GetAccountHistoryResponse, + InstitutionStatus, GetInstitutionsResponse, BudgetAmount, CategoryBudget, + CategoryGroupBudget, FlexExpenseBudget, TotalsByMonth, BudgetCategory, + Goal, GoalMonthlyContribution, GoalPlannedContribution, GoalV2, + GetBudgetsResponse, TransactionCategory, GetTransactionCategoriesResponse, + MerchantInfo, CategoryInfo, SplitTransaction as TransactionSplit, + TransactionWithSplits, GetTransactionSplitsResponse +) + +class TestModels(unittest.TestCase): + def test_account(self): + account = Account( + id="123", + display_name="Checking", + sync_disabled=False, + deactivated_at=None, + is_hidden=False, + is_asset=True, + mask="1234", + created_at=datetime.now(), + updated_at=datetime.now(), + display_last_updated_at=datetime.now(), + current_balance=1000.0, + display_balance=1000.0, + include_in_net_worth=True, + hide_from_list=False, + hide_transactions_from_reports=False, + include_balance_in_net_worth=True, + include_in_goal_balance=True, + data_provider="plaid", + data_provider_account_id="plaid123", + is_manual=False, + transactions_count=10, + holdings_count=0, + manual_investments_tracking_method=None, + order=1, + logo_url="https://example.com/logo.png", + type=AccountType(name="checking", display="Checking"), + subtype=AccountSubtype(name="personal", display="Personal"), + credential=None, + institution=None + ) + self.assertEqual(account.id, "123") + self.assertEqual(account.display_name, "Checking") + self.assertEqual(account.current_balance, 1000.0) + + def test_get_accounts_response(self): + response = GetAccountsResponse( + accounts=[ + Account( + id="123", + display_name="Checking", + sync_disabled=False, + deactivated_at=None, + is_hidden=False, + is_asset=True, + mask="1234", + created_at=datetime.now(), + updated_at=datetime.now(), + display_last_updated_at=datetime.now(), + current_balance=1000.0, + display_balance=1000.0, + include_in_net_worth=True, + hide_from_list=False, + hide_transactions_from_reports=False, + include_balance_in_net_worth=True, + include_in_goal_balance=True, + data_provider="plaid", + data_provider_account_id="plaid123", + is_manual=False, + transactions_count=10, + holdings_count=0, + manual_investments_tracking_method=None, + order=1, + logo_url="https://example.com/logo.png", + type=AccountType(name="checking", display="Checking"), + subtype=AccountSubtype(name="personal", display="Personal"), + credential=None, + institution=None + ) + ], + household_preferences=HouseholdPreferences( + id="456", + account_group_order=["123"] + ) + ) + self.assertEqual(len(response.accounts), 1) + self.assertEqual(response.accounts[0].id, "123") + self.assertEqual(response.household_preferences.id, "456") + + def test_get_account_type_options_response(self): + response = GetAccountTypeOptionsResponse( + account_type_options=[ + AccountTypeOption( + type=AccountType(name="checking", display="Checking"), + subtype=AccountSubtype(name="personal", display="Personal") + ) + ] + ) + self.assertEqual(len(response.account_type_options), 1) + self.assertEqual(response.account_type_options[0].type.name, "checking") + + def test_get_account_snapshots_response(self): + response = GetAccountSnapshotsResponse( + snapshots=[ + AccountSnapshot(date=date(2023, 1, 1), signed_balance=1000.0), + AccountSnapshot(date=date(2023, 1, 2), signed_balance=1100.0) + ] + ) + self.assertEqual(len(response.snapshots), 2) + self.assertEqual(response.snapshots[0].signed_balance, 1000.0) + + def test_get_aggregate_snapshots_response(self): + response = GetAggregateSnapshotsResponse( + aggregate_snapshots=[ + AggregateSnapshot(date=date(2023, 1, 1), balance=10000.0), + AggregateSnapshot(date=date(2023, 1, 2), balance=10100.0) + ] + ) + self.assertEqual(len(response.aggregate_snapshots), 2) + self.assertEqual(response.aggregate_snapshots[0].balance, 10000.0) + + def test_get_subscription_details_response(self): + response = GetSubscriptionDetailsResponse( + subscription=Subscription( + id="789", + payment_source="credit_card", + referral_code="REF123", + is_on_free_trial=False, + has_premium_entitlement=True + ) + ) + self.assertEqual(response.subscription.id, "789") + self.assertTrue(response.subscription.has_premium_entitlement) + + def test_get_transactions_summary_response(self): + response = GetTransactionsSummaryResponse( + summary=TransactionsSummary( + avg=100.0, + count=10, + max=500.0, + max_expense=300.0, + sum=1000.0, + sum_income=1500.0, + sum_expense=500.0, + first=datetime(2023, 1, 1), + last=datetime(2023, 1, 31) + ) + ) + self.assertEqual(response.summary.count, 10) + self.assertEqual(response.summary.sum_income, 1500.0) + + def test_get_transactions_response(self): + response = GetTransactionsResponse( + total_count=1, + results=[ + Transaction( + id="t123", + amount=-50.0, + pending=False, + date=date(2023, 1, 15), + hide_from_reports=False, + plaid_name="ACME Store", + notes="Groceries", + is_recurring=False, + review_status=None, + needs_review=False, + attachments=[], + is_split_transaction=False, + created_at=datetime.now(), + updated_at=datetime.now(), + category=Category(id="c1", name="Groceries", group={"id": "g1", "name": "Essentials"}), + merchant=Merchant(name="ACME Store", id="m1", transactions_count=5), + account=Account( + id="a1", + display_name="Checking", + sync_disabled=False, + deactivated_at=None, + is_hidden=False, + is_asset=True, + mask="1234", + created_at=datetime.now(), + updated_at=datetime.now(), + display_last_updated_at=datetime.now(), + current_balance=1000.0, + display_balance=1000.0, + include_in_net_worth=True, + hide_from_list=False, + hide_transactions_from_reports=False, + include_balance_in_net_worth=True, + include_in_goal_balance=True, + data_provider="plaid", + data_provider_account_id="plaid123", + is_manual=False, + transactions_count=10, + holdings_count=0, + manual_investments_tracking_method=None, + order=1, + logo_url="https://example.com/logo.png", + type=AccountType(name="checking", display="Checking"), + subtype=AccountSubtype(name="personal", display="Personal"), + credential=None, + institution=None + ), + tags=[] + ) + ], + transaction_rules=[] + ) + self.assertEqual(response.total_count, 1) + self.assertEqual(response.results[0].amount, -50.0) + self.assertEqual(response.results[0].merchant.name, "ACME Store") + + def test_get_transaction_category_groups_response(self): + response = GetTransactionCategoryGroupsResponse( + category_groups=[ + CategoryGroup( + id="g1", + name="Essentials", + type="expense", + order=1, + updated_at=datetime.now(), + created_at=datetime.now() + ) + ] + ) + self.assertEqual(len(response.category_groups), 1) + self.assertEqual(response.category_groups[0].name, "Essentials") + + def test_get_transaction_tags_response(self): + response = GetTransactionTagsResponse( + tags=[ + Tag(id="t1", name="Vacation", color="#FF0000", order=1) + ] + ) + self.assertEqual(len(response.tags), 1) + self.assertEqual(response.tags[0].name, "Vacation") + + def test_get_transaction_details_response(self): + response = GetTransactionDetailsResponse( + transaction=Transaction( + id="t123", + amount=-50.0, + pending=False, + date=date(2023, 1, 15), + hide_from_reports=False, + plaid_name="ACME Store", + notes="Groceries", + is_recurring=False, + review_status=None, + needs_review=False, + attachments=[], + is_split_transaction=False, + created_at=datetime.now(), + updated_at=datetime.now(), + category=Category(id="c1", name="Groceries", group={"id": "g1", "name": "Essentials"}), + merchant=Merchant(name="ACME Store", id="m1", transactions_count=5), + account=Account( + id="a1", + display_name="Checking", + sync_disabled=False, + deactivated_at=None, + is_hidden=False, + is_asset=True, + mask="1234", + created_at=datetime.now(), + updated_at=datetime.now(), + display_last_updated_at=datetime.now(), + current_balance=1000.0, + display_balance=1000.0, + include_in_net_worth=True, + hide_from_list=False, + hide_transactions_from_reports=False, + include_balance_in_net_worth=True, + include_in_goal_balance=True, + data_provider="plaid", + data_provider_account_id="plaid123", + is_manual=False, + transactions_count=10, + holdings_count=0, + manual_investments_tracking_method=None, + order=1, + logo_url="https://example.com/logo.png", + type=AccountType(name="checking", display="Checking"), + subtype=AccountSubtype(name="personal", display="Personal"), + credential=None, + institution=None + ), + tags=[] + ) + ) + self.assertEqual(response.transaction.id, "t123") + self.assertEqual(response.transaction.amount, -50.0) + + def test_get_cashflow_response(self): + response = GetCashFlowResponse( + by_category=[], + by_category_group=[], + by_merchant=[], + summary=CashFlowSummary( + sum_income=5000.0, + sum_expense=3000.0, + savings=2000.0, + savings_rate=0.4 + ) + ) + self.assertEqual(response.summary.sum_income, 5000.0) + self.assertEqual(response.summary.savings_rate, 0.4) + + def test_get_recurring_transactions_response(self): + response = GetRecurringTransactionsResponse( + recurring_transaction_items=[ + RecurringTransactionItem( + stream=RecurringTransactionStream( + id="rs1", + frequency="monthly", + amount=100.0, + is_approximate=False, + merchant=Merchant(name="Netflix", id="m2", transactions_count=12) + ), + date=date(2023, 2, 1), + is_past=False, + transaction_id=None, + amount=100.0, + amount_diff=0.0, + category=Category(id="c2", name="Streaming", group={"id": "g2", "name": "Entertainment"}), + account=Account( + id="a1", + display_name="Checking", + sync_disabled=False, + deactivated_at=None, + is_hidden=False, + is_asset=True, + mask="1234", + created_at=datetime.now(), + updated_at=datetime.now(), + display_last_updated_at=datetime.now(), + current_balance=1000.0, + display_balance=1000.0, + include_in_net_worth=True, + hide_from_list=False, + hide_transactions_from_reports=False, + include_balance_in_net_worth=True, + include_in_goal_balance=True, + data_provider="plaid", + data_provider_account_id="plaid123", + is_manual=False, + transactions_count=10, + holdings_count=0, + manual_investments_tracking_method=None, + order=1, + logo_url="https://example.com/logo.png", + type=AccountType(name="checking", display="Checking"), + subtype=AccountSubtype(name="personal", display="Personal"), + credential=None, + institution=None + ) + ) + ] + ) + self.assertEqual(len(response.recurring_transaction_items), 1) + self.assertEqual(response.recurring_transaction_items[0].stream.frequency, "monthly") + self.assertEqual(response.recurring_transaction_items[0].amount, 100.0) + + def test_get_recent_account_balances_response(self): + response = GetRecentAccountBalancesResponse( + accounts=[ + RecentAccountBalance( + id="acc1", + recent_balances=[1000.0, 1100.0, 1200.0] + ), + RecentAccountBalance( + id="acc2", + recent_balances=[2000.0, 2100.0, 2200.0] + ) + ] + ) + self.assertEqual(len(response.accounts), 2) + self.assertEqual(response.accounts[0].id, "acc1") + self.assertEqual(len(response.accounts[0].recent_balances), 3) + self.assertEqual(response.accounts[0].recent_balances[0], 1000.0) + self.assertEqual(response.accounts[1].id, "acc2") + self.assertEqual(response.accounts[1].recent_balances[-1], 2200.0) + + def test_get_account_snapshots_by_type_response(self): + response = GetAccountSnapshotsByTypeResponse( + snapshots_by_account_type=[ + AccountSnapshotByType(account_type="checking", month="2023-05", balance=1000.0) + ], + account_types=[ + AccountType(name="checking", group="assets") + ] + ) + self.assertEqual(len(response.snapshots_by_account_type), 1) + self.assertEqual(response.snapshots_by_account_type[0].account_type, "checking") + self.assertEqual(len(response.account_types), 1) + self.assertEqual(response.account_types[0].name, "checking") + + def test_get_account_holdings_response(self): + response = GetAccountHoldingsResponse( + aggregate_holdings=[ + AggregateHolding( + id="ah1", + quantity=10.0, + basis=1000.0, + total_value=1100.0, + security_price_change_dollars=100.0, + security_price_change_percent=0.1, + last_synced_at=datetime.now(), + holdings=[ + Holding( + id="h1", + type="stock", + type_display="Stock", + name="ACME Corp", + ticker="ACME", + closing_price=110.0, + is_manual=False, + closing_price_updated_at=datetime.now() + ) + ], + security=Security( + id="s1", + name="ACME Corp", + type="stock", + ticker="ACME", + type_display="Stock", + current_price=110.0, + current_price_updated_at=datetime.now(), + closing_price=109.0, + closing_price_updated_at=datetime.now(), + one_day_change_percent=0.01, + one_day_change_dollars=1.0 + ) + ) + ] + ) + self.assertEqual(len(response.aggregate_holdings), 1) + self.assertEqual(response.aggregate_holdings[0].quantity, 10.0) + self.assertEqual(response.aggregate_holdings[0].holdings[0].name, "ACME Corp") + self.assertEqual(response.aggregate_holdings[0].security.ticker, "ACME") + + def test_get_account_history_response(self): + response = GetAccountHistoryResponse( + account_balance_history=[ + AccountHistorySnapshot( + date=date(2023, 1, 1), + signed_balance=1000.0, + account_id="acc1", + account_name="Checking" + ), + AccountHistorySnapshot( + date=date(2023, 1, 2), + signed_balance=1100.0, + account_id="acc1", + account_name="Checking" + ) + ] + ) + self.assertEqual(len(response.account_balance_history), 2) + self.assertEqual(response.account_balance_history[0].signed_balance, 1000.0) + self.assertEqual(response.account_balance_history[1].account_name, "Checking") + + def test_get_institutions_response(self): + response = GetInstitutionsResponse( + credentials=[ + Credential( + id="cred1", + display_last_updated_at=datetime.now(), + data_provider="plaid", + update_required=False, + disconnected_from_data_provider_at=None, + institution=Institution( + id="inst1", + name="Bank of America", + url="https://www.bankofamerica.com", + status=InstitutionStatus( + has_issues_reported=False, + has_issues_reported_message=None, + plaid_status="HEALTHY", + status="ACTIVE", + balance_status="UPDATED", + transactions_status="UPDATED" + ) + ) + ) + ], + accounts=[], + subscription={} + ) + self.assertEqual(len(response.credentials), 1) + self.assertEqual(response.credentials[0].institution.name, "Bank of America") + self.assertEqual(response.credentials[0].institution.status.plaid_status, "HEALTHY") + + def test_get_budgets_response(self): + response = GetBudgetsResponse( + budget_data={}, + category_groups=[ + CategoryGroup( + id="cg1", + name="Food", + order=1, + group_level_budgeting_enabled=True, + budget_variability="fixed", + rollover_period=None, + categories=[ + BudgetCategory( + id="c1", + name="Groceries", + order=1, + budget_variability="fixed", + rollover_period=None + ) + ], + type="expense" + ) + ], + goals=None, + goal_monthly_contributions=None, + goal_planned_contributions=None, + goals_v2=None, + budget_system="default" + ) + self.assertEqual(len(response.category_groups), 1) + self.assertEqual(response.category_groups[0].name, "Food") + self.assertEqual(len(response.category_groups[0].categories), 1) + self.assertEqual(response.category_groups[0].categories[0].name, "Groceries") + + def test_get_transaction_categories_response(self): + response = GetTransactionCategoriesResponse( + categories=[ + TransactionCategory( + id="cat1", + order=1, + name="Groceries", + system_category=True, + is_system_category=True, + is_disabled=False, + updated_at=datetime.now(), + created_at=datetime.now(), + group=CategoryGroup( + id="group1", + name="Food & Dining", + type="expense" + ) + ), + TransactionCategory( + id="cat2", + order=2, + name="Salary", + system_category=True, + is_system_category=True, + is_disabled=False, + updated_at=datetime.now(), + created_at=datetime.now(), + group=CategoryGroup( + id="group2", + name="Income", + type="income" + ) + ) + ] + ) + self.assertEqual(len(response.categories), 2) + self.assertEqual(response.categories[0].name, "Groceries") + self.assertEqual(response.categories[0].group.name, "Food & Dining") + self.assertEqual(response.categories[1].name, "Salary") + self.assertEqual(response.categories[1].group.type, "income") + + def test_get_transaction_splits_response(self): + response = GetTransactionSplitsResponse( + transaction=TransactionWithSplits( + id="trans1", + amount=Decimal("100.00"), + category=CategoryInfo(id="cat1", name="Groceries"), + merchant=MerchantInfo(id="merch1", name="Supermarket"), + split_transactions=[ + TransactionSplit( + id="split1", + merchant=MerchantInfo(id="merch1", name="Supermarket"), + category=CategoryInfo(id="cat2", name="Food"), + amount=Decimal("60.00"), + notes="Foodstuff" + ), + TransactionSplit( + id="split2", + merchant=MerchantInfo(id="merch1", name="Supermarket"), + category=CategoryInfo(id="cat3", name="Household"), + amount=Decimal("40.00"), + notes="Cleaning supplies" + ) + ] + ) + ) + + self.assertEqual(response.transaction.id, "trans1") + self.assertEqual(response.transaction.amount, Decimal("100.00")) + self.assertEqual(response.transaction.category.name, "Groceries") + self.assertEqual(response.transaction.merchant.name, "Supermarket") + self.assertEqual(len(response.transaction.split_transactions), 2) + self.assertEqual(response.transaction.split_transactions[0].amount, Decimal("60.00")) + self.assertEqual(response.transaction.split_transactions[0].category.name, "Food") + self.assertEqual(response.transaction.split_transactions[1].amount, Decimal("40.00")) + self.assertEqual(response.transaction.split_transactions[1].category.name, "Household") + +if __name__ == '__main__': + unittest.main() \ No newline at end of file