diff --git a/docs/interacting.md b/docs/interacting.md index bfd2a43..2f6b7a4 100644 --- a/docs/interacting.md +++ b/docs/interacting.md @@ -31,6 +31,7 @@ lunch = LunchMoney(access_token="xxxxxxxxxxx") | POST | [insert_into_category_group](#lunchable.LunchMoney.insert_into_category_group) | Add to a Category Group | | POST | [insert_transaction_group](#lunchable.LunchMoney.insert_transaction_group) | Create a Transaction Group of Two or More Transactions | | POST | [insert_transactions](#lunchable.LunchMoney.insert_transactions) | Create One or Many Lunch Money Transactions | +| POST | [trigger_fetch_from_plaid](#lunchable.LunchMoney.trigger_fetch_from_plaid) | Trigger a Plaid Sync | | POST | [unsplit_transactions](#lunchable.LunchMoney.unsplit_transactions) | Unsplit Transactions | | PUT | [upsert_budget](#lunchable.LunchMoney.upsert_budget) | Upsert a Budget for a Category and Date | | PUT | [update_asset](#lunchable.LunchMoney.update_asset) | Update a Single Asset | diff --git a/lunchable/models/_descriptions.py b/lunchable/models/_descriptions.py index 2470e52..c359e07 100644 --- a/lunchable/models/_descriptions.py +++ b/lunchable/models/_descriptions.py @@ -107,6 +107,17 @@ class _CategoriesDescriptions: For category groups, this will populate with the categories nested within and include id, name, description and created_at fields. """ + archived = """ + If true, the category is archived and not displayed in relevant + areas of the Lunch Money app. + """ + archived_on = """ + The date and time of when the category was last archived + (in the ISO 8601 extended format). + """ + order = """ + Numerical ordering of categories + """ class _CryptoDescriptions: @@ -453,3 +464,7 @@ class _TransactionDescriptions: (for synced investment transactions only) The quantity as set by Plaid for investment transactions. """ + to_base = """ + The amount converted to the user's primary currency. If the multicurrency + feature is not being used, to_base and amount will be the same. + """ diff --git a/lunchable/models/assets.py b/lunchable/models/assets.py index 98ab3b9..0a974c0 100644 --- a/lunchable/models/assets.py +++ b/lunchable/models/assets.py @@ -89,7 +89,7 @@ class _AssetsParamsPost(LunchableModel): currency: Optional[str] = None institution_name: Optional[str] = None closed_on: Optional[datetime.date] = None - exclude_transactions: bool = False + exclude_transactions: Optional[bool] = None @field_validator("balance", mode="before") @classmethod @@ -193,7 +193,7 @@ def insert_asset( currency: Optional[str] = None, institution_name: Optional[str] = None, closed_on: Optional[datetime.date] = None, - exclude_transactions: bool = False, + exclude_transactions: Optional[bool] = None, ) -> AssetsObject: """ Create a single (manually-managed) asset. diff --git a/lunchable/models/categories.py b/lunchable/models/categories.py index 701e157..7419aab 100644 --- a/lunchable/models/categories.py +++ b/lunchable/models/categories.py @@ -4,10 +4,13 @@ https://lunchmoney.dev/#categories """ +from __future__ import annotations + import datetime import json import logging -from typing import List, Optional +from enum import Enum +from typing import List, Optional, Union from pydantic import Field @@ -27,9 +30,11 @@ class ModelCreateCategory(LunchableModel): name: str description: Optional[str] = None - is_income: Optional[bool] = False - exclude_from_budget: Optional[bool] = False - exclude_from_totals: Optional[bool] = False + is_income: Optional[bool] = None + exclude_from_budget: Optional[bool] = None + exclude_from_totals: Optional[bool] = None + archived: Optional[bool] = None + group_id: Optional[int] = None class CategoryChild(LunchableModel): @@ -66,6 +71,10 @@ class CategoriesObject(LunchableModel): exclude_from_totals: bool = Field( description=_CategoriesDescriptions.exclude_from_totals ) + archived: bool = Field(False, description=_CategoriesDescriptions.archived) + archived_on: Optional[datetime.datetime] = Field( + None, description=_CategoriesDescriptions.archived_on + ) updated_at: Optional[datetime.datetime] = Field( None, description=_CategoriesDescriptions.updated_at ) @@ -74,7 +83,8 @@ class CategoriesObject(LunchableModel): ) is_group: bool = Field(description=_CategoriesDescriptions.is_group) group_id: Optional[int] = Field(None, description=_CategoriesDescriptions.group_id) - children: Optional[List[CategoryChild]] = Field( + order: Optional[int] = Field(None, description=_CategoriesDescriptions.order) + children: Optional[List[Union[CategoriesObject, CategoryChild]]] = Field( None, description=_CategoriesDescriptions.children, ) @@ -90,6 +100,7 @@ class _CategoriesParamsPut(LunchableModel): is_income: Optional[bool] = None exclude_from_budget: Optional[bool] = None exclude_from_totals: Optional[bool] = None + archived: Optional[bool] = None group_id: Optional[int] = None @@ -100,9 +111,9 @@ class _CategoriesParamsPost(LunchableModel): name: str description: Optional[str] = None - is_income: Optional[bool] = False - exclude_from_budget: Optional[bool] = False - exclude_from_totals: Optional[bool] = False + is_income: Optional[bool] = None + exclude_from_budget: Optional[bool] = None + exclude_from_totals: Optional[bool] = None category_ids: Optional[List[int]] = None new_categories: Optional[List[str]] = None @@ -116,24 +127,55 @@ class _CategoriesAddParamsPost(LunchableModel): new_categories: Optional[List[str]] = None +class CategoriesFormatEnum(str, Enum): + """ + Format Enum + """ + + nested: str = "nested" + flattened: str = "flattened" + + +class _GetCategoriesParams(LunchableModel): + """ + https://lunchmoney.dev/#get-all-categories + """ + + format: Optional[CategoriesFormatEnum] = None + + class CategoriesClient(LunchMoneyAPIClient): """ Lunch Money Categories Interactions """ - def get_categories(self) -> List[CategoriesObject]: + def get_categories( + self, format: str | CategoriesFormatEnum | None = None + ) -> List[CategoriesObject]: """ Get Spending categories Use this endpoint to get a list of all categories associated with the user's account. https://lunchmoney.dev/#get-all-categories + Parameters + ---------- + format: str | CategoriesFormatEnum | None + Can either `flattened` or `nested`. If `flattened`, returns a singular + array of categories, ordered alphabetically. If `nested`, returns + top-level categories (either category groups or categories not part + of a category group) in an array. Subcategories are nested within + the category group under the property `children`. Defaults to None + which will return a `flattened` list of categories. + Returns ------- List[CategoriesObject] """ response_data = self.make_request( - method=self.Methods.GET, url_path=APIConfig.LUNCHMONEY_CATEGORIES + method=self.Methods.GET, + url_path=APIConfig.LUNCHMONEY_CATEGORIES, + params=_GetCategoriesParams(format=format).model_dump(exclude_none=True), ) categories = response_data["categories"] category_objects = [ @@ -145,9 +187,11 @@ def insert_category( self, name: str, description: Optional[str] = None, - is_income: Optional[bool] = False, - exclude_from_budget: Optional[bool] = False, - exclude_from_totals: Optional[bool] = False, + is_income: Optional[bool] = None, + exclude_from_budget: Optional[bool] = None, + exclude_from_totals: Optional[bool] = None, + archived: Optional[bool] = None, + group_id: Optional[int] = None, ) -> int: """ Create a Spending Category @@ -170,6 +214,10 @@ def insert_category( exclude_from_totals: Optional[bool] Whether or not transactions in this category should be excluded from calculated totals. Defaults to False. + archived: Optional[bool] + Whether or not category should be archived. + group_id: Optional[int] + Assigns the newly-created category to an existing category group. Returns ------- @@ -182,6 +230,8 @@ def insert_category( is_income=is_income, exclude_from_budget=exclude_from_budget, exclude_from_totals=exclude_from_totals, + archived=archived, + group_id=group_id, ).model_dump(exclude_none=True) response_data = self.make_request( method=self.Methods.POST, @@ -285,6 +335,7 @@ def update_category( exclude_from_budget: Optional[bool] = None, exclude_from_totals: Optional[bool] = None, group_id: Optional[int] = None, + archived: Optional[bool] = None, ) -> bool: """ Update a single category @@ -312,6 +363,8 @@ def update_category( calculated totals. Defaults to False. group_id: Optional[int] For a category, set the group_id to include it in a category group + archived: Optional[bool] + Whether or not category should be archived. Returns ------- @@ -324,6 +377,7 @@ def update_category( exclude_from_budget=exclude_from_budget, exclude_from_totals=exclude_from_totals, group_id=group_id, + archived=archived, ).model_dump(exclude_none=True) response_data = self.make_request( method=self.Methods.PUT, @@ -336,9 +390,9 @@ def insert_category_group( self, name: str, description: Optional[str] = None, - is_income: Optional[bool] = False, - exclude_from_budget: Optional[bool] = False, - exclude_from_totals: Optional[bool] = False, + is_income: Optional[bool] = None, + exclude_from_budget: Optional[bool] = None, + exclude_from_totals: Optional[bool] = None, category_ids: Optional[List[int]] = None, new_categories: Optional[List[str]] = None, ) -> int: diff --git a/lunchable/models/plaid_accounts.py b/lunchable/models/plaid_accounts.py index 4fa2865..cef14fd 100644 --- a/lunchable/models/plaid_accounts.py +++ b/lunchable/models/plaid_accounts.py @@ -4,6 +4,8 @@ https://lunchmoney.dev/#plaid-accounts """ +from __future__ import annotations + import datetime import logging from typing import List, Optional @@ -18,6 +20,18 @@ logger = logging.getLogger(__name__) +class _PlaidFetchRequest(LunchableModel): + """ + Trigger Fetch from Plaid + + https://lunchmoney.dev/#trigger-fetch-from-plaid + """ + + start_date: Optional[datetime.date] = None + end_date: Optional[datetime.date] = None + plaid_account_id: Optional[int] = None + + class PlaidAccountObject(LunchableModel): """ Assets synced from Plaid @@ -77,3 +91,49 @@ def get_plaid_accounts(self) -> List[PlaidAccountObject]: accounts = response_data.get(APIConfig.LUNCHMONEY_PLAID_ACCOUNTS) account_objects = [PlaidAccountObject.model_validate(item) for item in accounts] return account_objects + + def trigger_fetch_from_plaid( + self, + start_date: Optional[datetime.date] = None, + end_date: Optional[datetime.date] = None, + plaid_account_id: Optional[int] = None, + ) -> bool: + """ + Trigger Fetch from Plaid + + ** This is an experimental endpoint and parameters and/or response may change. ** + + Use this endpoint to trigger a fetch for latest data from Plaid. + + Returns true if there were eligible Plaid accounts to trigger a fetch for. Eligible + accounts are those who last_fetch value is over 1 minute ago. (Although the limit + is every minute, please use this endpoint sparingly!) + + Note that fetching from Plaid is a background job. This endpoint simply queues up + the job. You may track the plaid_last_successful_update, last_fetch and last_import + properties to verify the results of the fetch. + + Parameters + ---------- + start_date: Optional[datetime.date] + Start date for fetch (ignored if end_date is null) + end_date: Optional[datetime.date] + End date for fetch (ignored if start_date is null) + plaid_account_id: Optional[int] + Specific ID of a plaid account to fetch. If left empty, + endpoint will trigger a fetch for all eligible accounts + + Returns + ------- + bool + Returns true if there were eligible Plaid accounts to trigger a fetch for. + """ + fetch_request = _PlaidFetchRequest( + start_date=start_date, end_date=end_date, plaid_account_id=plaid_account_id + ) + response: bool = self.make_request( + method=self.Methods.POST, + url_path=[APIConfig.LUNCHMONEY_PLAID_ACCOUNTS, "fetch"], + data=fetch_request.model_dump(exclude_none=True), + ) + return response diff --git a/lunchable/models/recurring_expenses.py b/lunchable/models/recurring_expenses.py index ec69570..144ca9d 100644 --- a/lunchable/models/recurring_expenses.py +++ b/lunchable/models/recurring_expenses.py @@ -69,7 +69,7 @@ class RecurringExpenseParamsGet(LunchableModel): """ start_date: datetime.date - debit_as_negative: bool + debit_as_negative: Optional[bool] = None class RecurringExpensesClient(LunchMoneyAPIClient): @@ -80,7 +80,7 @@ class RecurringExpensesClient(LunchMoneyAPIClient): def get_recurring_expenses( self, start_date: Optional[datetime.date] = None, - debit_as_negative: bool = False, + debit_as_negative: Optional[bool] = None, ) -> List[RecurringExpensesObject]: """ Get Recurring Expenses @@ -115,7 +115,7 @@ def get_recurring_expenses( start_date = datetime.datetime.now().date().replace(day=1) params = RecurringExpenseParamsGet( start_date=start_date, debit_as_negative=debit_as_negative - ).model_dump() + ).model_dump(exclude_none=True) response_data = self.make_request( method=self.Methods.GET, url_path=[APIConfig.LUNCH_MONEY_RECURRING_EXPENSES], diff --git a/lunchable/models/tags.py b/lunchable/models/tags.py index 22defdb..40362b2 100644 --- a/lunchable/models/tags.py +++ b/lunchable/models/tags.py @@ -28,6 +28,13 @@ class TagsObject(LunchableModel): description: Optional[str] = Field( None, description="User-defined description of tag" ) + archived: bool = Field( + False, + description=( + "If true, the tag will not show up when creating or " + "updating transactions in the Lunch Money app" + ), + ) class TagsClient(LunchMoneyAPIClient): diff --git a/lunchable/models/transactions.py b/lunchable/models/transactions.py index a9cde9d..dcae602 100644 --- a/lunchable/models/transactions.py +++ b/lunchable/models/transactions.py @@ -191,6 +191,7 @@ class TransactionObject(TransactionBaseObject): fees: Optional[str] = Field(None, description=_TransactionDescriptions.fees) price: Optional[str] = Field(None, description=_TransactionDescriptions.price) quantity: Optional[str] = Field(None, description=_TransactionDescriptions.quantity) + to_base: Optional[float] = Field(None, description=_TransactionDescriptions.to_base) def get_update_object(self) -> TransactionUpdateObject: """ @@ -266,11 +267,11 @@ class _TransactionInsertParamsPost(LunchableModel): """ transactions: List[TransactionInsertObject] - apply_rules: bool = False - skip_duplicates: bool = False - check_for_recurring: bool = False - debit_as_negative: bool = False - skip_balance_update: bool = True + apply_rules: Optional[bool] = None + skip_duplicates: Optional[bool] = None + check_for_recurring: Optional[bool] = None + debit_as_negative: Optional[bool] = None + skip_balance_update: Optional[bool] = None class _TransactionGroupParamsPost(LunchableModel): @@ -293,8 +294,8 @@ class _TransactionUpdateParamsPut(LunchableModel): split: Optional[List[TransactionSplitObject]] = None transaction: Optional[TransactionUpdateObject] = None - debit_as_negative: bool = False - skip_balance_update: bool = True + debit_as_negative: Optional[bool] = None + skip_balance_update: Optional[bool] = None class _TransactionsUnsplitPost(LunchableModel): @@ -303,7 +304,7 @@ class _TransactionsUnsplitPost(LunchableModel): """ parent_ids: List[int] - remove_parents: bool = False + remove_parents: Optional[bool] = None class TransactionsClient(LunchMoneyAPIClient): @@ -424,7 +425,9 @@ def get_transactions( ] return transaction_objects - def get_transaction(self, transaction_id: int) -> TransactionObject: + def get_transaction( + self, transaction_id: int, debit_as_negative: Optional[bool] = None + ) -> TransactionObject: """ Get a Transaction by ID @@ -432,6 +435,9 @@ def get_transaction(self, transaction_id: int) -> TransactionObject: ---------- transaction_id: int Lunch Money Transaction ID + debit_as_negative: Optional[bool] + Pass in true if you'd like expenses to be returned as negative + amounts and credits as positive amounts. Defaults to false. Returns ------- @@ -455,6 +461,9 @@ def get_transaction(self, transaction_id: int) -> TransactionObject: response_data = self.make_request( method=self.Methods.GET, url_path=[APIConfig.LUNCHMONEY_TRANSACTIONS, transaction_id], + params={"debit_as_negative": debit_as_negative} + if debit_as_negative is not None + else {}, ) return TransactionObject.model_validate(response_data) @@ -475,8 +484,8 @@ def update_transaction( transaction_id: int, transaction: ListOrSingleTransactionUpdateObject = None, split: Optional[List[TransactionSplitObject]] = None, - debit_as_negative: bool = False, - skip_balance_update: bool = True, + debit_as_negative: Optional[bool] = None, + skip_balance_update: Optional[bool] = None, ) -> Dict[str, Any]: """ Update a Transaction @@ -496,10 +505,10 @@ def update_transaction( split: Optional[List[TransactionSplitObject]] Defines the split of a transaction. You may not split an already-split transaction, recurring transaction, or group transaction. - debit_as_negative: bool + debit_as_negative: Optional[bool] If true, will assume negative amount values denote expenses and positive amount values denote credits. Defaults to false. - skip_balance_update: bool + skip_balance_update: Optional[bool] If false, will skip updating balance if an asset_id is present for any of the transactions. @@ -563,11 +572,11 @@ def update_transaction( def insert_transactions( self, transactions: ListOrSingleTransactionInsertObject, - apply_rules: bool = False, - skip_duplicates: bool = True, - debit_as_negative: bool = False, - check_for_recurring: bool = False, - skip_balance_update: bool = True, + apply_rules: Optional[bool] = None, + skip_duplicates: Optional[bool] = None, + debit_as_negative: Optional[bool] = None, + check_for_recurring: Optional[bool] = None, + skip_balance_update: Optional[bool] = None, ) -> List[int]: """ Create One or Many Lunch Money Transactions @@ -583,20 +592,20 @@ def insert_transactions( transactions: ListOrSingleTransactionTypeObject Transactions to insert. Either a single TransactionInsertObject object or a list of them - apply_rules: bool + apply_rules: Optional[bool] If true, will apply account's existing rules to the inserted transactions. Defaults to false. - skip_duplicates: bool + skip_duplicates: Optional[bool] If true, the system will automatically dedupe based on transaction date, payee and amount. Note that deduping by external_id will occur regardless of this flag. - check_for_recurring: bool + check_for_recurring: Optional[bool] if true, will check new transactions for occurrences of new monthly expenses. Defaults to false. - debit_as_negative: bool + debit_as_negative: Optional[bool] If true, will assume negative amount values denote expenses and positive amount values denote credits. Defaults to false. - skip_balance_update: bool + skip_balance_update: Optional[bool] If false, will skip updating balance if an asset_id is present for any of the transactions. @@ -640,7 +649,7 @@ def insert_transactions( check_for_recurring=check_for_recurring, debit_as_negative=debit_as_negative, skip_balance_update=skip_balance_update, - ).model_dump(exclude_unset=True) + ).model_dump(exclude_none=True) response_data = self.make_request( method=self.Methods.POST, url_path=APIConfig.LUNCHMONEY_TRANSACTIONS, @@ -735,7 +744,7 @@ def remove_transaction_group(self, transaction_group_id: int) -> List[int]: return response_data["transactions"] def unsplit_transactions( - self, parent_ids: List[int], remove_parents: bool = False + self, parent_ids: List[int], remove_parents: Optional[bool] = None ) -> List[int]: """ Unsplit Transactions @@ -751,7 +760,7 @@ def unsplit_transactions( parent_ids: List[int] Array of transaction IDs to unsplit. If one transaction is unsplittable, no transaction will be unsplit. - remove_parents: bool + remove_parents: Optional[bool] If true, deletes the original parent transaction as well. Note, this is unreversable! diff --git a/tests/models/cassettes/test_get_categories_flattened.yaml b/tests/models/cassettes/test_get_categories_flattened.yaml new file mode 100644 index 0000000..70de1bd --- /dev/null +++ b/tests/models/cassettes/test_get_categories_flattened.yaml @@ -0,0 +1,54 @@ +interactions: +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - XXXXXXXXXX + connection: + - keep-alive + content-type: + - application/json + host: + - dev.lunchmoney.app + user-agent: + - lunchable/1.0.3 + method: GET + uri: https://dev.lunchmoney.app/v1/categories?format=flattened + response: + content: '{"categories":[{"id":658761,"name":"Another Another Test Category","description":null,"is_income":false,"exclude_from_budget":true,"exclude_from_totals":false,"updated_at":"2023-12-15T02:42:01.147Z","created_at":"2023-12-15T02:42:01.147Z","is_group":false,"group_id":658694,"archived":false,"archived_on":null,"order":null},{"id":443128,"name":"Groceries","description":"Test Category Description Updated","is_income":false,"exclude_from_budget":true,"exclude_from_totals":false,"updated_at":"2023-03-07T02:10:27.268Z","created_at":"2023-03-07T02:10:27.268Z","is_group":false,"group_id":658694,"archived":false,"archived_on":null,"order":null},{"id":443125,"name":"Home","description":null,"is_income":false,"exclude_from_budget":false,"exclude_from_totals":false,"updated_at":"2023-03-07T02:09:42.960Z","created_at":"2023-03-07T02:09:42.960Z","is_group":false,"group_id":null,"archived":false,"archived_on":null,"order":null},{"id":443129,"name":"Income","description":null,"is_income":true,"exclude_from_budget":false,"exclude_from_totals":false,"updated_at":"2023-03-07T02:40:15.216Z","created_at":"2023-03-07T02:40:15.216Z","is_group":false,"group_id":null,"archived":false,"archived_on":null,"order":null},{"id":443127,"name":"Personal Care","description":null,"is_income":false,"exclude_from_budget":false,"exclude_from_totals":false,"updated_at":"2023-03-07T02:10:17.225Z","created_at":"2023-03-07T02:10:17.225Z","is_group":false,"group_id":null,"archived":false,"archived_on":null,"order":null},{"id":443126,"name":"Shopping","description":null,"is_income":false,"exclude_from_budget":false,"exclude_from_totals":false,"updated_at":"2023-03-07T02:10:12.260Z","created_at":"2023-03-07T02:10:12.260Z","is_group":false,"group_id":680257,"archived":false,"archived_on":null,"order":null},{"id":660672,"name":"Splitwise","description":null,"is_income":false,"exclude_from_budget":false,"exclude_from_totals":false,"updated_at":"2023-12-15T20:45:04.146Z","created_at":"2023-12-15T20:45:04.146Z","is_group":false,"group_id":null,"archived":false,"archived_on":null,"order":0},{"id":680257,"name":"Sub Shopping","description":null,"is_income":false,"exclude_from_budget":false,"exclude_from_totals":false,"updated_at":"2023-12-22T05:45:15.233Z","created_at":"2023-12-22T05:45:15.233Z","is_group":true,"group_id":null,"archived":false,"archived_on":null,"order":null,"children":[{"id":443126,"name":"Shopping","description":null,"is_income":false,"exclude_from_budget":false,"exclude_from_totals":false,"updated_at":"2023-03-07T02:10:12.260Z","created_at":"2023-03-07T02:10:12.260Z","is_group":false,"group_id":680257,"archived":false,"archived_on":null,"order":null}]},{"id":658693,"name":"Test Category","description":"Test Category Description","is_income":false,"exclude_from_budget":true,"exclude_from_totals":false,"updated_at":"2023-12-15T02:32:23.352Z","created_at":"2023-12-15T02:32:23.352Z","is_group":false,"group_id":null,"archived":false,"archived_on":null,"order":null},{"id":658694,"name":"Test Category Group","description":"Test Category Group!!","is_income":false,"exclude_from_budget":true,"exclude_from_totals":false,"updated_at":"2023-12-15T02:32:24.849Z","created_at":"2023-12-15T02:32:24.849Z","is_group":true,"group_id":null,"archived":false,"archived_on":null,"order":null,"children":[{"id":658761,"name":"Another Another Test Category","description":null,"is_income":false,"exclude_from_budget":true,"exclude_from_totals":false,"updated_at":"2023-12-15T02:42:01.147Z","created_at":"2023-12-15T02:42:01.147Z","is_group":false,"group_id":658694,"archived":false,"archived_on":null,"order":null},{"id":443128,"name":"Groceries","description":"Test Category Description Updated","is_income":false,"exclude_from_budget":true,"exclude_from_totals":false,"updated_at":"2023-03-07T02:10:27.268Z","created_at":"2023-03-07T02:10:27.268Z","is_group":false,"group_id":658694,"archived":false,"archived_on":null,"order":null}]}]}' + headers: + Access-Control-Allow-Credentials: + - 'true' + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 22 Dec 2023 05:46:34 GMT + Etag: + - W/"f65-tCC2ArgL9RN/F4CiaAh1Lpc4NyA" + Nel: + - '{"report_to":"heroku-nel","max_age":3600,"success_fraction":0.005,"failure_fraction":0.05,"response_headers":["Via"]}' + Report-To: + - '{"group":"heroku-nel","max_age":3600,"endpoints":[{"url":"https://nel.heroku.com/reports?ts=1703223994&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=VcDy3EU92mcCuB%2Fhy80hehxszNKBE8gPpWAO5na%2BUTY%3D"}]}' + Reporting-Endpoints: + - heroku-nel=https://nel.heroku.com/reports?ts=1703223994&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=VcDy3EU92mcCuB%2Fhy80hehxszNKBE8gPpWAO5na%2BUTY%3D + Server: + - Cowboy + Transfer-Encoding: + - chunked + Vary: + - Origin, Accept-Encoding + Via: + - 1.1 vegur + X-Powered-By: + - Express + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/models/cassettes/test_get_categories_nested.yaml b/tests/models/cassettes/test_get_categories_nested.yaml new file mode 100644 index 0000000..b72b940 --- /dev/null +++ b/tests/models/cassettes/test_get_categories_nested.yaml @@ -0,0 +1,54 @@ +interactions: +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - XXXXXXXXXX + connection: + - keep-alive + content-type: + - application/json + host: + - dev.lunchmoney.app + user-agent: + - lunchable/1.0.3 + method: GET + uri: https://dev.lunchmoney.app/v1/categories?format=nested + response: + content: '{"categories":[{"id":660672,"name":"Splitwise","description":null,"is_income":false,"exclude_from_budget":false,"exclude_from_totals":false,"updated_at":"2023-12-15T20:45:04.146Z","created_at":"2023-12-15T20:45:04.146Z","is_group":false,"group_id":null,"archived":false,"archived_on":null,"order":0},{"id":443125,"name":"Home","description":null,"is_income":false,"exclude_from_budget":false,"exclude_from_totals":false,"updated_at":"2023-03-07T02:09:42.960Z","created_at":"2023-03-07T02:09:42.960Z","is_group":false,"group_id":null,"archived":false,"archived_on":null,"order":null},{"id":443129,"name":"Income","description":null,"is_income":true,"exclude_from_budget":false,"exclude_from_totals":false,"updated_at":"2023-03-07T02:40:15.216Z","created_at":"2023-03-07T02:40:15.216Z","is_group":false,"group_id":null,"archived":false,"archived_on":null,"order":null},{"id":443127,"name":"Personal Care","description":null,"is_income":false,"exclude_from_budget":false,"exclude_from_totals":false,"updated_at":"2023-03-07T02:10:17.225Z","created_at":"2023-03-07T02:10:17.225Z","is_group":false,"group_id":null,"archived":false,"archived_on":null,"order":null},{"id":680257,"name":"Sub Shopping","description":null,"is_income":false,"exclude_from_budget":false,"exclude_from_totals":false,"updated_at":"2023-12-22T05:45:15.233Z","created_at":"2023-12-22T05:45:15.233Z","is_group":true,"group_id":null,"archived":false,"archived_on":null,"order":null,"children":[{"id":443126,"name":"Shopping","description":null,"is_income":false,"exclude_from_budget":false,"exclude_from_totals":false,"updated_at":"2023-03-07T02:10:12.260Z","created_at":"2023-03-07T02:10:12.260Z","is_group":false,"group_id":680257,"archived":false,"archived_on":null,"order":null}]},{"id":658693,"name":"Test Category","description":"Test Category Description","is_income":false,"exclude_from_budget":true,"exclude_from_totals":false,"updated_at":"2023-12-15T02:32:23.352Z","created_at":"2023-12-15T02:32:23.352Z","is_group":false,"group_id":null,"archived":false,"archived_on":null,"order":null},{"id":658694,"name":"Test Category Group","description":"Test Category Group!!","is_income":false,"exclude_from_budget":true,"exclude_from_totals":false,"updated_at":"2023-12-15T02:32:24.849Z","created_at":"2023-12-15T02:32:24.849Z","is_group":true,"group_id":null,"archived":false,"archived_on":null,"order":null,"children":[{"id":658761,"name":"Another Another Test Category","description":null,"is_income":false,"exclude_from_budget":true,"exclude_from_totals":false,"updated_at":"2023-12-15T02:42:01.147Z","created_at":"2023-12-15T02:42:01.147Z","is_group":false,"group_id":658694,"archived":false,"archived_on":null,"order":null},{"id":443128,"name":"Groceries","description":"Test Category Description Updated","is_income":false,"exclude_from_budget":true,"exclude_from_totals":false,"updated_at":"2023-03-07T02:10:27.268Z","created_at":"2023-03-07T02:10:27.268Z","is_group":false,"group_id":658694,"archived":false,"archived_on":null,"order":null}]}]}' + headers: + Access-Control-Allow-Credentials: + - 'true' + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 22 Dec 2023 05:46:34 GMT + Etag: + - W/"bcf-cA7oyT1dg2Kx1YJwRekcvQ+hszA" + Nel: + - '{"report_to":"heroku-nel","max_age":3600,"success_fraction":0.005,"failure_fraction":0.05,"response_headers":["Via"]}' + Report-To: + - '{"group":"heroku-nel","max_age":3600,"endpoints":[{"url":"https://nel.heroku.com/reports?ts=1703223994&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=VcDy3EU92mcCuB%2Fhy80hehxszNKBE8gPpWAO5na%2BUTY%3D"}]}' + Reporting-Endpoints: + - heroku-nel=https://nel.heroku.com/reports?ts=1703223994&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=VcDy3EU92mcCuB%2Fhy80hehxszNKBE8gPpWAO5na%2BUTY%3D + Server: + - Cowboy + Transfer-Encoding: + - chunked + Vary: + - Origin, Accept-Encoding + Via: + - 1.1 vegur + X-Powered-By: + - Express + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/models/cassettes/test_get_recurring_expenses.yaml b/tests/models/cassettes/test_get_recurring_expenses.yaml index 0dad9f3..b28df47 100644 --- a/tests/models/cassettes/test_get_recurring_expenses.yaml +++ b/tests/models/cassettes/test_get_recurring_expenses.yaml @@ -1,21 +1,23 @@ interactions: - request: - body: + body: '' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Connection: - - keep-alive - Content-Type: - - application/json - User-Agent: - - python-requests/2.29.0 authorization: - XXXXXXXXXX + connection: + - keep-alive + content-type: + - application/json + host: + - dev.lunchmoney.app + user-agent: + - lunchable/1.0.3 method: GET - uri: https://dev.lunchmoney.app/v1/recurring_expenses?debit_as_negative=false&start_date=2022-11-01 + uri: https://dev.lunchmoney.app/v1/recurring_expenses?start_date=2022-11-01 response: content: '{"recurring_expenses":[{"id":585244,"start_date":"2021-12-01","end_date":null,"cadence":"monthly","payee":"Test Item","amount":"100.0000","currency":"usd","created_at":"2023-12-15T02:38:04.131Z","description":"Video Streaming","billing_date":"2022-11-01","type":"cleared","original_name":null,"source":"manual","plaid_account_id":null,"asset_id":null,"category_id":443126,"transaction_id":null}]}' headers: @@ -28,15 +30,15 @@ interactions: Content-Type: - application/json; charset=utf-8 Date: - - Fri, 15 Dec 2023 02:38:40 GMT + - Fri, 22 Dec 2023 15:42:08 GMT Etag: - W/"18c-Y2ukKiTxVJNnFDEIhjY0XlrmVgc" Nel: - '{"report_to":"heroku-nel","max_age":3600,"success_fraction":0.005,"failure_fraction":0.05,"response_headers":["Via"]}' Report-To: - - '{"group":"heroku-nel","max_age":3600,"endpoints":[{"url":"https://nel.heroku.com/reports?ts=1702607920&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=ODq7%2BtzP93fHx0jIG2mzN8v19hfSsKtMTc8%2F80zMDFM%3D"}]}' + - '{"group":"heroku-nel","max_age":3600,"endpoints":[{"url":"https://nel.heroku.com/reports?ts=1703259728&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=p8MMyvC2toZx7NhSpdaXza7UayhtI%2BrgAKK4o%2Bn1Vmo%3D"}]}' Reporting-Endpoints: - - heroku-nel=https://nel.heroku.com/reports?ts=1702607920&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=ODq7%2BtzP93fHx0jIG2mzN8v19hfSsKtMTc8%2F80zMDFM%3D + - heroku-nel=https://nel.heroku.com/reports?ts=1703259728&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=p8MMyvC2toZx7NhSpdaXza7UayhtI%2BrgAKK4o%2Bn1Vmo%3D Server: - Cowboy Vary: diff --git a/tests/models/cassettes/test_get_tags.yaml b/tests/models/cassettes/test_get_tags.yaml index c3b5c84..8f36362 100644 --- a/tests/models/cassettes/test_get_tags.yaml +++ b/tests/models/cassettes/test_get_tags.yaml @@ -1,19 +1,21 @@ interactions: - request: - body: + body: '' headers: - Accept: + accept: - '*/*' - Accept-Encoding: + accept-encoding: - gzip, deflate - Connection: - - keep-alive - Content-Type: - - application/json - User-Agent: - - python-requests/2.29.0 authorization: - XXXXXXXXXX + connection: + - keep-alive + content-type: + - application/json + host: + - dev.lunchmoney.app + user-agent: + - lunchable/1.0.3 method: GET uri: https://dev.lunchmoney.app/v1/tags response: @@ -28,15 +30,15 @@ interactions: Content-Type: - application/json; charset=utf-8 Date: - - Fri, 15 Dec 2023 02:36:18 GMT + - Fri, 22 Dec 2023 15:48:41 GMT Etag: - W/"40-46wK3k5iVXZWvLGgrDEZnekjOD0" Nel: - '{"report_to":"heroku-nel","max_age":3600,"success_fraction":0.005,"failure_fraction":0.05,"response_headers":["Via"]}' Report-To: - - '{"group":"heroku-nel","max_age":3600,"endpoints":[{"url":"https://nel.heroku.com/reports?ts=1702607778&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=%2Bn1LzGjK2z7WHGM%2BDE6zzJ9k%2BCfen4h0lips%2BrB%2FGHk%3D"}]}' + - '{"group":"heroku-nel","max_age":3600,"endpoints":[{"url":"https://nel.heroku.com/reports?ts=1703260121&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=Alw2L4p%2Fd6gFPGo1NnJ9A%2BPaXUGilV4ZtG%2B3wh7FCpE%3D"}]}' Reporting-Endpoints: - - heroku-nel=https://nel.heroku.com/reports?ts=1702607778&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=%2Bn1LzGjK2z7WHGM%2BDE6zzJ9k%2BCfen4h0lips%2BrB%2FGHk%3D + - heroku-nel=https://nel.heroku.com/reports?ts=1703260121&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=Alw2L4p%2Fd6gFPGo1NnJ9A%2BPaXUGilV4ZtG%2B3wh7FCpE%3D Server: - Cowboy Vary: diff --git a/tests/models/cassettes/test_trigger_fetch_from_plaid.yaml b/tests/models/cassettes/test_trigger_fetch_from_plaid.yaml new file mode 100644 index 0000000..5288ec0 --- /dev/null +++ b/tests/models/cassettes/test_trigger_fetch_from_plaid.yaml @@ -0,0 +1,54 @@ +interactions: +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + authorization: + - XXXXXXXXXX + connection: + - keep-alive + content-length: + - '0' + content-type: + - application/json + host: + - dev.lunchmoney.app + user-agent: + - lunchable/1.0.3 + method: POST + uri: https://dev.lunchmoney.app/v1/plaid_accounts/fetch + response: + content: 'true' + headers: + Access-Control-Allow-Credentials: + - 'true' + Connection: + - keep-alive + Content-Length: + - '4' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 22 Dec 2023 05:19:16 GMT + Etag: + - W/"4-X/5TO4MPCKAyY0ipFgr6/IraRNs" + Nel: + - '{"report_to":"heroku-nel","max_age":3600,"success_fraction":0.005,"failure_fraction":0.05,"response_headers":["Via"]}' + Report-To: + - '{"group":"heroku-nel","max_age":3600,"endpoints":[{"url":"https://nel.heroku.com/reports?ts=1703222356&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=6dOowV5gM%2BCZbs2abGbSVvPMS5g9g24RA5aQ7uyfidE%3D"}]}' + Reporting-Endpoints: + - heroku-nel=https://nel.heroku.com/reports?ts=1703222356&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=6dOowV5gM%2BCZbs2abGbSVvPMS5g9g24RA5aQ7uyfidE%3D + Server: + - Cowboy + Vary: + - Origin, Accept-Encoding + Via: + - 1.1 vegur + X-Powered-By: + - Express + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/models/test_categories.py b/tests/models/test_categories.py index 2a547b6..7eff3d3 100644 --- a/tests/models/test_categories.py +++ b/tests/models/test_categories.py @@ -107,3 +107,24 @@ def test_add_to_category_group(lunch_money_obj: LunchMoney): ) logger.info("Category Group ID # %s was just created: %s", category.id, name) assert isinstance(category, CategoriesObject) + + +@lunchable_cassette +def test_get_categories_nested(lunch_money_obj: LunchMoney): + """ + Get Categories and Assert that they're categories + """ + categories = lunch_money_obj.get_categories(format="nested") + categories_with_children = list(filter(lambda x: x.children, categories)) + assert len(categories_with_children) >= 1 + + +@lunchable_cassette +def test_get_categories_flattened(lunch_money_obj: LunchMoney): + """ + Get Categories and Assert that they're categories + """ + categories = lunch_money_obj.get_categories(format="flattened") + assert len(categories) >= 1 + for category in categories: + assert isinstance(category, CategoriesObject) diff --git a/tests/models/test_plaid_accounts.py b/tests/models/test_plaid_accounts.py index 501958e..b1846ff 100644 --- a/tests/models/test_plaid_accounts.py +++ b/tests/models/test_plaid_accounts.py @@ -21,3 +21,12 @@ def test_get_plaid_accounts(lunch_money_obj: LunchMoney): for plaid_account in plaid_accounts: assert isinstance(plaid_account, PlaidAccountObject) logger.info("%s Plaid Accounts returned", len(plaid_accounts)) + + +@lunchable_cassette +def test_trigger_fetch_from_plaid(lunch_money_obj: LunchMoney): + """ + Trigger Plaid Fetch + """ + plaid_fetch_request = lunch_money_obj.trigger_fetch_from_plaid() + assert plaid_fetch_request is True