From 2aeef1567dc6fc17e209b7b91c5c3125b3edff8a Mon Sep 17 00:00:00 2001 From: juftin Date: Thu, 25 Jan 2024 19:56:32 -0700 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20remove=20plugins=20from=20lu?= =?UTF-8?q?nchable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +- codecov.yaml | 3 - docs/{plugins/index.md => plugins.md} | 22 +- docs/plugins/primelunch.md | 109 -- docs/plugins/pushlunch.md | 51 - docs/plugins/splitlunch.md | 114 -- lunchable/_config/api_config.py | 2 +- lunchable/plugins/primelunch/README.md | 84 -- lunchable/plugins/primelunch/__init__.py | 3 - lunchable/plugins/primelunch/cli.py | 58 - lunchable/plugins/primelunch/primelunch.py | 440 ------ lunchable/plugins/pushlunch/README.md | 47 - lunchable/plugins/pushlunch/__init__.py | 3 - lunchable/plugins/pushlunch/cli.py | 39 - lunchable/plugins/pushlunch/pushover.py | 282 ---- lunchable/plugins/splitlunch/README.md | 43 - lunchable/plugins/splitlunch/__init__.py | 3 - lunchable/plugins/splitlunch/_config.py | 14 - lunchable/plugins/splitlunch/cli.py | 211 --- lunchable/plugins/splitlunch/exceptions.py | 11 - .../splitlunch/lunchmoney_splitwise.py | 1238 ----------------- lunchable/plugins/splitlunch/models.py | 29 - mkdocs.yaml | 6 +- pyproject.toml | 27 +- requirements.txt | 47 +- requirements/requirements-all.py3.10.txt | 49 +- requirements/requirements-all.py3.11.txt | 49 +- requirements/requirements-all.py3.12.txt | 49 +- requirements/requirements-all.py3.8.txt | 49 +- requirements/requirements-all.py3.9.txt | 49 +- requirements/requirements-docs.txt | 43 +- requirements/requirements-lint.txt | 2 +- requirements/requirements-test.txt | 43 +- tests/plugins/__init__.py | 3 - tests/plugins/pushlunch/__init__.py | 3 - .../cassettes/test_post_transaction.yaml | 216 --- .../cassettes/test_send_notification.yaml | 205 --- tests/plugins/pushlunch/test_pushlunch.py | 35 - tests/plugins/splitlunch/__init__.py | 3 - .../cassettes/test_update_balance.yaml | 296 ---- .../data/splitwise_non_involved_expense.json | 83 -- .../data/splitwise_non_involved_transfer.json | 83 -- .../data/splitwise_non_user_paid_expense.json | 101 -- .../splitwise_non_user_paid_transfer.json | 91 -- .../data/splitwise_user_paid_expense.json | 102 -- .../data/splitwise_user_paid_transfer.json | 83 -- tests/plugins/splitlunch/test_splitwise.py | 73 - 47 files changed, 335 insertions(+), 4275 deletions(-) delete mode 100644 codecov.yaml rename docs/{plugins/index.md => plugins.md} (88%) delete mode 100644 docs/plugins/primelunch.md delete mode 100644 docs/plugins/pushlunch.md delete mode 100644 docs/plugins/splitlunch.md delete mode 100644 lunchable/plugins/primelunch/README.md delete mode 100644 lunchable/plugins/primelunch/__init__.py delete mode 100644 lunchable/plugins/primelunch/cli.py delete mode 100644 lunchable/plugins/primelunch/primelunch.py delete mode 100644 lunchable/plugins/pushlunch/README.md delete mode 100644 lunchable/plugins/pushlunch/__init__.py delete mode 100644 lunchable/plugins/pushlunch/cli.py delete mode 100644 lunchable/plugins/pushlunch/pushover.py delete mode 100644 lunchable/plugins/splitlunch/README.md delete mode 100644 lunchable/plugins/splitlunch/__init__.py delete mode 100644 lunchable/plugins/splitlunch/_config.py delete mode 100644 lunchable/plugins/splitlunch/cli.py delete mode 100644 lunchable/plugins/splitlunch/exceptions.py delete mode 100644 lunchable/plugins/splitlunch/lunchmoney_splitwise.py delete mode 100644 lunchable/plugins/splitlunch/models.py delete mode 100644 tests/plugins/__init__.py delete mode 100644 tests/plugins/pushlunch/__init__.py delete mode 100644 tests/plugins/pushlunch/cassettes/test_post_transaction.yaml delete mode 100644 tests/plugins/pushlunch/cassettes/test_send_notification.yaml delete mode 100644 tests/plugins/pushlunch/test_pushlunch.py delete mode 100644 tests/plugins/splitlunch/__init__.py delete mode 100644 tests/plugins/splitlunch/cassettes/test_update_balance.yaml delete mode 100644 tests/plugins/splitlunch/data/splitwise_non_involved_expense.json delete mode 100644 tests/plugins/splitlunch/data/splitwise_non_involved_transfer.json delete mode 100644 tests/plugins/splitlunch/data/splitwise_non_user_paid_expense.json delete mode 100644 tests/plugins/splitlunch/data/splitwise_non_user_paid_transfer.json delete mode 100644 tests/plugins/splitlunch/data/splitwise_user_paid_expense.json delete mode 100644 tests/plugins/splitlunch/data/splitwise_user_paid_transfer.json delete mode 100644 tests/plugins/splitlunch/test_splitwise.py diff --git a/README.md b/README.md index 302a3f1a..63936d6c 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ **lunchable** is a Python Client for the [Lunch Money Developer API](https://lunchmoney.dev). It's built on top of [pydantic](https://github.com/pydantic/pydantic) and [httpx](https://github.com/encode/httpx/), it offers an _intuitive_ API, a _simple_ CLI, complete coverage of all endpoints, -and _plugins_ to other external services. +and a _plugin_ framework for extending the functionality of the library. ### Installation @@ -46,10 +46,20 @@ first_transaction: TransactionObject = transactions[0] transaction_as_dict: Dict[str, Any] = first_transaction.model_dump() ``` +### CLI + +To use the CLI, you'll need to set the `LUNCHMONEY_ACCESS_TOKEN` environment variable. +It's recommended to use [pipx](https://github.com/pypa/pipx) to install the CLI - +use the `lunchable[plugins]` extra to include all the known plugins: + +```shell +pipx install "lunchable[plugins]" +``` + ```shell export LUNCHMONEY_ACCESS_TOKEN="xxxxxxxxxxx" lunchable transactions get --limit 5 -lunchable http -X GET https://dev.lunchmoney.app/v1/assets +lunchable http -X GET v1/assets ``` diff --git a/codecov.yaml b/codecov.yaml deleted file mode 100644 index 9fdc10e4..00000000 --- a/codecov.yaml +++ /dev/null @@ -1,3 +0,0 @@ -ignore: -- lunchable/plugins/** -- tests/** diff --git a/docs/plugins/index.md b/docs/plugins.md similarity index 88% rename from docs/plugins/index.md rename to docs/plugins.md index ad635d48..0762f4bb 100644 --- a/docs/plugins/index.md +++ b/docs/plugins.md @@ -1,19 +1,21 @@ # Plugins -To install all the known plugins, and their dependencies, install +`lunchable` plugins are Python packages outside of the `lunchable` package that +can be installed into the same environment as `lunchable` to add additional +functionality to the CLI. To install all the known plugins, and their dependencies, install lunchable with the `plugins` extra: ```shell pipx install "lunchable[plugins]" ``` -lunchable supports CLI plugins with other external packages. See below for what's been built already. -If you can't find what you're looking for, consider building it yourself and opening a pull-request -to add it to the list below: +lunchable supports CLI plugins with other external packages. See below for what's been built +already. If you can't find what you're looking for, consider building it yourself and +opening a pull-request to add it to the list: -- [PushLunch](pushlunch.md): Push Notifications via Pushover -- [SplitLunch](splitlunch.md): Splitwise Integration -- [PrimeLunch](primelunch.md): Amazon Transaction Updater +- [PushLunch](https://github.com/juftin/lunchable-pushlunch): Push Notifications via Pushover +- [SplitLunch](https://github.com/juftin/lunchable-splitlunch): Splitwise Integration +- [PrimeLunch](https://github.com/juftin/lunchable-primelunch): Amazon Transaction Updater ## LunchableApp @@ -23,12 +25,12 @@ and more. Notice a few of the main attributes / methods of the `LunchableApp` cl attribute / method | description | type ------------------------------------------------------------------------------|--------------------------------------------------------------------------------|------------------------------------------------------- - **`lunch`** | The `LunchMoney` client | [LunchMoney](../interacting.md#lunchmoney) + **`lunch`** | The `LunchMoney` client | [LunchMoney](interacting.md#lunchmoney) **`data`** ¹ | The `LunchableData` object | [LunchableData](#lunchable.plugins.app.LunchableData) [refresh_data](#lunchable.plugins.LunchableApp.refresh_data) | Refresh all data (besides Transactions) | `method` [refresh_transactions](#lunchable.plugins.LunchableApp.refresh_transactions) | Refresh transactions, takes same parameters as `LunchMoney.get_transactions()` | `method` [refresh](#lunchable.plugins.LunchableApp.refresh) ² | Refresh the data for one particular model, takes **kwargs | `method` - [clear_transactions](#lunchable.plugins.LunchableApp.clear_transactions) ³ | Clear all transactions from the internal data | `method` + [clear_transactions](#lunchable.plugins.LunchableApp.clear_transactions) ³ | Clear all transactions from the internal data | `method` > ¹ This attribute contains all of the data that is loaded from LunchMoney. It has attributes > for `assets`, `categories`, `plaid_accounts`, `tags`, `transactions`, `crypto` and `user`. @@ -37,7 +39,7 @@ and more. Notice a few of the main attributes / methods of the `LunchableApp` cl > ² This method refreshes all of the data for one particular model. For example, > `refresh(AssetsObject)` will refresh the assets on the underling `data.assets` -> attribute and return a `dict[int, AssetsObject]` object. +> attribute and return a `dict[int, AssetsObject]` object. > ³ This the same as running `app.data.transactions.clear()` diff --git a/docs/plugins/primelunch.md b/docs/plugins/primelunch.md deleted file mode 100644 index 071b6511..00000000 --- a/docs/plugins/primelunch.md +++ /dev/null @@ -1,109 +0,0 @@ -# PrimeLunch: Amazon Transaction Updater - -
-

- lunchable - lunchable -

-
- ---- - -`PrimeLunch` is a command line tool that supports updating Amazon transaction notes with the items from -Amazon itself. This tool uses CSVs generated by the -[Amazon Order History Reporter](https://chrome.google.com/webstore/detail/amazon-order-history-repo/mgkilgclilajckgnedgjgnfdokkgnibi) -plugin on Chrome. Once you've gathered your transactions, export them as a CSV and scan them with the tool. -You'll be asked which transactions you'd like to update. - -
- -
- ---- - -The plugin uses the dollar amounts on the CSV export to match Amazon transactions in LunchMoney. -When a matching dollar amount is found, `PrimeLunch` compares the date window between the transactions -to determine if they're really a match. - -We're using -the [Amazon Order History Reporter](https://chrome.google.com/webstore/detail/amazon-order-history-repo/mgkilgclilajckgnedgjgnfdokkgnibi) -plugin because it gives us some functionality that Amazon doesn't: exporting Amazon transactions as they're -grouped on actual credit card transactions. - -## Run via the [Lunchable CLI](../cli.md#lunchable-cli) - -You can install lunchable with [pip](https://pypi.org/project/lunchable/) or -[pipx](https://pypa.github.io/pipx/): - -```shell -pipx install "lunchable[primelunch]" -``` - -```shell -pip install "lunchable[primelunch]" -``` - -The below command runs the `PrimeLunch` update tool: - -```shell -lunchable plugins primelunch run -f ~/Downloads/amazon_order_history.csv -``` - -> IMPORTANT: **Windows Users** -> -> The commands on this documentation correspond to running on a -> Mac or Linux Machine. If you are a Windows user take note of the following items: -> -> - Multiline commands in Windows machines use the `^` character instead of `\` to escape new lines -> - On macs, the default CSV file is located at `~/Downloads/amazon_order_history.csv`, on Windows this file -> is located someplace like `C:\Users\YourUserName\Downloads\amazon_order_history.csv` -> - My personal recommendation is to use the -> [Windows Terminal](https://apps.microsoft.com/store/detail/windows-terminal/9N0DX20HK701) -> along with -> [WSL](https://learn.microsoft.com/en-us/windows/wsl/) (Windows Subsystem for Linux) to -> access a Linux shell on your Windows machine. This will allow you to use the commands -> as written. - -The below command runs the `PrimeLunch` update tool using a date window of fourteen days -instead of the default seven days (these larger windows are especially useful for finding refunds and recurring -purchases): - -```shell -lunchable plugins primelunch run \ - --file ~/Downloads/amazon_order_history.csv \ - --window 14 -``` - -Update all transactions without going through the confirmation prompt for each one: - -```shell -lunchable plugins primelunch run \ - --file ~/Downloads/amazon_order_history.csv \ - --all -``` - -Provide a LunchMoney API access token manually (`PrimeLunch` defaults to inheriting from the `LUNCHMONEY_ACCESS_TOKEN` -environment variable): - -```shell -lunchable plugins primelunch run \ - --file ~/Downloads/amazon_order_history.csv \ - --token ABCDEFGHIJKLMNOP -``` - -## Command Line Documentation - -::: mkdocs-click - :module: lunchable.plugins.primelunch.cli - :command: run_primelunch - :prog_name: lunchable plugins primelunch run - :style: table - :list_subcommands: True - - -## References - -This lunchable plugin was inspired by the original Lunchable Amazon importer -at [samwelnella/amazon-transactions-to-lunchmoney](https://github.com/samwelnella/amazon-transactions-to-lunchmoney). diff --git a/docs/plugins/pushlunch.md b/docs/plugins/pushlunch.md deleted file mode 100644 index fe96821e..00000000 --- a/docs/plugins/pushlunch.md +++ /dev/null @@ -1,51 +0,0 @@ -# PushLunch: Push Notifications via Pushover - -
-

- lunchable - lunchable -

-
- ---- - -`PushLunch` supports Push Notifications via [Pushover](https://pushover.net). Pushover supports iOS -and Android Push notifications. To get started just provide your Pushover -`User Key` directly or via the `PUSHOVER_USER_KEY` environment variable. - -## Run via the Lunchable CLI - -The below command checks for un-reviewed transactions in the current period -and sends them as Push Notifications. The `--continuous` flag tells it to run -forever which will only send you a push notification once for each transaction. -By default it will check every 60 minutes, but this can be changed using the -`--interval` argument. - -```shell -lunchable plugins pushlunch notify --continuous -``` - -## Run via Docker - -```shell -docker run --rm \ - --env LUNCHMONEY_ACCESS_TOKEN=${LUNCHMONEY_ACCESS_TOKEN} \ - --env PUSHOVER_USER_KEY=${PUSHOVER_USER_KEY} \ - juftin/lunchable:latest \ - lunchable plugins pushlunch notify --continuous -``` - -## Run via Python - -```python -from lunchable.plugins.pushlunch import PushLunch -``` - -::: lunchable.plugins.pushlunch.pushover.PushLunch - handler: python - options: - show_bases: false - allow_inspection: true - heading_level: 3 diff --git a/docs/plugins/splitlunch.md b/docs/plugins/splitlunch.md deleted file mode 100644 index 654132e1..00000000 --- a/docs/plugins/splitlunch.md +++ /dev/null @@ -1,114 +0,0 @@ -# SplitLunch: Splitwise Integration - -
-

- lunchable - lunchable -

-
- ---- - -## Integrations - -This plugin supports different operations, and some of those operations have prerequisites: - -### Auto Importer - -It supports the auto-importing of Splitwise expenses into Lunch Money transactions. This requires -a manual asset exist in your Lunch Money account with "Splitwise" in the Name. Expenses that -have been deleted or which don't impact you (i.e. are only between other users in your group) -are skipped. By default, payments and expenses for which you are recorded as the payer are -skipped as well, but these can be overridden by the `--allow-payments` and `--allow-self-paid` -CLI flags, respectively. - -#### Prerequisites - -- Accounts: - - Splitwise must be in the account name - -### LunchMoney -> Splitwise - -It supports the creation of Splitwise transactions directly from synced Lunch Money accounts. This syncing requires you create a tag called `SplitLunchImport`. Transactions with this tag will be created in Splitwise with your "financial partner". Once transactions are created in Splitwise they will be split in half in Lunch Money. Half of the split will be marked in the `Reimbursement` category which must be created. - -#### Prerequisites - -- Financial Partners: - - If you only have one friend in Splitwise, this is your Financial Partner - - Financial Partners can be individual users or groups and transactions will be split accordingly - - Financial Partners must be specified by their Splitwise Group ID, Splitwise User ID, or Email Address -- Tags: - - `SplitLunchImport` -- Categories: - - `Reimbursement` - -### SplitLunch - -It supports a workflow where you mark transactions as split (identical to `Lunch Money -> Splitwise`) without importing them into Splitwise. This syncing requires you create a tag called `SplitLunch` and a category named `Reimbursement` - -#### Prerequisites - -- Tags: - - `SplitLunch` -- Categories: - - `Reimbursement` - -### LunchMoney -> Splitwise (without splitting) - -It supports the creation of Splitwise transactions directly from synced Lunch Money accounts. This syncing requires you create a tag called `SplitLunchDirectImport`. Transactions with this tag will be created in Splitwise with the total completely owed by your "financial partner". The entire transaction wil then be categorized as `Reimbursement` without being split. - -#### Prerequisites - -- Financial Partners: - - If you only have one friend in Splitwise, this is your Financial Partner - - Financial Partners can be individual users or groups and transactions will be split accordingly - - Financial Partners must be specified by their Splitwise Group ID, Splitwise User ID, or Email Address -- Tags: - - `SplitLunchDirectImport` -- Categories: - - `Reimbursement` - -> **Note:** Some of the above scenarios allow for tagging of a `Splitwise` tag on updated transactions. This tag must be created for this functionality to work. - -## Installation - -```shell -pip install lunchable[splitlunch] -``` - -## Run the SplitLunch plugin for the Lunchable CLI - -```shell -lunchable plugins splitlunch --help -``` - -## Run the SplitLunch plugin for the Lunchable CLI via Docker - -```shell -docker pull juftin/lunchable -``` - -```shell -docker run \ - --env LUNCHMONEY_ACCESS_TOKEN=${LUNCHMONEY_ACCESS_TOKEN} \ - --env SPLITWISE_CONSUMER_KEY=${SPLITWISE_CONSUMER_KEY} \ - --env SPLITWISE_CONSUMER_SECRET=${SPLITWISE_CONSUMER_SECRET} \ - --env SPLITWISE_API_KEY=${SPLITWISE_API_KEY} \ - juftin/lunchable:latest \ - lunchable plugins splitlunch --help -``` - -## Run via Python - -```python -from lunchable.plugins.splitlunch.lunchmoney_splitwise import SplitLunch -``` - -::: lunchable.plugins.splitlunch.lunchmoney_splitwise.SplitLunch - handler: python - options: - show_bases: false - allow_inspection: true - heading_level: 3 diff --git a/lunchable/_config/api_config.py b/lunchable/_config/api_config.py index c0c29db5..1fe52ce0 100644 --- a/lunchable/_config/api_config.py +++ b/lunchable/_config/api_config.py @@ -55,7 +55,7 @@ def get_access_token(access_token: Optional[str] = None) -> str: str """ if access_token is None: - logger.info( + logger.debug( "Loading Lunch Money Developer API Access token from environment" ) access_token = getenv(APIConfig._access_token_environment_variable, None) diff --git a/lunchable/plugins/primelunch/README.md b/lunchable/plugins/primelunch/README.md deleted file mode 100644 index 26908d39..00000000 --- a/lunchable/plugins/primelunch/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# PrimeLunch: Amazon Transaction Updater - -
-

- lunchable - lunchable -

-
- ---- - -`PrimeLunch` is a command line tool that supports updating Amazon transaction notes with the items from -Amazon itself. This tool uses CSVs generated by the -[Amazon Order History Reporter](https://chrome.google.com/webstore/detail/amazon-order-history-repo/mgkilgclilajckgnedgjgnfdokkgnibi) -plugin on Chrome. Once you've gathered your transactions, export them as a CSV and scan them with the tool. -You'll be asked which transactions you'd like to update. - -
- -
- ---- - -The plugin uses the dollar amounts on the CSV export to match Amazon transactions in LunchMoney. -When a matching dollar amount is found, `PrimeLunch` compares the date window between the transactions -to determine if they're really a match. - -We're using -the [Amazon Order History Reporter](https://chrome.google.com/webstore/detail/amazon-order-history-repo/mgkilgclilajckgnedgjgnfdokkgnibi) -plugin because it gives us some functionality that Amazon doesn't: exporting Amazon transactions as they're -grouped on actual credit card transactions. - -## Run via the [Lunchable CLI](cli.md#lunchable-cli) - -You can install lunchable with [pip](https://pypi.org/project/lunchable/) or -[pipx](https://pypa.github.io/pipx/): - -```shell -pipx install "lunchable[primelunch]" -``` - -```shell -pip install "lunchable[primelunch]" -``` - -The below command runs the `PrimeLunch` update tool: - -```shell -lunchable plugins primelunch run -f ~/Downloads/amazon_order_history.csv -``` - -The below command runs the `PrimeLunch` update tool using a date window of fourteen days -instead of the default seven days (these larger windows are especially useful for finding refunds and recurring -purchases): - -```shell -lunchable plugins primelunch run \ - --file ~/Downloads/amazon_order_history.csv \ - --window 14 -``` - -Update all transactions without going through the confirmation prompt for each one: - -```shell -lunchable plugins primelunch run \ - --file ~/Downloads/amazon_order_history.csv \ - --all -``` - -Provide a LunchMoney API access token manually (`PrimeLunch` defaults to inheriting from the `LUNCHMONEY_ACCESS_TOKEN` -environment variable): - -```shell -lunchable plugins primelunch run \ - --file ~/Downloads/amazon_order_history.csv \ - --token ABCDEFGHIJKLMNOP -``` - -## References - -This lunchable plugin was inspired by the original Lunchable Amazon importer -at [samwelnella/amazon-transactions-to-lunchmoney](https://github.com/samwelnella/amazon-transactions-to-lunchmoney). diff --git a/lunchable/plugins/primelunch/__init__.py b/lunchable/plugins/primelunch/__init__.py deleted file mode 100644 index 36ca9a5a..00000000 --- a/lunchable/plugins/primelunch/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -PrimeLunch Plugin -""" diff --git a/lunchable/plugins/primelunch/cli.py b/lunchable/plugins/primelunch/cli.py deleted file mode 100644 index ad6a750a..00000000 --- a/lunchable/plugins/primelunch/cli.py +++ /dev/null @@ -1,58 +0,0 @@ -import click - - -@click.group -def primelunch() -> None: - """ - PrimeLunch CLI - Syncing LunchMoney with Amazon - """ - - -@primelunch.command("run") -@click.option( - "-f", - "--file", - "csv_file", - type=click.Path(exists=True, resolve_path=True), - help="File Path of the Amazon Export", - required=True, -) -@click.option( - "-w", - "--window", - "window", - type=click.INT, - help="Allowable time window between Amazon transaction date and " - "credit card transaction date", - default=7, -) -@click.option( - "-a", - "--all", - "update_all", - is_flag=True, - type=click.BOOL, - help="Whether to skip the confirmation step and simply update all matched " - "transactions", - default=False, -) -@click.option( - "-t", - "--token", - "access_token", - type=click.STRING, - help="LunchMoney Access Token - defaults to the LUNCHMONEY_ACCESS_TOKEN environment variable", - envvar="LUNCHMONEY_ACCESS_TOKEN", -) -def run_primelunch( - csv_file: str, window: int, update_all: bool, access_token: str -) -> None: - """ - Run the PrimeLunch Update Process - """ - from lunchable.plugins.primelunch.primelunch import PrimeLunch - - primelunch = PrimeLunch( - file_path=csv_file, time_window=window, access_token=access_token - ) - primelunch.process_transactions(confirm=not update_all) diff --git a/lunchable/plugins/primelunch/primelunch.py b/lunchable/plugins/primelunch/primelunch.py deleted file mode 100644 index f12edd58..00000000 --- a/lunchable/plugins/primelunch/primelunch.py +++ /dev/null @@ -1,440 +0,0 @@ -""" -PrimeLunch Utils -""" - -from __future__ import annotations - -import datetime -import html -import logging -import os -import pathlib -from typing import Any, Iterable, List, Optional, Type, Union - -from rich import print, table -from rich.prompt import Confirm - -from lunchable._version import __application__ -from lunchable.exceptions import LunchMoneyImportError -from lunchable.models import ( - CategoriesObject, - LunchableModel, - TransactionObject, - TransactionUpdateObject, - UserObject, -) -from lunchable.plugins import LunchableApp, LunchableModelType - -try: - import numpy as np - import pandas as pd - from numpy import datetime64 -except ImportError as e: - msg = f'PrimeLunch requires the `primelunch` extras to be installed: `pip install "{__application__}[primelunch]"`' - raise LunchMoneyImportError(msg) from e - -logger = logging.getLogger(__name__) - - -class PrimeLunch(LunchableApp): - """ - PrimeLunch: Amazon Notes Updater - """ - - def __init__( - self, - file_path: Union[str, os.PathLike[str], pathlib.Path], - time_window: int = 7, - access_token: Optional[str] = None, - ) -> None: - """ - Initialize and set internal data - """ - super().__init__(access_token=access_token) - self.file_path = pathlib.Path(file_path) - self.time_window = time_window - - @staticmethod - def models_to_df(models: Iterable[LunchableModel]) -> pd.DataFrame: - """ - Convert Transactions Array to DataFrame - - Parameters - ---------- - models: List[LunchableModel] - - Returns - ------- - pd.DataFrame - """ - if not isinstance(models, list): - models = list(models) - return pd.DataFrame( - [item.model_dump() for item in models], - columns=models[0].model_fields.keys(), - ) - - @staticmethod - def df_to_models( - df: pd.DataFrame, model_type: Type[LunchableModelType] - ) -> List[LunchableModelType]: - """ - Convert DataFrame to Transaction Array - - Parameters - ---------- - df: pd.DataFrame - model_type: Type[LunchableModel] - - Returns - ------- - List[LunchableModel] - """ - array_df = df.copy() - array_df = array_df.fillna(np.NaN).replace([np.NaN], [None]) - model_array = array_df.to_dict(orient="records") - return [model_type.model_validate(item) for item in model_array] - - def amazon_to_df(self) -> pd.DataFrame: - """ - Read an Amazon Data File to a DataFrame - - This is pretty simple, except duplicate header rows need to be cleaned - - Returns - ------- - pd.DataFrame - """ - dt64: np.dtype[datetime64] = np.dtype("datetime64[ns]") - expected_columns = { - "order id": str, - "items": str, - "to": str, - "date": dt64, - "total": np.float64, - "shipping": np.float64, - "gift": np.float64, - "refund": np.float64, - "payments": str, - } - amazon_df = pd.read_csv( - self.file_path, - usecols=expected_columns.keys(), - ) - header_row_eval = pd.concat( - [amazon_df[item] == item for item in expected_columns.keys()], axis=1 - ).all(axis=1) - duplicate_header_rows = np.where(header_row_eval)[0] - amazon_df.drop(duplicate_header_rows, axis=0, inplace=True) - amazon_df["total"] = ( - amazon_df["total"].astype("string").str.replace(",", "").astype(np.float64) - ) - amazon_df = amazon_df.astype(dtype=expected_columns, copy=True, errors="raise") - logger.info("Amazon Data File loaded: %s", self.file_path) - return amazon_df - - @classmethod - def filter_amazon_transactions(cls, df: pd.DataFrame) -> pd.DataFrame: - """ - Filter a DataFrame to Amazon Transactions - - Parameters - ---------- - df: pd.DataFrame - - Returns - ------- - pd.DataFrame - """ - amazon_transactions = df.copy() - amazon_transactions["original_name"] = amazon_transactions[ - "original_name" - ].fillna("") - amazon_transactions = amazon_transactions[ - amazon_transactions.payee.str.match( - r"(?i)(Amazon|AMZN|Whole Foods)(\s?(Prime|Marketplace|MKTP)|\.\w+)?", - case=False, - ) - | amazon_transactions.original_name.str.match( - r"(?i)(Amazon|AMZN|Whole Foods)(\s?(Prime|Marketplace|MKTP)|\.\w+)?", - case=False, - ) - ] - return amazon_transactions - - @classmethod - def deduplicate_matched(cls, df: pd.DataFrame) -> pd.DataFrame: - """ - Deduplicate Multiple Connections Made - - Parameters - ---------- - df: pd.DataFrame - - Returns - ------- - pd.DataFrame - """ - deduped = df.copy() - deduped["duplicated"] = deduped.duplicated(subset=["id"], keep=False) - deduped = deduped[deduped["duplicated"] == False] # noqa:E712 - return deduped - - @classmethod - def _extract_total_from_payments(cls, df: pd.DataFrame) -> pd.DataFrame: - """ - Extract the Credit Card Payments from the payments column - - There is quite a bit of data manipulation going on here. We - need to extract meaningful credit card transaction info from strings like this: - - Visa ending in 9470: September 11, 2022: $29.57; \ - Visa ending in 9470: September 11, 2022: $2.22; - """ - extracted = df.copy() - extracted["new_total"] = extracted["payments"].str.rstrip(";").str.split(";") - exploded_totals = extracted.explode("new_total", ignore_index=True) - exploded_totals = exploded_totals[ - exploded_totals["new_total"].str.strip() != "" - ] - currency_matcher = r"(?:[\£\$\€]{1}[,\d]+.?\d*)" - exploded_totals["parsed_total"] = exploded_totals["new_total"].str.findall( - currency_matcher - ) - exploded_totals = exploded_totals.explode("parsed_total", ignore_index=True) - exploded_totals["parsed_total"] = exploded_totals["parsed_total"].str.replace( - "[^0-9.]", "", regex=True - ) - exploded_totals["parsed_total"] = exploded_totals["parsed_total"].astype( - np.float64 - ) - exploded_totals = exploded_totals[~exploded_totals["parsed_total"].isnull()] - exploded_totals["total"] = np.where( - ~exploded_totals["parsed_total"].isnull(), - exploded_totals["parsed_total"], - exploded_totals["total"], - ) - return exploded_totals - - @classmethod - def _extract_extra_from_orders(cls, df: pd.DataFrame) -> pd.DataFrame: - """ - Extract the Credit Card Refunds and Whole Foods Orders - """ - refunds = df.copy() - refunded_data = refunds[refunds["refund"] > 0].copy() - refunded_data["total"] = -refunded_data["refund"] - refunded_data["items"] = "REFUND: " + refunded_data["items"] - complete_amazon_data = pd.concat([refunds, refunded_data], ignore_index=True) - complete_amazon_data["items"] = np.where( - complete_amazon_data["to"].str.startswith("Whole Foods"), - "Whole Foods Groceries", - complete_amazon_data["items"], - ) - return complete_amazon_data - - @classmethod - def merge_transactions( - cls, amazon: pd.DataFrame, transactions: pd.DataFrame, time_range: int = 7 - ) -> pd.DataFrame: - """ - Merge Amazon Transactions and LunchMoney Transaction - - Parameters - ---------- - amazon: pd.DataFrame - transactions: pd.DataFrame - time_range: int - Number of days used to connect credit card transactions with - Amazon transactions - - Returns - ------- - pd.DataFrame - """ - exploded_totals = cls._extract_total_from_payments(df=amazon) - complete_amazon_data = cls._extract_extra_from_orders(df=exploded_totals) - merged_data = transactions.copy() - merged_data = merged_data.merge( - complete_amazon_data, - how="inner", - left_on=["amount"], - right_on=["total"], - suffixes=(None, "_amazon"), - ) - merged_data["start_date"] = merged_data["date_amazon"] - merged_data["end_date"] = merged_data["date_amazon"] + datetime.timedelta( - days=time_range - ) - merged_data.query( - "start_date <= date <= end_date", - inplace=True, - ) - merged_data["notes"] = merged_data["items"] - deduplicated = cls.deduplicate_matched(df=merged_data) - logger.info("%s Matching Amazon Transactions Identified", len(deduplicated)) - return deduplicated[TransactionObject.__fields__.keys()] - - def cache_transactions( - self, start_date: datetime.date, end_date: datetime.date - ) -> dict[int, TransactionObject]: - """ - Cache Transactions to Memory - - Parameters - ---------- - start_date : datetime.date - end_date : datetime.date - - Returns - ------- - Dict[int, TransactionObject] - """ - end_cache_date = end_date + datetime.timedelta(days=self.time_window) - logger.info( - "Fetching LunchMoney transactions between %s and %s", - start_date, - end_cache_date, - ) - self.refresh_data(models=[UserObject, CategoriesObject]) - self.refresh_transactions(start_date=start_date, end_date=end_cache_date) - logger.info( - 'Scanning LunchMoney Budget: "%s"', - html.unescape(self.data.user.budget_name), - ) - logger.info( - "%s transactions returned from LunchMoney", - len(self.data.transactions), - ) - return self.data.transactions - - def print_transaction( - self, transaction: TransactionObject, former_transaction: TransactionObject - ) -> None: - """ - Print a Transaction for interactive input - """ - transaction_table = table.Table(show_header=False) - notes_table = table.Table(show_header=False) - transaction_table.add_row("🛒 Transaction ID", str(former_transaction.id)) - transaction_table.add_row("🏦 Payee", former_transaction.payee) - transaction_table.add_row("📅 Date", str(former_transaction.date)) - transaction_table.add_row( - "💰 Amount", self.format_currency(amount=former_transaction.amount) - ) - if former_transaction.category_id is not None: - transaction_table.add_row( - "📊 Category", - self.data.categories[former_transaction.category_id].name, - ) - if ( - former_transaction.original_name is not None - and former_transaction.original_name != former_transaction.payee - ): - transaction_table.add_row( - "🏦 Original Payee", former_transaction.original_name - ) - if former_transaction.notes is not None: - transaction_table.add_row("📝 Notes", former_transaction.notes) - notes_table.add_row( - "🗒 Amazon Notes", - transaction.notes.strip(), # type: ignore[union-attr] - ) - print() - print(transaction_table) - print(notes_table) - - def update_transaction( - self, transaction: TransactionObject, confirm: bool = True - ) -> Optional[dict[str, Any]]: - """ - Update a Transaction's Notes if they've changed - - Parameters - ---------- - transaction: TransactionObject - confirm: bool - - Returns - ------- - Optional[Dict[str, Any]] - """ - former_transaction = self.data.transactions[transaction.id] - response = None - stripped_notes = transaction.notes.strip() # type: ignore[union-attr] - acceptable_length = min(349, len(stripped_notes)) - new_notes = stripped_notes[:acceptable_length] - if former_transaction.notes != new_notes: - confirmation = True - if confirm is True: - self.print_transaction( - transaction=transaction, former_transaction=former_transaction - ) - confirmation = Confirm.ask( - f"\t❓ Should we update transaction #{transaction.id}?" - ) - if confirmation is True: - response = self.lunch.update_transaction( - transaction_id=transaction.id, - transaction=TransactionUpdateObject(notes=new_notes), - ) - if confirm is True: - print(f"\t✅ Transaction #{transaction.id} updated") - return response - - def process_transactions(self, confirm: bool = True) -> None: - """ - Run the End-to-End Process - """ - logger.info( - "Beginning search to match Amazon and LunchMoney - using %s day window", - self.time_window, - ) - amazon_df = self.amazon_to_df() - min_date = amazon_df["date"].min().to_pydatetime().date() - max_date = amazon_df["date"].max().to_pydatetime().date() - logger.info( - "%s Amazon transactions loaded ranging from %s to %s", - len(amazon_df), - min_date, - max_date, - ) - self.cache_transactions(start_date=min_date, end_date=max_date) - transaction_df = self.models_to_df( - models=self.data.transactions.values(), - ) - amazon_transaction_df = self.filter_amazon_transactions(df=transaction_df) - merged_data = self.merge_transactions( - transactions=amazon_transaction_df, - amazon=amazon_df, - time_range=self.time_window, - ) - updated_transactions = self.df_to_models( - df=merged_data, model_type=TransactionObject - ) - responses = [] - for item in updated_transactions: - resp = self.update_transaction(transaction=item, confirm=confirm) - if resp is not None: - responses.append(resp) - logger.info("%s LunchMoney transactions updated", len(responses)) - - @staticmethod - def format_currency(amount: float) -> str: - """ - Format currency amounts to be pleasant and human readable - - Parameters - ---------- - amount: float - Float Amount to be converted into a string - - Returns - ------- - str - """ - if amount < 0: - float_string = f"[bold red]$ ({float(abs(amount)):,.2f})[/bold red]" - else: - float_string = f"[bold green]$ {float(amount):,.2f}[/bold green]" - return float_string diff --git a/lunchable/plugins/pushlunch/README.md b/lunchable/plugins/pushlunch/README.md deleted file mode 100644 index 2fd14624..00000000 --- a/lunchable/plugins/pushlunch/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# PushLunch - -## Lunch Money Push Notifications via Pushover - -
-

- lunchable - lunchable -

-
- -[![Lunchable Version](https://img.shields.io/pypi/v/lunchable?color=blue&label=lunchable)](https://github.com/juftin/lunchable) -[![PyPI](https://img.shields.io/pypi/pyversions/lunchable)](https://pypi.python.org/pypi/lunchable/) -[![Testing Status](https://github.com/juftin/lunchable/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/juftin/lunchable/actions/workflows/tests.yml?query=branch%3Amain) -[![GitHub License](https://img.shields.io/github/license/juftin/lunchable?color=blue&label=License)](https://github.com/juftin/lunchable/blob/main/LICENSE) -[![Documentation Status](https://readthedocs.org/projects/lunchable/badge/?version=latest)](https://lunchable.readthedocs.io/en/latest/?badge=latest) - -`PushLunch` supports Push Notifications via [Pushover](https://pushover.net). Pushover supports iOS -and Android Push notifications. To get started just provide your Pushover -`User Key` directly or via the `PUSHOVER_USER_KEY` environment variable. - -```shell -pip install lunchable -``` - -The below command checks for un-reviewed transactions in the current period and sends them as Push -Notifications. The `–-continuous` flag tells it to run forever which will only send you a push -notification once for each transaction. By default, it will check every 60 minutes - but this can be -changed using the `-–interval` argument. - -```shell -lunchable plugins pushlunch notify --continuous --user-key -``` - -### Run via Docker - -```shell -docker run --rm \ - --env LUNCHMONEY_ACCESS_TOKEN=${LUNCHMONEY_ACCESS_TOKEN} \ - --env PUSHOVER_USER_KEY=${PUSHOVER_USER_KEY} \ - juftin/lunchable:latest \ - lunchable plugins pushlunch notify --continuous -``` - -### More info on the [ReadTheDocs](https://lunchable.readthedocs.io/en/latest/pushlunch.html) diff --git a/lunchable/plugins/pushlunch/__init__.py b/lunchable/plugins/pushlunch/__init__.py deleted file mode 100644 index de419977..00000000 --- a/lunchable/plugins/pushlunch/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -PushLunch lunchable Plugin -""" diff --git a/lunchable/plugins/pushlunch/cli.py b/lunchable/plugins/pushlunch/cli.py deleted file mode 100644 index 3a009d5f..00000000 --- a/lunchable/plugins/pushlunch/cli.py +++ /dev/null @@ -1,39 +0,0 @@ -import click - - -@click.group -def pushlunch() -> None: - """ - Push Notifications for Lunch Money: PushLunch 📲 - """ - pass - - -@pushlunch.command("notify") -@click.option( - "--continuous", - is_flag=True, - help="Whether to continuously check for more uncleared transactions, " - "waiting a fixed amount in between checks.", -) -@click.option( - "--interval", - default=None, - help="Sleep Interval in Between Tries - only applies if `continuous` is set. " - "Defaults to 60 (minutes). Cannot be less than 5 (minutes)", -) -@click.option( - "--user-key", - default=None, - help="Pushover User Key. Defaults to `PUSHOVER_USER_KEY` env var", -) -def notify(continuous: bool, interval: int, user_key: str) -> None: - """ - Send a Notification for each Uncleared Transaction - """ - from lunchable.plugins.pushlunch.pushover import PushLunch - - push = PushLunch(user_key=user_key) - if interval is not None: - interval = int(interval) - push.notify_uncleared_transactions(continuous=continuous, interval=interval) diff --git a/lunchable/plugins/pushlunch/pushover.py b/lunchable/plugins/pushlunch/pushover.py deleted file mode 100644 index 0c521438..00000000 --- a/lunchable/plugins/pushlunch/pushover.py +++ /dev/null @@ -1,282 +0,0 @@ -""" -Pushover Notifications via lunchable -""" - -import logging -from base64 import b64decode -from json import loads -from os import getenv -from textwrap import dedent -from time import sleep -from typing import Any, Dict, List, Optional - -import httpx - -from lunchable.models import ( - AssetsObject, - CategoriesObject, - PlaidAccountObject, - TransactionObject, -) -from lunchable.plugins import LunchableApp - -logger = logging.getLogger(__name__) - - -class PushLunchError(Exception): - """ - PushLunch Exception - """ - - pass - - -class PushLunch(LunchableApp): - """ - Lunch Money Pushover Notifications via Lunchable - """ - - pushover_endpoint = "https://api.pushover.net/1/messages.json" - - def __init__( - self, - user_key: Optional[str] = None, - app_token: Optional[str] = None, - lunchmoney_access_token: Optional[str] = None, - ): - """ - Initialize - - Parameters - ---------- - user_key : Optional[str] - Pushover User Key. Will attempt to inherit from `PUSHOVER_USER_KEY` environment - variable if none defined - app_token: Optional[str] - Pushover app token, will attempt to inherit from `PUSHOVER_APP_TOKEN` environment - variable. If no token available, the official lunchable app token will be provided - lunchmoney_access_token: Optional[str] - LunchMoney Access Token. Will be inherited from `LUNCHMONEY_ACCESS_TOKEN` - environment variable. - """ - super().__init__(access_token=lunchmoney_access_token) - self.pushover_session = httpx.Client() - self.pushover_session.headers.update({"Content-Type": "application/json"}) - - _courtesy_token = b"YXpwMzZ6MjExcWV5OGFvOXNicWF0cmdraXc4aGVz" - if app_token is None: - app_token = getenv("PUSHOVER_APP_TOKEN", None) - token = app_token or b64decode(_courtesy_token).decode("utf-8") - user_key = user_key or getenv("PUSHOVER_USER_KEY", None) - if user_key in [None, ""]: - raise PushLunchError( - "You must provide a Pushover User Key or define it with " - "a `PUSHOVER_USER_KEY` environment variable" - ) - self._params = {"user": user_key, "token": token} - self.refresh_data(models=[AssetsObject, PlaidAccountObject, CategoriesObject]) - - self.notified_transactions: List[int] = [] - - def send_notification( - self, - message: str, - attachment: Optional[object] = None, - device: Optional[str] = None, - title: Optional[str] = None, - url: Optional[str] = None, - url_title: Optional[str] = None, - priority: Optional[int] = None, - sound: Optional[str] = None, - timestamp: Optional[str] = None, - html: bool = False, - ) -> httpx.Response: - """ - Send a Pushover Notification - - Parameters - ---------- - message: Optional[str] - your message - attachment: Optional[object] - an image attachment to send with the message; see attachments for more information - on how to upload files - device: Optional[str] - your user's device name to send the message directly to that device, rather than - all of the user's devices (multiple devices may be separated by a comma) - title: Optional[str] - your message's title, otherwise your app's name is used - url: Optional[str] - a supplementary URL to show with your message - url_title: Optional[str] - a title for your supplementary URL, otherwise just the URL is shown - priority: Optional[int] - send as -2 to generate no notification/alert, -1 to always send as a quiet - notification, 1 to display as high-priority and bypass the user's quiet hours, - or 2 to also require confirmation from the user - sound: Optional[str] - the name of one of the sounds supported by device clients to override the - user's default sound choice - timestamp: Optional[str] - a Unix timestamp of your message's date and time to display to the user, rather - than the time your message is received by our API - html: Union[None, 1] - Pass 1 if message contains HTML contents - - Returns - ------- - httpx.Response - """ - html_param = 1 if html not in [None, False] else None - params_dict = { - "message": message, - "attachment": attachment, - "device": device, - "title": title, - "url": url, - "url_title": url_title, - "priority": priority, - "sound": sound, - "timestamp": timestamp, - "html": html_param, - } - params: Dict[str, Any] = { - key: value for key, value in params_dict.items() if value is not None - } - params.update(self._params) - response = self.pushover_session.post(url=self.pushover_endpoint, params=params) - response.raise_for_status() - return response - - def post_transaction( - self, transaction: TransactionObject - ) -> Optional[Dict[str, Any]]: - """ - Post a Lunch Money Transaction as a Pushover Notification - - Assuming the instance of the - class hasn't already posted this particular notification - - Parameters - ---------- - transaction: TransactionObject - - Returns - ------- - Dict[str, Any] - """ - if transaction.id in self.notified_transactions: - return None - if transaction.category_id is None: - category = "N/A" - else: - category = self.data.categories[transaction.category_id].name - account_id = transaction.plaid_account_id or transaction.asset_id - assert account_id is not None - account = self.data.asset_map[account_id] - if isinstance(account, AssetsObject): - account_name = account.display_name or account.name - else: - account_name = account.name - transaction_formatted = dedent( - f""" - Payee: {transaction.payee} - Amount: {self._format_float(transaction.amount)} - Date: {transaction.date.strftime("%A %B %-d, %Y")} - Category: {category} - Account: {account_name} - """ - ).strip() - if transaction.currency is not None: - transaction_formatted += ( - f"\nCurrency: {transaction.currency.upper()}" - ) - if transaction.status is not None: - transaction_formatted += ( - f"\nStatus: {transaction.status.title()}" - ) - if transaction.notes is not None: - note = f"Notes: {transaction.notes}" - transaction_formatted += f"\n{note}" - if transaction.status == "uncleared": - url = ( - '' - "Uncleared Transactions from this Period" - ) - transaction_formatted += f"\n\n{url}" - - response = self.send_notification( - message=transaction_formatted, title="Lunch Money Transaction", html=True - ) - self.notified_transactions.append(transaction.id) - return loads(response.content) - - @classmethod - def _format_float(cls, amount: float) -> str: - """ - Format Floats to be pleasant and human readable - - Parameters - ---------- - amount: float - Float Amount to be converted into a string - - Returns - ------- - str - """ - if amount < 0: - float_string = f"$ ({float(amount):,.2f})".replace("-", "") - else: - float_string = f"$ {float(amount):,.2f}" - return float_string - - def notify_uncleared_transactions( - self, continuous: bool = False, interval: Optional[int] = None - ) -> List[TransactionObject]: - """ - Get the Current Period's Uncleared Transactions and Send a Notification for each - - Parameters - ---------- - continuous : bool - Whether to continuously check for more uncleared transactions, - waiting a fixed amount in between checks. - interval: Optional[int] - Sleep Interval in Between Tries - only applies if `continuous` is set. - Defaults to 60 (minutes). Cannot be less than 5 (minutes) - - Returns - ------- - List[TransactionObject] - """ - if interval is None: - interval = 60 - if continuous is True and interval < 5: - logger.warning( - "Check interval cannot be less than 5 minutes. Defaulting to 5." - ) - interval = 5 - if continuous is True: - logger.info("Continuous Notifications Enabled. Beginning PushLunch.") - - uncleared_transactions = [] - continuous_search = True - - while continuous_search is True: - found_transactions = len(self.notified_transactions) - uncleared_transactions += self.lunch.get_transactions(status="uncleared") - for transaction in uncleared_transactions: - self.post_transaction(transaction=transaction) - if continuous is True: - notified = len(self.notified_transactions) - new_transactions = notified - found_transactions - logger.info( - "%s new transactions pushed. %s total.", new_transactions, notified - ) - sleep(interval * 60) - else: - continuous_search = False - - return uncleared_transactions diff --git a/lunchable/plugins/splitlunch/README.md b/lunchable/plugins/splitlunch/README.md deleted file mode 100644 index 617412e4..00000000 --- a/lunchable/plugins/splitlunch/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# SplitLunch - -## A lunchable plugin to Splitwise - -
-

- lunchable - lunchable -

-
- -[![Lunchable Version](https://img.shields.io/pypi/v/lunchable?color=blue&label=lunchable)](https://github.com/juftin/lunchable) -[![PyPI](https://img.shields.io/pypi/pyversions/lunchable)](https://pypi.python.org/pypi/lunchable/) -[![Testing Status](https://github.com/juftin/lunchable/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/juftin/lunchable/actions/workflows/tests.yml?query=branch%3Amain) -[![GitHub License](https://img.shields.io/github/license/juftin/lunchable?color=blue&label=License)](https://github.com/juftin/lunchable/blob/main/LICENSE) -[![Documentation Status](https://readthedocs.org/projects/lunchable/badge/?version=latest)](https://lunchable.readthedocs.io/en/latest/?badge=latest) - -```shell -pip install lunchable[splitlunch] -``` - -```python -from lunchable.plugins.splitlunch.lunchmoney_splitwise import SplitLunch - -splitlunch = SplitLunch() -splitlunch.refresh_splitwise_transactions() -``` - -```shell -lunchable plugins splitlunch expenses --limit 5 -lunchable plugins splitlunch refresh -``` - -The goals of this plugin are to support a few things: - -1. Auto-Importing of Splitwise Transactions -2. Creation of Splitwise transactions directly from Lunch Money -3. Syncing of Splitwise Account Balance -4. A simple workflow to split transactions in half and mark half as reimbursed - -### More info on the [ReadTheDocs](https://lunchable.readthedocs.io/en/latest/splitlunch.html) diff --git a/lunchable/plugins/splitlunch/__init__.py b/lunchable/plugins/splitlunch/__init__.py deleted file mode 100644 index 29d0b8d7..00000000 --- a/lunchable/plugins/splitlunch/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Splitwise Plugin for Lunchmoney -""" diff --git a/lunchable/plugins/splitlunch/_config.py b/lunchable/plugins/splitlunch/_config.py deleted file mode 100644 index 9b624ca8..00000000 --- a/lunchable/plugins/splitlunch/_config.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -SplitLunch Configuration Helpers -""" - - -class SplitLunchConfig: - """ - Configuration Namespace for SplitLunch - """ - - splitlunch_tag: str = "SplitLunch" - splitwise_tag: str = "Splitwise" - splitlunch_import_tag: str = "SplitLunchImport" - splitlunch_direct_import_tag: str = "SplitLunchDirectImport" diff --git a/lunchable/plugins/splitlunch/cli.py b/lunchable/plugins/splitlunch/cli.py deleted file mode 100644 index 9a703a26..00000000 --- a/lunchable/plugins/splitlunch/cli.py +++ /dev/null @@ -1,211 +0,0 @@ -import datetime -import logging -from typing import Optional, Union - -import click -from pydantic_core import to_jsonable_python -from rich import print_json - -logger = logging.getLogger(__name__) - - -@click.group -def splitlunch() -> None: - """ - Splitwise Plugin for lunchable, SplitLunch 💲🍱 - """ - pass - - -dated_after = click.option( - "--dated-after", - default=None, - help="ISO 8601 Date time. Return expenses later that this date", -) -dated_before = click.option( - "--dated-before", - default=None, - help="ISO 8601 Date time. Return expenses earlier than this date", -) - - -@splitlunch.command("expenses") -@click.option( - "--limit", default=None, help="Limit the amount of Results. 0 returns everything." -) -@click.option("--offset", default=None, help="Number of expenses to be skipped") -@click.option("--limit", default=None, help="Number of expenses to be returned") -@click.option("--group-id", default=None, help="GroupID of the expenses") -@click.option("--friendship-id", default=None, help="FriendshipID of the expenses") -@dated_after -@dated_before -@click.option( - "--updated-after", - default=None, - help="ISO 8601 Date time. Return expenses updated after this date", -) -@click.option( - "--updated-before", - default=None, - help="ISO 8601 Date time. Return expenses updated before this date", -) -def splitlunch_expenses(**kwargs: Union[int, str, bool]) -> None: - """ - Retrieve Splitwise Expenses - """ - from lunchable.plugins.splitlunch.lunchmoney_splitwise import SplitLunch - - splitlunch = SplitLunch() - if set(kwargs.values()) == {None}: - kwargs["limit"] = 5 - expenses = splitlunch.get_expenses(**kwargs) # type: ignore[arg-type] - json_data = to_jsonable_python(expenses) - print_json(data=json_data) - - -tag_transactions = click.option( - "--tag-transactions", - is_flag=True, - help="Tag the resulting transactions with a `Splitwise` tag.", -) -financial_partner_id = click.option( - "--financial-partner-id", - default=None, - type=click.INT, - help="Splitwise ID of your financial partner.", -) -financial_partner_email = click.option( - "--financial-partner-email", - default=None, - help="Splitwise Email Address of your financial partner.", -) -financial_partner_group_id = click.option( - "--financial-partner-group-id", - default=None, - type=click.INT, - help="Splitwise Group ID for financial partner transactions.", -) - - -@splitlunch.command("splitlunch") -@tag_transactions -def make_splitlunch(**kwargs: Union[int, str, bool]) -> None: - """ - Split all `SplitLunch` tagged transactions in half. - - One of these new splits will be recategorized to `Reimbursement`. - """ - from lunchable.plugins.splitlunch.lunchmoney_splitwise import SplitLunch - - splitlunch = SplitLunch() - results = splitlunch.make_splitlunch(**kwargs) # type: ignore[arg-type] - json_data = to_jsonable_python(results) - print_json(data=json_data) - - -@splitlunch.command("splitlunch-import") -@tag_transactions -@financial_partner_id -@financial_partner_email -@financial_partner_group_id -def make_splitlunch_import(**kwargs: Union[int, str, bool]) -> None: - """ - Import `SplitLunchImport` tagged transactions to Splitwise and Split them in Lunch Money - - Send a transaction to Splitwise and then split the original transaction in Lunch Money. - One of these new splits will be recategorized to `Reimbursement`. Any tags will be - reapplied. - """ - from lunchable.plugins.splitlunch.lunchmoney_splitwise import SplitLunch - - financial_partner_id: Optional[int] = kwargs.pop("financial_partner_id") # type: ignore[assignment] - financial_partner_email: Optional[str] = kwargs.pop("financial_partner_email") # type: ignore[assignment] - financial_partner_group_id: Optional[int] = kwargs.pop("financial_partner_group_id") # type: ignore[assignment] - splitlunch = SplitLunch( - financial_partner_id=financial_partner_id, - financial_partner_email=financial_partner_email, - financial_partner_group_id=financial_partner_group_id, - ) - results = splitlunch.make_splitlunch_import(**kwargs) # type: ignore[arg-type] - json_data = to_jsonable_python(results) - print_json(data=json_data) - - -@splitlunch.command("splitlunch-direct-import") -@tag_transactions -@financial_partner_id -@financial_partner_email -@financial_partner_group_id -def make_splitlunch_direct_import(**kwargs: Union[int, str, bool]) -> None: - """ - Import `SplitLunchDirectImport` tagged transactions to Splitwise and Split them in Lunch Money - - Send a transaction to Splitwise and then split the original transaction in Lunch Money. - One of these new splits will be recategorized to `Reimbursement`. Any tags will be - reapplied. - """ - from lunchable.plugins.splitlunch.lunchmoney_splitwise import SplitLunch - - financial_partner_id: Optional[int] = kwargs.pop("financial_partner_id") # type: ignore[assignment] - financial_partner_email: Optional[str] = kwargs.pop("financial_partner_email") # type: ignore[assignment] - financial_partner_group_id: Optional[int] = kwargs.pop("financial_partner_group_id") # type: ignore[assignment] - splitlunch = SplitLunch( - financial_partner_id=financial_partner_id, - financial_partner_email=financial_partner_email, - financial_partner_group_id=financial_partner_group_id, - ) - results = splitlunch.make_splitlunch_direct_import(**kwargs) # type: ignore[arg-type] - json_data = to_jsonable_python(results) - print_json(data=json_data) - - -@splitlunch.command("update-balance") -def update_splitwise_balance() -> None: - """ - Update the Splitwise Asset Balance - """ - from lunchable.plugins.splitlunch.lunchmoney_splitwise import SplitLunch - - splitlunch = SplitLunch() - updated_asset = splitlunch.update_splitwise_balance() - json_data = to_jsonable_python(updated_asset) - print_json(data=json_data) - - -@splitlunch.command("refresh") -@dated_after -@dated_before -@click.option( - "--allow-self-paid/--no-allow-self-paid", - default=False, - help="Allow self-paid expenses to be imported (filtered out by default).", -) -@click.option( - "--allow-payments/--no-allow-payments", - default=False, - help="Allow payments to be imported (filtered out by default).", -) -def refresh_splitwise_transactions( - dated_before: Optional[datetime.datetime], - dated_after: Optional[datetime.datetime], - allow_self_paid: bool, - allow_payments: bool, -) -> None: - """ - Import New Splitwise Transactions to Lunch Money and - - This function gets all transactions from Splitwise, all transactions from - your Lunch Money Splitwise account and compares the two. This also updates - the account balance. - """ - import lunchable.plugins.splitlunch.lunchmoney_splitwise - - splitlunch = lunchable.plugins.splitlunch.lunchmoney_splitwise.SplitLunch() - response = splitlunch.refresh_splitwise_transactions( - dated_before=dated_before, - dated_after=dated_after, - allow_self_paid=allow_self_paid, - allow_payments=allow_payments, - ) - json_data = to_jsonable_python(response) - print_json(data=json_data) diff --git a/lunchable/plugins/splitlunch/exceptions.py b/lunchable/plugins/splitlunch/exceptions.py deleted file mode 100644 index f0be0775..00000000 --- a/lunchable/plugins/splitlunch/exceptions.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -SplitLunch Exceptions -""" - -from lunchable import LunchMoneyError - - -class SplitLunchError(LunchMoneyError): - """ - Split Lunch Errors - """ diff --git a/lunchable/plugins/splitlunch/lunchmoney_splitwise.py b/lunchable/plugins/splitlunch/lunchmoney_splitwise.py deleted file mode 100644 index ddfeff7a..00000000 --- a/lunchable/plugins/splitlunch/lunchmoney_splitwise.py +++ /dev/null @@ -1,1238 +0,0 @@ -""" -Lunchable Plugin for Splitwise -""" - -import datetime -import logging -from math import floor -from os import getenv -from random import shuffle -from textwrap import dedent -from typing import Any, Dict, List, Optional, Tuple, Union - -from lunchable import LunchMoney, __application__ -from lunchable.exceptions import LunchMoneyImportError -from lunchable.models import ( - AssetsObject, - CategoriesObject, - TagsObject, - TransactionInsertObject, - TransactionObject, - TransactionSplitObject, - TransactionUpdateObject, -) -from lunchable.plugins.splitlunch._config import SplitLunchConfig -from lunchable.plugins.splitlunch.exceptions import SplitLunchError -from lunchable.plugins.splitlunch.models import SplitLunchExpense - -logger = logging.getLogger(__name__) - -try: - import splitwise - from dateutil.tz import tzlocal -except ImportError as ie: - logger.exception(ie) - _pip_extra_error = ( - "Looks like you don't have the Splitwise plugin installed: " - f"`pip install {__application__}[splitlunch]`" - ) - raise LunchMoneyImportError(_pip_extra_error) from ie - - -def _get_splitwise_impact( - expense: splitwise.Expense, current_user_id: int -) -> Tuple[float, bool]: - """ - Get the Financial Impact of a Splitwise Transaction - - Parameters - ---------- - expense: splitwise.Expense - - Returns - ------- - Tuple[float, bool] - """ - financial_impact = 0.00 - self_paid = False - if len(expense.repayments) >= 1: - for debt in expense.repayments: - if debt.fromUser == current_user_id: - financial_impact += float(debt.amount) - elif debt.toUser == current_user_id: - self_paid = True - financial_impact -= float(debt.amount) - elif len(expense.repayments) == 0: - assert len(expense.users) == 1 - if expense.users[0].id == current_user_id: - self_paid = True - return financial_impact, self_paid - - -class SplitLunch(splitwise.Splitwise): - """ - Lunchable Plugin For Interacting With Splitwise - """ - - def __init__( - self, - lunch_money_access_token: Optional[str] = None, - financial_partner_id: Optional[int] = None, - financial_partner_email: Optional[str] = None, - financial_partner_group_id: Optional[int] = None, - consumer_key: Optional[str] = None, - consumer_secret: Optional[str] = None, - api_key: Optional[str] = None, - lunchable_client: Optional[LunchMoney] = None, - ): - """ - Initialize the Parent Class with some additional properties - - Parameters - ---------- - financial_partner_id: Optional[int] - Splitwise User ID of financial partner - financial_partner_email: Optional[str] - Splitwise linked email address of financial partner - financial_partner_group_id: Optional[int] - Splitwise Group ID for financial partner transactions - consumer_key: Optional[str] - Consumer Key provided by Splitwise. Defaults to `SPLITWISE_CONSUMER_KEY` environment - variable - consumer_secret: Optional[str] - Consumer Key provided by Splitwise. Defaults to `SPLITWISE_CONSUMER_SECRET` - environment variable - api_key: Optional[str] - Consumer Key provided by Splitwise. Defaults to `SPLITWISE_API_KEY` environment - variable. - lunch_money_access_token: Optional[str] - Lunch Money Access Token. Will be inherited from `LUNCHMONEY_ACCESS_TOKEN` - environment variable if not provided. - lunchable_client: LunchMoney - Instantiated LunchMoney object to use as internal client. One will - be created using environment variables otherwise. - """ - init_kwargs = self._get_splitwise_init_kwargs( - consumer_key=consumer_key, consumer_secret=consumer_secret, api_key=api_key - ) - super(SplitLunch, self).__init__(**init_kwargs) - self.current_user: splitwise.CurrentUser = self.getCurrentUser() - self.financial_partner: splitwise.Friend = self.get_friend( - friend_id=financial_partner_id, email_address=financial_partner_email - ) - self.financial_group = financial_partner_group_id - self.last_check: Optional[datetime.datetime] = None - self.lunchable = ( - LunchMoney(access_token=lunch_money_access_token) - if lunchable_client is None - else lunchable_client - ) - self._none_tag = TagsObject(id=0, name="SplitLunchPlaceholder") - self.splitwise_tag = self._none_tag.model_copy() - self.splitlunch_tag = self._none_tag.model_copy() - self.splitlunch_import_tag = self._none_tag.model_copy() - self.splitlunch_direct_import_tag = self._none_tag.model_copy() - self._get_splitwise_tags() - self.earliest_start_date = datetime.date(1812, 1, 1) - today = datetime.date.today() - self.latest_end_date = datetime.date(today.year + 10, 12, 31) - self.splitwise_asset = self._get_splitwise_asset() - self.reimbursement_category = self._get_reimbursement_category() - - def __repr__(self) -> str: - """ - String Representation - - Returns - ------- - str - """ - return f"" - - @classmethod - def _split_amount(cls, amount: float, splits: int) -> Tuple[float, ...]: - """ - Split a money amount into fair shares - - Parameters - ---------- - amount: float - splits: int - - Returns - ------- - Tuple[float] - """ - try: - assert amount == round(amount, 2) - except AssertionError as ae: - raise SplitLunchError( - f"{amount} caused an error, you must provide a real " "spending amount." - ) from ae - equal_shares = round(amount, 2) / splits - remainder_dollars = floor(equal_shares) - remainder_cents = floor((equal_shares - remainder_dollars) * 100) / 100 - remainder_left = round( - (equal_shares - remainder_dollars - remainder_cents) * splits * 100, 0 - ) - owed_amount = remainder_dollars + remainder_cents - return_amounts = [owed_amount for _ in range(splits)] - for i in range(int(remainder_left)): - return_amounts[i] += 0.010 - shuffle(return_amounts) - return tuple([round(item, 2) for item in return_amounts]) - - @classmethod - def split_a_transaction(cls, amount: Union[float, int]) -> Tuple[float, ...]: - """ - Split a Transaction into Two - - Split a bill into a tuple of two amounts (and take care - of the extra penny if needed) - - Parameters - ---------- - amount: A Currency amount (no more precise than cents) - - Returns - ------- - tuple - A tuple is returned with each participant's amount - """ - amounts_due = cls._split_amount(amount=amount, splits=2) - return amounts_due - - def create_self_paid_expense( - self, amount: float, description: str, date: datetime.date - ) -> SplitLunchExpense: - """ - Create and Submit a Splitwise Expense - - Parameters - ---------- - amount: float - Transaction Amount - description: str - Transaction Description - - Returns - ------- - Expense - """ - # CREATE THE NEW EXPENSE OBJECT - new_expense = splitwise.Expense() - new_expense.setDescription(desc=description) - new_expense.setDate(date=date) - if self.financial_group: - new_expense.setGroupId(self.financial_group) - # GET AND SET AMOUNTS OWED - primary_user_owes, financial_partner_owes = self.split_a_transaction( - amount=amount - ) - new_expense.setCost(cost=amount) - # CONFIGURE PRIMARY USER - primary_user = splitwise.user.ExpenseUser() - primary_user.setId(id=self.current_user.id) - primary_user.setPaidShare(paid_share=amount) - primary_user.setOwedShare(owed_share=primary_user_owes) - # CONFIGURE SECONDARY USER - financial_partner = splitwise.user.ExpenseUser() - financial_partner.setId(id=self.financial_partner.id) - financial_partner.setPaidShare(paid_share=0.00) - financial_partner.setOwedShare(owed_share=financial_partner_owes) - # ADD USERS AND REPAYMENTS TO EXPENSE - new_expense.addUser(user=primary_user) - new_expense.addUser(user=financial_partner) - # SUBMIT THE EXPENSE AND GET THE RESPONSE - expense_response: splitwise.Expense - expense_response, expense_errors = self.createExpense(expense=new_expense) - try: - assert expense_errors is None - except AssertionError as ae: - raise SplitLunchError(expense_errors["base"][0]) from ae - logger.info("Expense Created: %s", expense_response.id) - message = f"Created via SplitLunch: {datetime.datetime.now()}" - self.createComment(expense_id=expense_response.id, content=message) - pydantic_response = self.splitwise_to_pydantic(expense=expense_response) - return pydantic_response - - def create_expense_on_behalf_of_partner( - self, amount: float, description: str, date: datetime.date - ) -> SplitLunchExpense: - """ - Create and Submit a Splitwise Expense on behalf of your financial partner. - - This expense will be completely owed by the partner and maked as reimbursed. - - Parameters - ---------- - amount: float - Transaction Amount - description: str - Transaction Description - - Returns - ------- - Expense - """ - # CREATE THE NEW EXPENSE OBJECT - new_expense = splitwise.Expense() - new_expense.setDescription(desc=description) - new_expense.setDate(date=date) - if self.financial_group: - new_expense.setGroupId(self.financial_group) - # GET AND SET AMOUNTS OWED - new_expense.setCost(cost=amount) - # CONFIGURE PRIMARY USER - primary_user = splitwise.user.ExpenseUser() - primary_user.setId(id=self.current_user.id) - primary_user.setPaidShare(paid_share=amount) - primary_user.setOwedShare(owed_share=0.00) - # CONFIGURE SECONDARY USER - financial_partner = splitwise.user.ExpenseUser() - financial_partner.setId(id=self.financial_partner.id) - financial_partner.setPaidShare(paid_share=0.00) - financial_partner.setOwedShare(owed_share=amount) - # ADD USERS AND REPAYMENTS TO EXPENSE - new_expense.addUser(user=primary_user) - new_expense.addUser(user=financial_partner) - # SUBMIT THE EXPENSE AND GET THE RESPONSE - expense_response: splitwise.Expense - expense_response, expense_errors = self.createExpense(expense=new_expense) - try: - assert expense_errors is None - except AssertionError as ae: - raise SplitLunchError(expense_errors["base"][0]) from ae - logger.info("Expense Created: %s", expense_response.id) - message = f"Created via SplitLunch: {datetime.datetime.now()}" - self.createComment(expense_id=expense_response.id, content=message) - pydantic_response = self.splitwise_to_pydantic(expense=expense_response) - return pydantic_response - - def get_friend( - self, email_address: Optional[str] = None, friend_id: Optional[int] = None - ) -> Optional[splitwise.Friend]: - """ - Retrieve a Financial Partner by Email Address - - Parameters - ---------- - email_address: str - Email Address of Friend's user in Splitwise - friend_id: Optional[int] - Splitwise friend ID. Notice the friend ID in the following - URL: https://secure.splitwise.com/#/friends/12345678 - - Returns - ------- - Optional[splitwise.Friend] - """ - friend_list: List[splitwise.Friend] = self.getFriends() - if len(friend_list) == 1: - return friend_list[0] - for friend in friend_list: - if friend_id is not None and friend.id == friend_id: - return friend - elif ( - email_address is not None - and friend.email.lower() == email_address.lower() - ): - return friend - return None - - def get_expenses( - self, - offset: Optional[int] = None, - limit: Optional[int] = None, - group_id: Optional[int] = None, - friendship_id: Optional[int] = None, - dated_after: Optional[datetime.datetime] = None, - dated_before: Optional[datetime.datetime] = None, - updated_after: Optional[datetime.datetime] = None, - updated_before: Optional[datetime.datetime] = None, - ) -> List[SplitLunchExpense]: - """ - Get Splitwise Expenses - - Parameters - ---------- - offset: Optional[int] - Number of expenses to be skipped - limit: Optional[int] - Number of expenses to be returned - group_id: Optional[int] - GroupID of the expenses - friendship_id: Optional[int] - FriendshipID of the expenses - dated_after: Optional[datetime.datetime] - ISO 8601 Date time. Return expenses later that this date - dated_before: Optional[datetime.datetime] - ISO 8601 Date time. Return expenses earlier than this date - updated_after: Optional[datetime.datetime] - ISO 8601 Date time. Return expenses updated after this date - updated_before: Optional[datetime.datetime] - ISO 8601 Date time. Return expenses updated before this date - - Returns - ------- - List[SplitLunchExpense] - """ - expenses = self.getExpenses( - offset=offset, - limit=limit, - group_id=group_id, - friendship_id=friendship_id, - dated_after=dated_after, - dated_before=dated_before, - updated_after=updated_after, - updated_before=updated_before, - ) - pydantic_expenses = [ - self.splitwise_to_pydantic(expense) for expense in expenses - ] - return pydantic_expenses - - @classmethod - def _get_splitwise_init_kwargs( - cls, - consumer_key: Optional[str] = None, - consumer_secret: Optional[str] = None, - api_key: Optional[str] = None, - ) -> Dict[str, Any]: - """ - Get the Splitwise Kwargs - - Parameters - ---------- - consumer_key: Optional[str] - consumer_secret: Optional[str] - api_key: Optional[str] - """ - if consumer_key is None: - consumer_key = getenv("SPLITWISE_CONSUMER_KEY") - if consumer_secret is None: - consumer_secret = getenv("SPLITWISE_CONSUMER_SECRET") - if api_key is None: - api_key = getenv("SPLITWISE_API_KEY", None) - init_kwargs = { - "consumer_key": consumer_key, - "consumer_secret": consumer_secret, - "api_key": api_key, - } - if consumer_key is None or consumer_secret is None or api_key is None: - error_message = ( - dedent( - """ - You must set your Splitwise credentials explicitly or by assigning - the `SPLITWISE_CONSUMER_KEY`, `SPLITWISE_CONSUMER_SECRET`, and the - `SPLITWISE_API_KEY`environment variables - """ - ) - .replace("\n", " ") - .replace(" ", " ") - ) - logger.error(error_message) - raise SplitLunchError(error_message) - return init_kwargs - - def splitwise_to_pydantic(self, expense: splitwise.Expense) -> SplitLunchExpense: - """ - Convert Splitwise Object to Pydantic - - Parameters - ---------- - expense: splitwise.Expense - - Returns - ------- - SplitLunchExpense - """ - financial_impact, self_paid = _get_splitwise_impact( - expense=expense, current_user_id=self.current_user.id - ) - expense = SplitLunchExpense( - splitwise_id=expense.id, - original_amount=expense.cost, - financial_impact=financial_impact, - self_paid=self_paid, - description=expense.description, - category=expense.category.name, - details=expense.details, - payment=expense.payment, - date=expense.date, - users=[user.id for user in expense.users], - created_at=expense.created_at, - updated_at=expense.updated_at, - deleted_at=expense.deleted_at, - deleted=True if expense.deleted_at is not None else False, - ) - return expense - - def _get_splitwise_asset(self) -> Optional[AssetsObject]: - """ - Get the Splitwise asset - - Parse a user's Lunch Money accounts and return the manually managed - Splitwise account asset object - - Returns - ------- - AssetsObject - """ - assets = self.lunchable.get_assets() - splitwise_assets = [] - for asset in assets: - if ( - asset.institution_name is not None - and "splitwise" in asset.institution_name.lower() - ): - splitwise_assets.append(asset) - if len(splitwise_assets) == 0: - return None - elif len(splitwise_assets) > 1: - raise SplitLunchError( - "SplitLunch requires an manually managed Splitwise asset. " - "Make sure you have a single account where 'Splitwise' " - "is in the asset's `Institution Name`." - ) - else: - return splitwise_assets[0] - - def _get_reimbursement_category(self) -> Optional[CategoriesObject]: - """ - Get the Reimbusement Category - - Parse a user's Lunch Money categories and return the Reimbursement - category - - Returns - ------- - CategoriesObject - """ - categories = self.lunchable.get_categories() - reimbursement_list = [] - for category in categories: - if "reimbursement" == category.name.strip().lower(): - reimbursement_list.append(category) - if len(reimbursement_list) != 1: - return None - return reimbursement_list[0] - - def _get_splitwise_tags(self) -> None: - """ - Get Lunch Money Tags to Interact with - - Returns - ------- - Dict[str, int] - """ - all_tags = self.lunchable.get_tags() - for tag in all_tags: - if tag.name.lower() == SplitLunchConfig.splitlunch_tag.lower(): - self.splitlunch_tag = tag - elif tag.name.lower() == SplitLunchConfig.splitwise_tag.lower(): - self.splitwise_tag = tag - elif tag.name.lower() == SplitLunchConfig.splitlunch_import_tag.lower(): - self.splitlunch_import_tag = tag - elif ( - tag.name.lower() - == SplitLunchConfig.splitlunch_direct_import_tag.lower() - ): - self.splitlunch_direct_import_tag = tag - - def _raise_nonexistent_tag_error(self, tags: List[str]) -> None: - """ - Raise a warning for specific SplitLunch Tags - - tags: List[str] - A list of tags to raise the error for - """ - if ( - self.splitlunch_tag == self._none_tag - and SplitLunchConfig.splitlunch_tag in tags - ): - error_message = ( - f"a `{SplitLunchConfig.splitlunch_tag}` tag is required. " - f"This tag is used for splitting transactions in half and have half " - f"marked as reimbursed." - ) - raise SplitLunchError(error_message) - if ( - self.splitwise_tag == self._none_tag - and SplitLunchConfig.splitwise_tag in tags - ): - error_message = ( - f"a `{SplitLunchConfig.splitwise_tag}` tag is required. " - f"This tag is used for splitting transactions in half and have half " - f"marked as reimbursed." - ) - raise SplitLunchError(error_message) - if ( - self.splitlunch_import_tag == self._none_tag - and SplitLunchConfig.splitlunch_import_tag in tags - ): - error_message = ( - f"a `{SplitLunchConfig.splitlunch_import_tag}` tag is required. " - f"This tag is used for creating Splitwise transactions directly from " - f"Lunch Money transactions. These transactions will be split in half," - f"and have one half marked as reimbursed." - ) - raise SplitLunchError(error_message) - if ( - self.splitlunch_direct_import_tag == self._none_tag - and SplitLunchConfig.splitlunch_direct_import_tag in tags - ): - error_message = ( - f"a `{SplitLunchConfig.splitlunch_direct_import_tag}` tag is " - "required. This tag is used for creating Splitwise transactions " - "directly from Lunch Money transactions. These transactions will " - "be completely owed by your financial partner." - ) - raise SplitLunchError(error_message) - - def get_splitlunch_tagged_transactions( - self, - start_date: Optional[datetime.date] = None, - end_date: Optional[datetime.date] = None, - ) -> List[TransactionObject]: - """ - Retrieve all transactions with the "Splitlunch" Tag - - Parameters - ---------- - start_date: Optional[datetime.date] - end_date : Optional[datetime.date] - - Returns - ------- - List[TransactionObject] - """ - if start_date is None: - start_date = self.earliest_start_date - if end_date is None: - end_date = self.latest_end_date - self._raise_nonexistent_tag_error(tags=[SplitLunchConfig.splitlunch_tag]) - transactions = self.lunchable.get_transactions( - tag_id=self.splitlunch_tag.id, start_date=start_date, end_date=end_date - ) - return transactions - - def get_splitlunch_import_tagged_transactions( - self, - start_date: Optional[datetime.date] = None, - end_date: Optional[datetime.date] = None, - ) -> List[TransactionObject]: - """ - Retrieve all transactions with the "SplitLunchImport" Tag - - Parameters - ---------- - start_date: Optional[datetime.date] - end_date : Optional[datetime.date] - - Returns - ------- - List[TransactionObject] - """ - if start_date is None: - start_date = self.earliest_start_date - if end_date is None: - end_date = self.latest_end_date - self._raise_nonexistent_tag_error(tags=[SplitLunchConfig.splitlunch_import_tag]) - transactions = self.lunchable.get_transactions( - tag_id=self.splitlunch_import_tag.id, - start_date=start_date, - end_date=end_date, - ) - return transactions - - def get_splitlunch_direct_import_tagged_transactions( - self, - start_date: Optional[datetime.date] = None, - end_date: Optional[datetime.date] = None, - ) -> List[TransactionObject]: - """ - Retrieve all transactions with the "SplitLunchDirectImport" Tag - - Parameters - ---------- - start_date: Optional[datetime.date] - end_date : Optional[datetime.date] - - Returns - ------- - List[TransactionObject] - """ - if start_date is None: - start_date = self.earliest_start_date - if end_date is None: - end_date = self.latest_end_date - self._raise_nonexistent_tag_error( - tags=[SplitLunchConfig.splitlunch_direct_import_tag] - ) - transactions = self.lunchable.get_transactions( - tag_id=self.splitlunch_direct_import_tag.id, - start_date=start_date, - end_date=end_date, - ) - return transactions - - def get_splitwise_tagged_transactions( - self, - start_date: Optional[datetime.date] = None, - end_date: Optional[datetime.date] = None, - ) -> List[TransactionObject]: - """ - Retrieve all transactions with the "Splitwise" Tag - - Parameters - ---------- - start_date: Optional[datetime.date] - end_date : Optional[datetime.date] - - Returns - ------- - List[TransactionObject] - """ - if start_date is None: - start_date = self.earliest_start_date - if end_date is None: - end_date = self.latest_end_date - self._raise_nonexistent_tag_error(tags=[SplitLunchConfig.splitwise_tag]) - transactions = self.lunchable.get_transactions( - tag_id=self.splitwise_tag.id, start_date=start_date, end_date=end_date - ) - return transactions - - def make_splitlunch(self, tag_transactions: bool = False) -> List[Dict[int, Any]]: - """ - Operate on `SplitLunch` tagged transactions - - Split all transactions with the `SplitLunch` tag in half. One of these - new splits will be recategorized to `Reimbursement`. Both new splits will receive - the `Splitwise` tag without any preexisting tags. - """ - if self.reimbursement_category is None: - self._raise_category_reimbursement_error() - raise ValueError("ReimbursementCategory") - split_transaction_ids = [] - tagged_objects = self.get_splitlunch_tagged_transactions() - for transaction in tagged_objects: - # Split the Original Amount - amount_1, amount_2 = self.split_a_transaction(amount=transaction.amount) - # Generate the First Split - split_object = TransactionSplitObject( - date=transaction.date, - category_id=transaction.category_id, - notes=transaction.notes, - amount=amount_1, - ) - # Generate the second split as a copy, change some properties - reimbursement_object = split_object.model_copy() - reimbursement_object.amount = amount_2 - reimbursement_object.category_id = self.reimbursement_category.id - logger.debug( - "Splitting transaction: %s -> (%s, %s)", - transaction.amount, - amount_1, - amount_2, - ) - - update_response = self.lunchable.update_transaction( - transaction_id=transaction.id, - split=[split_object, reimbursement_object], - ) - # Tag each of the new transactions generated - split_transaction_ids.append({transaction.id: update_response["split"]}) - for split_transaction_id in update_response["split"]: - update_tags = transaction.tags if transaction.tags is not None else [] - tags = [ - tag.name - for tag in update_tags - if tag is not None - and tag.name.lower() != self.splitlunch_tag.name.lower() - ] - if self.splitwise_tag.name not in tags and tag_transactions is True: - self._raise_nonexistent_tag_error( - tags=[SplitLunchConfig.splitwise_tag] - ) - tags.append(self.splitwise_tag.name) - tag_update = TransactionUpdateObject(tags=tags) - self.lunchable.update_transaction( - transaction_id=split_transaction_id, transaction=tag_update - ) - return split_transaction_ids - - def make_splitlunch_import( - self, tag_transactions: bool = False - ) -> List[Dict[str, Any]]: - """ - Operate on `SplitLunchImport` tagged transactions - - Send a transaction to Splitwise and then split the original transaction in Lunch Money. - One of these new splits will be recategorized to `Reimbursement`. Both new splits - will receive the `Splitwise` tag without the `SplitLunchImport` tag. Any other tags will be - reapplied. - - Parameters - ---------- - tag_transactions : bool - Whether to tag the transactions with the `Splitwise` tag after splitting them. - Defaults to False which - - Returns - ------- - List[Dict[str, Any]] - """ - self._raise_financial_partner_error() - if self.reimbursement_category is None: - self._raise_category_reimbursement_error() - raise ValueError("ReimbursementCategory") - tagged_objects = self.get_splitlunch_import_tagged_transactions() - update_responses = [] - for transaction in tagged_objects: - # Split the Original Amount - description = str(transaction.payee) - if transaction.notes is not None: - description = f"{transaction.payee} - {transaction.notes}" - new_transaction = self.create_self_paid_expense( - amount=transaction.amount, - description=description, - date=transaction.date, - ) - notes = f"Splitwise ID: {new_transaction.splitwise_id}" - if transaction.notes is not None: - notes = f"{transaction.notes} || {notes}" - split_object = TransactionSplitObject( - date=transaction.date, - category_id=transaction.category_id, - notes=notes, - # financial_impact for self-paid transactions will be negative - amount=round( - transaction.amount - abs(new_transaction.financial_impact), 2 - ), - ) - reimbursement_object = split_object.model_copy() - reimbursement_object.amount = abs(new_transaction.financial_impact) - reimbursement_object.category_id = self.reimbursement_category.id - logger.debug( - f"Transaction split by Splitwise: {transaction.amount} -> " - f"({split_object.amount}, {reimbursement_object.amount})" - ) - update_response = self.lunchable.update_transaction( - transaction_id=transaction.id, - split=[split_object, reimbursement_object], - ) - formatted_update_response = { - "original_id": transaction.id, - "payee": transaction.payee, - "amount": transaction.amount, - "reimbursement_amount": reimbursement_object.amount, - "notes": transaction.notes, - "splitwise_id": new_transaction.splitwise_id, - "updated": update_response["updated"], - "split": update_response["split"], - } - update_responses.append(formatted_update_response) - # Tag each of the new transactions generated - for split_transaction_id in update_response["split"]: - update_tags = transaction.tags or [] - tags = [ - tag.name - for tag in update_tags - if tag.name.lower() != self.splitlunch_import_tag.name.lower() - ] - if self.splitwise_tag.name not in tags and tag_transactions is True: - self._raise_nonexistent_tag_error( - tags=[SplitLunchConfig.splitwise_tag] - ) - tags.append(self.splitwise_tag.name) - tag_update = TransactionUpdateObject(tags=tags) - self.lunchable.update_transaction( - transaction_id=split_transaction_id, transaction=tag_update - ) - return update_responses - - def make_splitlunch_direct_import( - self, tag_transactions: bool = False - ) -> List[Dict[str, Any]]: - """ - Operate on `SplitLunchDirectImport` tagged transactions - - Send a transaction to Splitwise and then mark the transaction under the - `Reimbursement` category. The sum of the transaction will be completely owed - by the financial partner. - - Parameters - ---------- - tag_transactions : bool - Whether to tag the transactions with the `Splitwise` tag after splitting them. - Defaults to False which - """ - self._raise_financial_partner_error() - if self.reimbursement_category is None: - self._raise_category_reimbursement_error() - raise ValueError("ReimbursementCategory") - tagged_objects = self.get_splitlunch_direct_import_tagged_transactions() - update_responses = [] - for transaction in tagged_objects: - # Split the Original Amount - description = str(transaction.payee) - if transaction.notes is not None: - description = f"{transaction.payee} - {transaction.notes}" - new_transaction = self.create_expense_on_behalf_of_partner( - amount=transaction.amount, - description=description, - date=transaction.date, - ) - notes = f"Splitwise ID: {new_transaction.splitwise_id}" - if transaction.notes is not None: - notes = f"{transaction.notes} || {notes}" - existing_tags = transaction.tags or [] - tags = [ - tag.name - for tag in existing_tags - if tag.name.lower() != self.splitlunch_direct_import_tag.name.lower() - ] - if self.splitwise_tag.name not in tags and tag_transactions is True: - self._raise_nonexistent_tag_error(tags=[SplitLunchConfig.splitwise_tag]) - tags.append(self.splitwise_tag.name) - update = TransactionUpdateObject( - category_id=self.reimbursement_category.id, tags=tags, notes=notes - ) - response = self.lunchable.update_transaction( - transaction_id=transaction.id, transaction=update - ) - formatted_update_response = { - "original_id": transaction.id, - "payee": transaction.payee, - "amount": transaction.amount, - "reimbursement_amount": transaction.amount, - "notes": transaction.notes, - "splitwise_id": new_transaction.splitwise_id, - "updated": response["updated"], - "split": None, - } - update_responses.append(formatted_update_response) - return update_responses - - def splitwise_to_lunchmoney( - self, - expenses: List[SplitLunchExpense], - allow_self_paid: bool = False, - allow_payments: bool = False, - ) -> List[int]: - """ - Ingest Splitwise Expenses into Lunch Money - - This function inserts splitwise expenses into Lunch Money. If an expense not deleted and has - a non-0 impact on the user's splitwise balance it qualifies for ingestion. By default, - payments and self-paid transactions are also ineligible. Otherwise it will be ignored. - - Parameters - ---------- - expenses: List[SplitLunchExpense] - - Returns - ------- - List[int] - New Lunch Money transaction IDs - """ - if self.splitwise_asset is None: - self._raise_splitwise_asset_error() - raise ValueError("SplitwiseAsset") - batch = [] - new_transaction_ids = [] - filtered_expenses = self.filter_relevant_splitwise_expenses( - expenses=expenses, - allow_self_paid=allow_self_paid, - allow_payments=allow_payments, - ) - for splitwise_transaction in filtered_expenses: - new_lunchmoney_transaction = TransactionInsertObject( - date=splitwise_transaction.date.astimezone(tzlocal()), - payee=splitwise_transaction.description, - amount=splitwise_transaction.financial_impact, - asset_id=self.splitwise_asset.id, - external_id=splitwise_transaction.splitwise_id, - ) - batch.append(new_lunchmoney_transaction) - if len(batch) == 10: - new_ids = self.lunchable.insert_transactions( - transactions=batch, apply_rules=True - ) - new_transaction_ids += new_ids - batch = [] - if len(batch) > 0: - new_ids = self.lunchable.insert_transactions( - transactions=batch, apply_rules=True - ) - new_transaction_ids += new_ids - return new_transaction_ids - - @staticmethod - def filter_relevant_splitwise_expenses( - expenses: List[SplitLunchExpense], - allow_self_paid: bool = False, - allow_payments: bool = False, - ) -> List[SplitLunchExpense]: - """ - Filter Expenses in Splitwise into relevant expenses. - - This filtering action is important to understand when seeing why not - all transactions from Splitwise end up flowing into Lunch Money. - - 1) It filters out deleted expenses - - 2) It filters out expenses with a financial impact of 0, implying that the user was not - involved in the expense. - - 3) If the --allow-self-paid flag is not provided, it filters out `self-paid` expenses. A - `self-paid` expense is an expense in Splitwise where you originated the payment. This is - excluded because it is assumed that these transactions will have already been imported via a - different account. - - 4) If the --allow-payments flag is not provided, it filters out payments. Payments are - excluded because it is assumed that these transactions will have already been imported via a - different account. - - Parameters - ---------- - expenses: List[SplitLunchExpense] - - Returns - ------- - List[SplitLunchExpense] - """ - filtered_expenses = [] - for splitwise_transaction in expenses: - if all( - [ - splitwise_transaction.deleted is False, - splitwise_transaction.financial_impact != 0.00, - allow_self_paid or (splitwise_transaction.self_paid is False), - allow_payments or (splitwise_transaction.payment is False), - ] - ): - filtered_expenses.append(splitwise_transaction) - - return filtered_expenses - - def get_splitwise_balance(self) -> float: - """ - Get the net balance in Splitwise - - Returns - ------- - float - """ - groups = self.getGroups() - total_balance = 0.00 - for group in groups: - for debt in group.simplified_debts: - if debt.fromUser == self.current_user.id: - total_balance -= float(debt.amount) - elif debt.toUser == self.current_user.id: - total_balance += float(debt.amount) - return total_balance - - def update_splitwise_balance(self) -> AssetsObject: - """ - Get and update the Splitwise Asset in Lunch Money - - Returns - ------- - AssetsObject - Updated balance - """ - if self.splitwise_asset is None: - self._raise_splitwise_asset_error() - raise ValueError("SplitwiseAsset") - balance = self.get_splitwise_balance() - if balance != self.splitwise_asset.balance: - updated_asset = self.lunchable.update_asset( - asset_id=self.splitwise_asset.id, balance=balance - ) - self.splitwise_asset = updated_asset - return self.splitwise_asset - - _deleted_payee = "[DELETED FROM SPLITWISE]" - - def get_new_transactions( - self, - dated_after: Optional[datetime.datetime] = None, - dated_before: Optional[datetime.datetime] = None, - ) -> Tuple[List[SplitLunchExpense], List[TransactionObject]]: - """ - Get Splitwise Transactions that don't exist in Lunch Money - - Also return deleted transaction from LunchMoney - - Returns - ------- - Tuple[List[SplitLunchExpense], List[TransactionObject]] - New and Deleted Transactions - """ - if self.splitwise_asset is None: - self._raise_splitwise_asset_error() - raise ValueError("SplitwiseAsset") - splitlunch_expenses = self.lunchable.get_transactions( - asset_id=self.splitwise_asset.id, - start_date=datetime.datetime(1800, 1, 1), - end_date=datetime.datetime(2300, 12, 31), - ) - splitlunch_ids = { - int(item.external_id) - for item in splitlunch_expenses - if item.external_id is not None - } - splitwise_expenses = self.get_expenses( - limit=0, - dated_after=dated_after, - dated_before=dated_before, - ) - splitwise_ids = {item.splitwise_id for item in splitwise_expenses} - new_ids = splitwise_ids.difference(splitlunch_ids) - new_expenses = [ - expense for expense in splitwise_expenses if expense.splitwise_id in new_ids - ] - deleted_transactions = self.get_deleted_transactions( - splitlunch_expenses=splitlunch_expenses, - splitwise_transactions=splitwise_expenses, - ) - return new_expenses, deleted_transactions - - def get_deleted_transactions( - self, - splitlunch_expenses: List[TransactionObject], - splitwise_transactions: List[SplitLunchExpense], - ) -> List[TransactionObject]: - """ - Get Splitwise Transactions that exist in Lunch Money but have since been deleted - - Set these transactions to $0.00 and Make a Note - - Parameters - ---------- - splitlunch_expenses: List[TransactionObject] - splitwise_transactions: List[SplitLunchExpense] - - Returns - ------- - List[TransactionObject] - """ - if self.splitwise_asset is None: - self._raise_splitwise_asset_error() - raise ValueError("SplitwiseAsset") - existing_transactions = { - int(item.external_id) - for item in splitlunch_expenses - if item.external_id is not None - } - deleted_ids = { - item.splitwise_id for item in splitwise_transactions if item.deleted is True - } - untethered_transactions = deleted_ids.intersection(existing_transactions) - transactions_to_delete = [ - tran - for tran in splitlunch_expenses - if tran.external_id is not None - and int(tran.external_id) in untethered_transactions - and tran.payee != self._deleted_payee - ] - return transactions_to_delete - - def refresh_splitwise_transactions( - self, - dated_after: Optional[datetime.datetime] = None, - dated_before: Optional[datetime.datetime] = None, - allow_self_paid: bool = False, - allow_payments: bool = False, - ) -> Dict[str, Any]: - """ - Import New Splitwise Transactions to Lunch Money - - This function get's all transactions from Splitwise, all transactions from - your Lunch Money Splitwise account and compares the two. - - Returns - ------- - List[SplitLunchExpense] - """ - new_transactions, deleted_transactions = self.get_new_transactions( - dated_after=dated_after, - dated_before=dated_before, - ) - self.splitwise_to_lunchmoney( - expenses=new_transactions, - allow_self_paid=allow_self_paid, - allow_payments=allow_payments, - ) - splitwise_asset = self.update_splitwise_balance() - self.handle_deleted_transactions(deleted_transactions=deleted_transactions) - return { - "balance": splitwise_asset.balance, - "new": new_transactions, - "deleted": deleted_transactions, - } - - def handle_deleted_transactions( - self, - deleted_transactions: List[TransactionObject], - ) -> List[Dict[str, Any]]: - """ - Update Transactions That Exist in Splitwise, but have been deleted in Splitwise - - Parameters - ---------- - deleted_transactions: List[TransactionObject] - - Returns - ------- - List[Dict[str, Any]] - """ - updated_transactions = [] - for transaction in deleted_transactions: - update = self.lunchable.update_transaction( - transaction_id=transaction.id, - transaction=TransactionUpdateObject( - amount=0.00, - payee=self._deleted_payee, - notes=f"{transaction.payee} || {transaction.amount} || {transaction.notes}", - ), - ) - updated_transactions.append(update) - return updated_transactions - - def _raise_financial_partner_error(self) -> None: - """ - Raise Errors for Financial Partners - """ - if self.financial_partner is None: - raise SplitLunchError( - "You must designate a financial partner in Splitwise. " - "This can be done with the partner's Splitwise User ID # " - "or their email address." - ) - - def _raise_splitwise_asset_error(self) -> None: - """ - Raise Errors for Splitwise Asset - """ - raise SplitLunchError( - "You must create an asset (aka Account) in Lunch Money with " - "`Splitwise` in the name. There should only be one account " - "like this." - ) - - def _raise_category_reimbursement_error(self) -> None: - """ - Raise Errors for Splitwise Asset - """ - raise SplitLunchError( - "SplitLunch requires a reimbursement Category. " - "Make sure you have a category entitled `Reimbursement`. " - "This category will be excluded from budgeting." - "Half of split transactions will be created with " - "this category." - ) diff --git a/lunchable/plugins/splitlunch/models.py b/lunchable/plugins/splitlunch/models.py deleted file mode 100644 index 3e76beb7..00000000 --- a/lunchable/plugins/splitlunch/models.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -SplitLunch Data Models -""" - -import datetime -from typing import List, Optional - -from lunchable.models._base import LunchableModel - - -class SplitLunchExpense(LunchableModel): - """ - SplitLunch Object for Splitwise Expenses - """ - - splitwise_id: int - original_amount: float - self_paid: bool - financial_impact: float - description: str - category: str - details: Optional[str] = None - payment: bool - date: datetime.datetime - users: List[int] - created_at: datetime.datetime - updated_at: datetime.datetime - deleted_at: Optional[datetime.datetime] = None - deleted: bool diff --git a/mkdocs.yaml b/mkdocs.yaml index 6a8276a0..72ff4005 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -5,11 +5,7 @@ nav: - Home 🏠: index.md - Usage 📖: usage.md - LunchMoney 🍽️: interacting.md -- Apps and Plugins 🧩: - - plugins/index.md - - PushLunch 📲: plugins/pushlunch.md - - SplitLunch 🍱: plugins/splitlunch.md - - PrimeLunch 🥪: plugins/primelunch.md +- Apps and Plugins 🧩: plugins.md - Command Line Interface ⌨️: cli.md - API Documentation 🤖: reference/ - Contributing 🤝: contributing.md diff --git a/pyproject.toml b/pyproject.toml index 782e22fb..63318a29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,29 +35,20 @@ name = "lunchable" readme = "README.md" requires-python = ">=3.8,<4" -[project.entry-points."lunchable.cli"] -primelunch = "lunchable.plugins.primelunch.cli:primelunch" -pushlunch = "lunchable.plugins.pushlunch.cli:pushlunch" -splitlunch = "lunchable.plugins.splitlunch.cli:splitlunch" - [project.optional-dependencies] all = [ - "pandas", - "python-dateutil", - "splitwise>=2.3.0,<3.0.0" + "lunchable-primelunch", + "lunchable-pushlunch", + "lunchable-splitlunch" ] plugins = [ - "pandas", - "python-dateutil", - "splitwise>=2.3.0,<3.0.0" -] -primelunch = [ - "pandas" -] -splitlunch = [ - "python-dateutil", - "splitwise>=2.3.0,<3.0.0" + "lunchable-primelunch", + "lunchable-pushlunch", + "lunchable-splitlunch" ] +primelunch = ["lunchable-primelunch"] +pushlunch = ["lunchable-pushlunch"] +splitlunch = ["lunchable-splitlunch"] [project.scripts] lunchable = "lunchable._cli:cli" diff --git a/requirements.txt b/requirements.txt index 9f277e65..c35939bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,9 +7,9 @@ # - importlib-metadata>=3.6 # - pydantic<3,>=2 # - rich>=10.0.0 -# - pandas -# - python-dateutil -# - splitwise<3.0.0,>=2.3.0 +# - lunchable-primelunch +# - lunchable-pushlunch +# - lunchable-splitlunch # annotated-types==0.6.0 @@ -27,20 +27,41 @@ click==8.1.7 # via # hatch.envs.default # click-plugins + # lunchable + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch click-plugins==1.1.1 - # via hatch.envs.default + # via + # hatch.envs.default + # lunchable h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx httpx==0.25.2 - # via hatch.envs.default + # via + # hatch.envs.default + # lunchable idna==3.6 # via # anyio # httpx # requests importlib-metadata==7.0.1 + # via + # hatch.envs.default + # lunchable +lunchable==1.2.1 + # via + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch +lunchable-primelunch==1.0.0 + # via hatch.envs.default +lunchable-pushlunch==1.0.1 + # via hatch.envs.default +lunchable-splitlunch==1.0.0 # via hatch.envs.default markdown-it-py==3.0.0 # via rich @@ -51,16 +72,18 @@ numpy==1.26.2 oauthlib==3.2.2 # via requests-oauthlib pandas==2.1.4 - # via hatch.envs.default + # via lunchable-primelunch pydantic==2.5.2 - # via hatch.envs.default + # via + # hatch.envs.default + # lunchable pydantic-core==2.14.5 # via pydantic pygments==2.17.2 # via rich python-dateutil==2.8.2 # via - # hatch.envs.default + # lunchable-splitlunch # pandas pytz==2023.3.post1 # via pandas @@ -71,7 +94,11 @@ requests==2.31.0 requests-oauthlib==1.3.1 # via splitwise rich==13.7.0 - # via hatch.envs.default + # via + # hatch.envs.default + # lunchable + # lunchable-primelunch + # lunchable-splitlunch six==1.16.0 # via python-dateutil sniffio==1.3.0 @@ -79,7 +106,7 @@ sniffio==1.3.0 # anyio # httpx splitwise==2.5.0 - # via hatch.envs.default + # via lunchable-splitlunch typing-extensions==4.9.0 # via # pydantic diff --git a/requirements/requirements-all.py3.10.txt b/requirements/requirements-all.py3.10.txt index 7a0fbe50..731422e9 100644 --- a/requirements/requirements-all.py3.10.txt +++ b/requirements/requirements-all.py3.10.txt @@ -11,9 +11,9 @@ # - importlib-metadata>=3.6 # - pydantic<3,>=2 # - rich>=10.0.0 -# - pandas -# - python-dateutil -# - splitwise<3.0.0,>=2.3.0 +# - lunchable-primelunch +# - lunchable-pushlunch +# - lunchable-splitlunch # annotated-types==0.6.0 @@ -31,8 +31,14 @@ click==8.1.7 # via # hatch.envs.all.py3.10 # click-plugins + # lunchable + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch click-plugins==1.1.1 - # via hatch.envs.all.py3.10 + # via + # hatch.envs.all.py3.10 + # lunchable coverage==7.3.3 # via # coverage @@ -46,7 +52,9 @@ h11==0.14.0 httpcore==1.0.2 # via httpx httpx==0.25.2 - # via hatch.envs.all.py3.10 + # via + # hatch.envs.all.py3.10 + # lunchable idna==3.6 # via # anyio @@ -54,9 +62,22 @@ idna==3.6 # requests # yarl importlib-metadata==7.0.1 - # via hatch.envs.all.py3.10 + # via + # hatch.envs.all.py3.10 + # lunchable iniconfig==2.0.0 # via pytest +lunchable==1.2.1 + # via + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch +lunchable-primelunch==1.0.0 + # via hatch.envs.all.py3.10 +lunchable-pushlunch==1.0.1 + # via hatch.envs.all.py3.10 +lunchable-splitlunch==1.0.0 + # via hatch.envs.all.py3.10 markdown-it-py==3.0.0 # via rich mdurl==0.1.2 @@ -70,11 +91,13 @@ oauthlib==3.2.2 packaging==23.2 # via pytest pandas==2.1.4 - # via hatch.envs.all.py3.10 + # via lunchable-primelunch pluggy==1.3.0 # via pytest pydantic==2.5.2 - # via hatch.envs.all.py3.10 + # via + # hatch.envs.all.py3.10 + # lunchable pydantic-core==2.14.5 # via pydantic pygments==2.17.2 @@ -90,7 +113,7 @@ pytest-mock==3.12.0 # via hatch.envs.all.py3.10 python-dateutil==2.8.2 # via - # hatch.envs.all.py3.10 + # lunchable-splitlunch # pandas pytz==2023.3.post1 # via pandas @@ -103,7 +126,11 @@ requests==2.31.0 requests-oauthlib==1.3.1 # via splitwise rich==13.7.0 - # via hatch.envs.all.py3.10 + # via + # hatch.envs.all.py3.10 + # lunchable + # lunchable-primelunch + # lunchable-splitlunch six==1.16.0 # via python-dateutil sniffio==1.3.0 @@ -111,7 +138,7 @@ sniffio==1.3.0 # anyio # httpx splitwise==2.5.0 - # via hatch.envs.all.py3.10 + # via lunchable-splitlunch tomli==2.0.1 # via # coverage diff --git a/requirements/requirements-all.py3.11.txt b/requirements/requirements-all.py3.11.txt index 7b70629d..1a8499b7 100644 --- a/requirements/requirements-all.py3.11.txt +++ b/requirements/requirements-all.py3.11.txt @@ -11,9 +11,9 @@ # - importlib-metadata>=3.6 # - pydantic<3,>=2 # - rich>=10.0.0 -# - pandas -# - python-dateutil -# - splitwise<3.0.0,>=2.3.0 +# - lunchable-primelunch +# - lunchable-pushlunch +# - lunchable-splitlunch # annotated-types==0.6.0 @@ -31,8 +31,14 @@ click==8.1.7 # via # hatch.envs.all.py3.11 # click-plugins + # lunchable + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch click-plugins==1.1.1 - # via hatch.envs.all.py3.11 + # via + # hatch.envs.all.py3.11 + # lunchable coverage==7.3.3 # via # coverage @@ -42,7 +48,9 @@ h11==0.14.0 httpcore==1.0.2 # via httpx httpx==0.25.2 - # via hatch.envs.all.py3.11 + # via + # hatch.envs.all.py3.11 + # lunchable idna==3.6 # via # anyio @@ -50,9 +58,22 @@ idna==3.6 # requests # yarl importlib-metadata==7.0.1 - # via hatch.envs.all.py3.11 + # via + # hatch.envs.all.py3.11 + # lunchable iniconfig==2.0.0 # via pytest +lunchable==1.2.1 + # via + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch +lunchable-primelunch==1.0.0 + # via hatch.envs.all.py3.11 +lunchable-pushlunch==1.0.1 + # via hatch.envs.all.py3.11 +lunchable-splitlunch==1.0.0 + # via hatch.envs.all.py3.11 markdown-it-py==3.0.0 # via rich mdurl==0.1.2 @@ -66,11 +87,13 @@ oauthlib==3.2.2 packaging==23.2 # via pytest pandas==2.1.4 - # via hatch.envs.all.py3.11 + # via lunchable-primelunch pluggy==1.3.0 # via pytest pydantic==2.5.2 - # via hatch.envs.all.py3.11 + # via + # hatch.envs.all.py3.11 + # lunchable pydantic-core==2.14.5 # via pydantic pygments==2.17.2 @@ -86,7 +109,7 @@ pytest-mock==3.12.0 # via hatch.envs.all.py3.11 python-dateutil==2.8.2 # via - # hatch.envs.all.py3.11 + # lunchable-splitlunch # pandas pytz==2023.3.post1 # via pandas @@ -99,7 +122,11 @@ requests==2.31.0 requests-oauthlib==1.3.1 # via splitwise rich==13.7.0 - # via hatch.envs.all.py3.11 + # via + # hatch.envs.all.py3.11 + # lunchable + # lunchable-primelunch + # lunchable-splitlunch six==1.16.0 # via python-dateutil sniffio==1.3.0 @@ -107,7 +134,7 @@ sniffio==1.3.0 # anyio # httpx splitwise==2.5.0 - # via hatch.envs.all.py3.11 + # via lunchable-splitlunch typing-extensions==4.9.0 # via # pydantic diff --git a/requirements/requirements-all.py3.12.txt b/requirements/requirements-all.py3.12.txt index bcda3aa7..d4a276fc 100644 --- a/requirements/requirements-all.py3.12.txt +++ b/requirements/requirements-all.py3.12.txt @@ -11,9 +11,9 @@ # - importlib-metadata>=3.6 # - pydantic<3,>=2 # - rich>=10.0.0 -# - pandas -# - python-dateutil -# - splitwise<3.0.0,>=2.3.0 +# - lunchable-primelunch +# - lunchable-pushlunch +# - lunchable-splitlunch # annotated-types==0.6.0 @@ -31,8 +31,14 @@ click==8.1.7 # via # hatch.envs.all.py3.12 # click-plugins + # lunchable + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch click-plugins==1.1.1 - # via hatch.envs.all.py3.12 + # via + # hatch.envs.all.py3.12 + # lunchable coverage==7.3.3 # via # coverage @@ -42,7 +48,9 @@ h11==0.14.0 httpcore==1.0.2 # via httpx httpx==0.25.2 - # via hatch.envs.all.py3.12 + # via + # hatch.envs.all.py3.12 + # lunchable idna==3.6 # via # anyio @@ -50,9 +58,22 @@ idna==3.6 # requests # yarl importlib-metadata==7.0.1 - # via hatch.envs.all.py3.12 + # via + # hatch.envs.all.py3.12 + # lunchable iniconfig==2.0.0 # via pytest +lunchable==1.2.1 + # via + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch +lunchable-primelunch==1.0.0 + # via hatch.envs.all.py3.12 +lunchable-pushlunch==1.0.1 + # via hatch.envs.all.py3.12 +lunchable-splitlunch==1.0.0 + # via hatch.envs.all.py3.12 markdown-it-py==3.0.0 # via rich mdurl==0.1.2 @@ -66,11 +87,13 @@ oauthlib==3.2.2 packaging==23.2 # via pytest pandas==2.1.4 - # via hatch.envs.all.py3.12 + # via lunchable-primelunch pluggy==1.3.0 # via pytest pydantic==2.5.2 - # via hatch.envs.all.py3.12 + # via + # hatch.envs.all.py3.12 + # lunchable pydantic-core==2.14.5 # via pydantic pygments==2.17.2 @@ -86,7 +109,7 @@ pytest-mock==3.12.0 # via hatch.envs.all.py3.12 python-dateutil==2.8.2 # via - # hatch.envs.all.py3.12 + # lunchable-splitlunch # pandas pytz==2023.3.post1 # via pandas @@ -99,7 +122,11 @@ requests==2.31.0 requests-oauthlib==1.3.1 # via splitwise rich==13.7.0 - # via hatch.envs.all.py3.12 + # via + # hatch.envs.all.py3.12 + # lunchable + # lunchable-primelunch + # lunchable-splitlunch six==1.16.0 # via python-dateutil sniffio==1.3.0 @@ -107,7 +134,7 @@ sniffio==1.3.0 # anyio # httpx splitwise==2.5.0 - # via hatch.envs.all.py3.12 + # via lunchable-splitlunch typing-extensions==4.9.0 # via # pydantic diff --git a/requirements/requirements-all.py3.8.txt b/requirements/requirements-all.py3.8.txt index 57802fdf..9f2e659a 100644 --- a/requirements/requirements-all.py3.8.txt +++ b/requirements/requirements-all.py3.8.txt @@ -11,9 +11,9 @@ # - importlib-metadata>=3.6 # - pydantic<3,>=2 # - rich>=10.0.0 -# - pandas -# - python-dateutil -# - splitwise<3.0.0,>=2.3.0 +# - lunchable-primelunch +# - lunchable-pushlunch +# - lunchable-splitlunch # annotated-types==0.6.0 @@ -31,8 +31,14 @@ click==8.1.7 # via # hatch.envs.all.py3.8 # click-plugins + # lunchable + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch click-plugins==1.1.1 - # via hatch.envs.all.py3.8 + # via + # hatch.envs.all.py3.8 + # lunchable coverage==7.3.3 # via # coverage @@ -46,7 +52,9 @@ h11==0.14.0 httpcore==1.0.2 # via httpx httpx==0.25.2 - # via hatch.envs.all.py3.8 + # via + # hatch.envs.all.py3.8 + # lunchable idna==3.6 # via # anyio @@ -54,9 +62,22 @@ idna==3.6 # requests # yarl importlib-metadata==7.0.1 - # via hatch.envs.all.py3.8 + # via + # hatch.envs.all.py3.8 + # lunchable iniconfig==2.0.0 # via pytest +lunchable==1.2.1 + # via + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch +lunchable-primelunch==1.0.0 + # via hatch.envs.all.py3.8 +lunchable-pushlunch==1.0.1 + # via hatch.envs.all.py3.8 +lunchable-splitlunch==1.0.0 + # via hatch.envs.all.py3.8 markdown-it-py==3.0.0 # via rich mdurl==0.1.2 @@ -70,11 +91,13 @@ oauthlib==3.2.2 packaging==23.2 # via pytest pandas==2.0.3 - # via hatch.envs.all.py3.8 + # via lunchable-primelunch pluggy==1.3.0 # via pytest pydantic==2.5.2 - # via hatch.envs.all.py3.8 + # via + # hatch.envs.all.py3.8 + # lunchable pydantic-core==2.14.5 # via pydantic pygments==2.17.2 @@ -90,7 +113,7 @@ pytest-mock==3.12.0 # via hatch.envs.all.py3.8 python-dateutil==2.8.2 # via - # hatch.envs.all.py3.8 + # lunchable-splitlunch # pandas pytz==2023.3.post1 # via pandas @@ -103,7 +126,11 @@ requests==2.31.0 requests-oauthlib==1.3.1 # via splitwise rich==13.7.0 - # via hatch.envs.all.py3.8 + # via + # hatch.envs.all.py3.8 + # lunchable + # lunchable-primelunch + # lunchable-splitlunch six==1.16.0 # via python-dateutil sniffio==1.3.0 @@ -111,7 +138,7 @@ sniffio==1.3.0 # anyio # httpx splitwise==2.5.0 - # via hatch.envs.all.py3.8 + # via lunchable-splitlunch tomli==2.0.1 # via # coverage diff --git a/requirements/requirements-all.py3.9.txt b/requirements/requirements-all.py3.9.txt index c3c72d9a..9ccca4e5 100644 --- a/requirements/requirements-all.py3.9.txt +++ b/requirements/requirements-all.py3.9.txt @@ -11,9 +11,9 @@ # - importlib-metadata>=3.6 # - pydantic<3,>=2 # - rich>=10.0.0 -# - pandas -# - python-dateutil -# - splitwise<3.0.0,>=2.3.0 +# - lunchable-primelunch +# - lunchable-pushlunch +# - lunchable-splitlunch # annotated-types==0.6.0 @@ -31,8 +31,14 @@ click==8.1.7 # via # hatch.envs.all.py3.9 # click-plugins + # lunchable + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch click-plugins==1.1.1 - # via hatch.envs.all.py3.9 + # via + # hatch.envs.all.py3.9 + # lunchable coverage==7.3.3 # via # coverage @@ -46,7 +52,9 @@ h11==0.14.0 httpcore==1.0.2 # via httpx httpx==0.25.2 - # via hatch.envs.all.py3.9 + # via + # hatch.envs.all.py3.9 + # lunchable idna==3.6 # via # anyio @@ -54,9 +62,22 @@ idna==3.6 # requests # yarl importlib-metadata==7.0.1 - # via hatch.envs.all.py3.9 + # via + # hatch.envs.all.py3.9 + # lunchable iniconfig==2.0.0 # via pytest +lunchable==1.2.1 + # via + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch +lunchable-primelunch==1.0.0 + # via hatch.envs.all.py3.9 +lunchable-pushlunch==1.0.1 + # via hatch.envs.all.py3.9 +lunchable-splitlunch==1.0.0 + # via hatch.envs.all.py3.9 markdown-it-py==3.0.0 # via rich mdurl==0.1.2 @@ -70,11 +91,13 @@ oauthlib==3.2.2 packaging==23.2 # via pytest pandas==2.1.4 - # via hatch.envs.all.py3.9 + # via lunchable-primelunch pluggy==1.3.0 # via pytest pydantic==2.5.2 - # via hatch.envs.all.py3.9 + # via + # hatch.envs.all.py3.9 + # lunchable pydantic-core==2.14.5 # via pydantic pygments==2.17.2 @@ -90,7 +113,7 @@ pytest-mock==3.12.0 # via hatch.envs.all.py3.9 python-dateutil==2.8.2 # via - # hatch.envs.all.py3.9 + # lunchable-splitlunch # pandas pytz==2023.3.post1 # via pandas @@ -103,7 +126,11 @@ requests==2.31.0 requests-oauthlib==1.3.1 # via splitwise rich==13.7.0 - # via hatch.envs.all.py3.9 + # via + # hatch.envs.all.py3.9 + # lunchable + # lunchable-primelunch + # lunchable-splitlunch six==1.16.0 # via python-dateutil sniffio==1.3.0 @@ -111,7 +138,7 @@ sniffio==1.3.0 # anyio # httpx splitwise==2.5.0 - # via hatch.envs.all.py3.9 + # via lunchable-splitlunch tomli==2.0.1 # via # coverage diff --git a/requirements/requirements-docs.txt b/requirements/requirements-docs.txt index 16c5b63e..8bf25f3a 100644 --- a/requirements/requirements-docs.txt +++ b/requirements/requirements-docs.txt @@ -1,7 +1,7 @@ # # This file is autogenerated by hatch-pip-compile with Python 3.11 # -# [constraints] requirements.txt (SHA256: fab1a8fdd5601fab426b875234fb9b2fa894f0e59667df5182eb30bc9d261f69) +# [constraints] requirements.txt (SHA256: 9e43e99cd70a7f9f6a76ee8ad5acc2918100e37566fb9131f12d0d163f70670b) # # - markdown-callouts # - markdown-exec @@ -22,9 +22,9 @@ # - importlib-metadata>=3.6 # - pydantic<3,>=2 # - rich>=10.0.0 -# - pandas -# - python-dateutil -# - splitwise<3.0.0,>=2.3.0 +# - lunchable-primelunch +# - lunchable-pushlunch +# - lunchable-splitlunch # annotated-types==0.6.0 @@ -52,6 +52,10 @@ click==8.1.7 # -c requirements.txt # hatch.envs.docs # click-plugins + # lunchable + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch # mkdocs # mkdocs-click # mkdocstrings @@ -59,6 +63,7 @@ click-plugins==1.1.1 # via # -c requirements.txt # hatch.envs.docs + # lunchable colorama==0.4.6 # via # griffe @@ -85,6 +90,7 @@ httpx==0.25.2 # via # -c requirements.txt # hatch.envs.docs + # lunchable idna==3.6 # via # -c requirements.txt @@ -95,11 +101,30 @@ importlib-metadata==7.0.1 # via # -c requirements.txt # hatch.envs.docs + # lunchable jinja2==3.1.2 # via # mkdocs # mkdocs-material # mkdocstrings +lunchable==1.2.1 + # via + # -c requirements.txt + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch +lunchable-primelunch==1.0.0 + # via + # -c requirements.txt + # hatch.envs.docs +lunchable-pushlunch==1.0.1 + # via + # -c requirements.txt + # hatch.envs.docs +lunchable-splitlunch==1.0.0 + # via + # -c requirements.txt + # hatch.envs.docs markdown==3.5.1 # via # markdown-callouts @@ -174,7 +199,7 @@ paginate==0.5.6 pandas==2.1.4 # via # -c requirements.txt - # hatch.envs.docs + # lunchable-primelunch pathspec==0.12.1 # via mkdocs platformdirs==4.1.0 @@ -185,6 +210,7 @@ pydantic==2.5.2 # via # -c requirements.txt # hatch.envs.docs + # lunchable pydantic-core==2.14.5 # via # -c requirements.txt @@ -203,8 +229,8 @@ pymdown-extensions==10.5 python-dateutil==2.8.2 # via # -c requirements.txt - # hatch.envs.docs # ghp-import + # lunchable-splitlunch # pandas pytz==2023.3.post1 # via @@ -233,6 +259,9 @@ rich==13.7.0 # via # -c requirements.txt # hatch.envs.docs + # lunchable + # lunchable-primelunch + # lunchable-splitlunch six==1.16.0 # via # -c requirements.txt @@ -245,7 +274,7 @@ sniffio==1.3.0 splitwise==2.5.0 # via # -c requirements.txt - # hatch.envs.docs + # lunchable-splitlunch typing-extensions==4.9.0 # via # -c requirements.txt diff --git a/requirements/requirements-lint.txt b/requirements/requirements-lint.txt index f8acc070..1a540c8e 100644 --- a/requirements/requirements-lint.txt +++ b/requirements/requirements-lint.txt @@ -1,7 +1,7 @@ # # This file is autogenerated by hatch-pip-compile with Python 3.11 # -# [constraints] requirements.txt (SHA256: fab1a8fdd5601fab426b875234fb9b2fa894f0e59667df5182eb30bc9d261f69) +# [constraints] requirements.txt (SHA256: 9e43e99cd70a7f9f6a76ee8ad5acc2918100e37566fb9131f12d0d163f70670b) # # - mypy>=1.6.1 # - ruff~=0.1.7 diff --git a/requirements/requirements-test.txt b/requirements/requirements-test.txt index 29c8e88c..dcc98048 100644 --- a/requirements/requirements-test.txt +++ b/requirements/requirements-test.txt @@ -1,7 +1,7 @@ # # This file is autogenerated by hatch-pip-compile with Python 3.11 # -# [constraints] requirements.txt (SHA256: fab1a8fdd5601fab426b875234fb9b2fa894f0e59667df5182eb30bc9d261f69) +# [constraints] requirements.txt (SHA256: 9e43e99cd70a7f9f6a76ee8ad5acc2918100e37566fb9131f12d0d163f70670b) # # - pytest # - pytest-cov @@ -13,9 +13,9 @@ # - importlib-metadata>=3.6 # - pydantic<3,>=2 # - rich>=10.0.0 -# - pandas -# - python-dateutil -# - splitwise<3.0.0,>=2.3.0 +# - lunchable-primelunch +# - lunchable-pushlunch +# - lunchable-splitlunch # annotated-types==0.6.0 @@ -41,10 +41,15 @@ click==8.1.7 # -c requirements.txt # hatch.envs.test # click-plugins + # lunchable + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch click-plugins==1.1.1 # via # -c requirements.txt # hatch.envs.test + # lunchable coverage==7.3.3 # via # coverage @@ -61,6 +66,7 @@ httpx==0.25.2 # via # -c requirements.txt # hatch.envs.test + # lunchable idna==3.6 # via # -c requirements.txt @@ -72,8 +78,27 @@ importlib-metadata==7.0.1 # via # -c requirements.txt # hatch.envs.test + # lunchable iniconfig==2.0.0 # via pytest +lunchable==1.2.1 + # via + # -c requirements.txt + # lunchable-primelunch + # lunchable-pushlunch + # lunchable-splitlunch +lunchable-primelunch==1.0.0 + # via + # -c requirements.txt + # hatch.envs.test +lunchable-pushlunch==1.0.1 + # via + # -c requirements.txt + # hatch.envs.test +lunchable-splitlunch==1.0.0 + # via + # -c requirements.txt + # hatch.envs.test markdown-it-py==3.0.0 # via # -c requirements.txt @@ -97,13 +122,14 @@ packaging==23.2 pandas==2.1.4 # via # -c requirements.txt - # hatch.envs.test + # lunchable-primelunch pluggy==1.3.0 # via pytest pydantic==2.5.2 # via # -c requirements.txt # hatch.envs.test + # lunchable pydantic-core==2.14.5 # via # -c requirements.txt @@ -124,7 +150,7 @@ pytest-mock==3.12.0 python-dateutil==2.8.2 # via # -c requirements.txt - # hatch.envs.test + # lunchable-splitlunch # pandas pytz==2023.3.post1 # via @@ -145,6 +171,9 @@ rich==13.7.0 # via # -c requirements.txt # hatch.envs.test + # lunchable + # lunchable-primelunch + # lunchable-splitlunch six==1.16.0 # via # -c requirements.txt @@ -157,7 +186,7 @@ sniffio==1.3.0 splitwise==2.5.0 # via # -c requirements.txt - # hatch.envs.test + # lunchable-splitlunch typing-extensions==4.9.0 # via # -c requirements.txt diff --git a/tests/plugins/__init__.py b/tests/plugins/__init__.py deleted file mode 100644 index 3c2264b6..00000000 --- a/tests/plugins/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Plugins Unit Tests -""" diff --git a/tests/plugins/pushlunch/__init__.py b/tests/plugins/pushlunch/__init__.py deleted file mode 100644 index 6a9e0674..00000000 --- a/tests/plugins/pushlunch/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Pushover Plugin Testing -""" diff --git a/tests/plugins/pushlunch/cassettes/test_post_transaction.yaml b/tests/plugins/pushlunch/cassettes/test_post_transaction.yaml deleted file mode 100644 index 0807e7c1..00000000 --- a/tests/plugins/pushlunch/cassettes/test_post_transaction.yaml +++ /dev/null @@ -1,216 +0,0 @@ -interactions: -- request: - body: - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Type: - - application/json - User-Agent: - - python-requests/2.29.0 - authorization: - - XXXXXXXXXX - method: GET - uri: https://dev.lunchmoney.app/v1/assets - response: - content: '{"assets":[{"id":49335,"type_name":"cash","subtype_name":"checking","name":"Test Account","display_name":null,"balance":"-16513.5500","balance_as_of":"2023-03-07T02:43:48.383Z","closed_on":null,"currency":"usd","institution_name":null,"exclude_transactions":false,"created_at":"2023-03-07T02:05:23.670Z"},{"id":78214,"type_name":"cash","subtype_name":null,"name":"test-account-1","display_name":"Test Account #1","balance":"5.2000","balance_as_of":"2023-12-15T02:26:08.599Z","closed_on":null,"currency":"usd","institution_name":"Test Institution","exclude_transactions":false,"created_at":"2023-12-15T02:24:12.158Z"}]}' - headers: - Access-Control-Allow-Credentials: - - 'true' - Connection: - - keep-alive - Content-Length: - - '618' - Content-Type: - - application/json; charset=utf-8 - Date: - - Fri, 15 Dec 2023 03:27:35 GMT - Etag: - - W/"26a-RdZT78+tFlrfVSVBVJsvuI2fLCs" - 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=1702610855&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=jbOtlJShgUrEu1SyJzVSXw%2BEEVmle0iQjCmXiIgczW0%3D"}]}' - Reporting-Endpoints: - - heroku-nel=https://nel.heroku.com/reports?ts=1702610855&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=jbOtlJShgUrEu1SyJzVSXw%2BEEVmle0iQjCmXiIgczW0%3D - Server: - - Cowboy - Vary: - - Origin, Accept-Encoding - Via: - - 1.1 vegur - X-Powered-By: - - Express - http_version: HTTP/1.1 - status_code: 200 -- request: - body: - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Type: - - application/json - User-Agent: - - python-requests/2.29.0 - authorization: - - XXXXXXXXXX - method: GET - uri: https://dev.lunchmoney.app/v1/plaid_accounts - response: - content: '{"plaid_accounts": [{"id": 71394, "date_linked": "2023-12-15", "name": "Chase - Freedom", "display_name": "Chase Freedom", "type": "credit", "subtype": "creditcard", "mask": "5678", "institution_name": "Chase", "status": "active", "limit": 12345, "balance": "123.0900", "currency": "usd", "balance_last_update": "2023-12-14T15:55:00.753Z", "import_start_date": "2023-02-06", "last_import": "2023-12-14T15:55:03.163Z", "last_fetch": "2023-12-14T15:55:03.301Z", "plaid_last_successful_update": "2023-12-13T15:51:37.490Z"}, {"id": 71395, "date_linked": "2023-12-15", "name": "Chase - Sapphire", "display_name": "Chase Sapphire", "type": "credit", "subtype": "creditcard", "mask": "1234", "institution_name": "Chase", "status": "active", "limit": 12345, "balance": "456.0900", "currency": "usd", "balance_last_update": "2023-12-14T15:55:00.753Z", "import_start_date": "2023-02-06", "last_import": "2023-12-14T15:55:03.163Z", "last_fetch": "2023-12-14T15:55:03.301Z", "plaid_last_successful_update": "2023-12-13T15:51:37.490Z"}]}' - headers: - Access-Control-Allow-Credentials: - - 'true' - Connection: - - keep-alive - Content-Length: - - '21' - Content-Type: - - application/json; charset=utf-8 - Date: - - Fri, 15 Dec 2023 03:27:35 GMT - Etag: - - W/"15-HiUW5V/D9oSc9LAodzElf6GkW1U" - 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=1702610855&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=jbOtlJShgUrEu1SyJzVSXw%2BEEVmle0iQjCmXiIgczW0%3D"}]}' - Reporting-Endpoints: - - heroku-nel=https://nel.heroku.com/reports?ts=1702610855&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=jbOtlJShgUrEu1SyJzVSXw%2BEEVmle0iQjCmXiIgczW0%3D - Server: - - Cowboy - Vary: - - Origin, Accept-Encoding - Via: - - 1.1 vegur - X-Powered-By: - - Express - http_version: HTTP/1.1 - status_code: 200 -- request: - body: - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Type: - - application/json - User-Agent: - - python-requests/2.29.0 - authorization: - - XXXXXXXXXX - method: GET - uri: https://dev.lunchmoney.app/v1/categories - 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":null,"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-Type: - - application/json; charset=utf-8 - Date: - - Fri, 15 Dec 2023 03:27:35 GMT - Etag: - - W/"bf6-3cADdNauX+96ckDbRxINI6x4Zf0" - 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=1702610855&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=jbOtlJShgUrEu1SyJzVSXw%2BEEVmle0iQjCmXiIgczW0%3D"}]}' - Reporting-Endpoints: - - heroku-nel=https://nel.heroku.com/reports?ts=1702610855&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=jbOtlJShgUrEu1SyJzVSXw%2BEEVmle0iQjCmXiIgczW0%3D - Server: - - Cowboy - Transfer-Encoding: - - chunked - Vary: - - Origin, Accept-Encoding - Via: - - 1.1 vegur - X-Powered-By: - - Express - content-length: - - '3062' - http_version: HTTP/1.1 - status_code: 200 -- request: - body: - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Length: - - '0' - Content-Type: - - application/json - User-Agent: - - python-requests/2.29.0 - method: POST - uri: https://api.pushover.net/1/messages.json?html=1&message=%3Cb%3EPayee%3A%3C%2Fb%3E+%3Ci%3ETest%3C%2Fi%3E%0A%3Cb%3EAmount%3A%3C%2Fb%3E+%3Ci%3E%24+1.00%3C%2Fi%3E%0A%3Cb%3EDate%3A%3C%2Fb%3E+%3Ci%3ESunday+September+19%2C+2021%3C%2Fi%3E%0A%3Cb%3ECategory%3A%3C%2Fb%3E+%3Ci%3EAnother+Another+Test+Category%3C%2Fi%3E%0A%3Cb%3EAccount%3A%3C%2Fb%3E+%3Ci%3ETest+Account%3C%2Fi%3E%0A%3Cb%3ECurrency%3A%3C%2Fb%3E+%3Ci%3EUSD%3C%2Fi%3E%0A%3Cb%3EStatus%3A%3C%2Fb%3E+%3Ci%3EUncleared%3C%2Fi%3E%0A%3Cb%3ENotes%3A%3C%2Fb%3E+%3Ci%3EExample+Test+Notification+from+lunchable%3C%2Fi%3E%0A%0A%3Ca+href%3D%22https%3A%2F%2Fmy.lunchmoney.app%2Ftransactions%2F2021%2F09%3Fstatus%3Dunreviewed%22%3E%3Cb%3EUncleared+Transactions+from+this+Period%3C%2Fb%3E%3C%2Fa%3E&title=Lunch+Money+Transaction&token=XXXXXXXXXX&user=XXXXXXXXXX - response: - content: '{"status":1,"request":"b80ebc1d-6ae8-430e-98c1-4cb53791f420"}' - headers: - Access-Control-Allow-Headers: - - X-Requested-With, X-Prototype-Version, Origin, Accept, Content-Type, X-CSRF-Token, X-Pushover-App, Authorization - Access-Control-Allow-Methods: - - POST, OPTIONS - Access-Control-Allow-Origin: - - '*' - Access-Control-Max-Age: - - '1728000' - CF-Cache-Status: - - DYNAMIC - CF-RAY: - - 835b9778c9ac4de2-MCI - Cache-Control: - - no-cache - Connection: - - keep-alive - Content-Type: - - application/json; charset=utf-8 - Date: - - Fri, 15 Dec 2023 03:27:35 GMT - Referrer-Policy: - - strict-origin-when-cross-origin - Server: - - cloudflare - Transfer-Encoding: - - chunked - X-Content-Type-Options: - - nosniff - X-Download-Options: - - noopen - X-Frame-Options: - - SAMEORIGIN - X-Limit-App-Limit: - - '10000' - X-Limit-App-Remaining: - - '9939' - X-Limit-App-Reset: - - '1704088800' - X-Permitted-Cross-Domain-Policies: - - none - X-Request-Id: - - bb106ac8-e675-4c00-804b-203765b0f209 - X-Runtime: - - '0.012594' - X-XSS-Protection: - - 1; mode=block - http_version: HTTP/1.1 - status_code: 200 -version: 1 diff --git a/tests/plugins/pushlunch/cassettes/test_send_notification.yaml b/tests/plugins/pushlunch/cassettes/test_send_notification.yaml deleted file mode 100644 index d2d4034c..00000000 --- a/tests/plugins/pushlunch/cassettes/test_send_notification.yaml +++ /dev/null @@ -1,205 +0,0 @@ -interactions: -- request: - body: - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Type: - - application/json - User-Agent: - - python-requests/2.26.0 - authorization: - - XXXXXXXXXX - method: GET - uri: https://dev.lunchmoney.app/v1/assets - response: - content: '{"assets":[{"id":21845,"type_name":"cash","subtype_name":"digital wallet (paypal, venmo)","name":"Splitwise Balance","display_name":"Splitwise","balance":"176.6700","balance_as_of":"2021-11-15T17:44:52.277Z","currency":"usd","closed_on":null,"institution_name":"Splitwise","created_at":"2021-08-28T16:06:02.701Z"},{"id":23043,"type_name":"cash","subtype_name":"digital wallet (paypal, venmo)","name":"Test Account","display_name":"Test Account","balance":"181.7100","balance_as_of":"2021-11-02T22:27:05.311Z","currency":"usd","closed_on":null,"institution_name":"Test","created_at":"2021-09-20T05:32:29.060Z"}]}' - headers: - Access-Control-Allow-Credentials: - - 'true' - Connection: - - keep-alive - Content-Length: - - '611' - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 16 Nov 2021 01:56:23 GMT - Etag: - - W/"263-PuKlmQwTXplhQGNn6uzd/fxEv94" - Server: - - Cowboy - Vary: - - Origin, Accept-Encoding - Via: - - 1.1 vegur - X-Powered-By: - - Express - http_version: HTTP/1.1 - status_code: 200 -- request: - body: - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Type: - - application/json - User-Agent: - - python-requests/2.26.0 - authorization: - - XXXXXXXXXX - method: GET - uri: https://dev.lunchmoney.app/v1/plaid_accounts - response: - content: '{"plaid_accounts": [{"id": 71394, "date_linked": "2023-12-15", "name": "Chase - Freedom", "display_name": "Chase Freedom", "type": "credit", "subtype": "creditcard", "mask": "5678", "institution_name": "Chase", "status": "active", "limit": 12345, "balance": "123.0900", "currency": "usd", "balance_last_update": "2023-12-14T15:55:00.753Z", "import_start_date": "2023-02-06", "last_import": "2023-12-14T15:55:03.163Z", "last_fetch": "2023-12-14T15:55:03.301Z", "plaid_last_successful_update": "2023-12-13T15:51:37.490Z"}, {"id": 71395, "date_linked": "2023-12-15", "name": "Chase - Sapphire", "display_name": "Chase Sapphire", "type": "credit", "subtype": "creditcard", "mask": "1234", "institution_name": "Chase", "status": "active", "limit": 12345, "balance": "456.0900", "currency": "usd", "balance_last_update": "2023-12-14T15:55:00.753Z", "import_start_date": "2023-02-06", "last_import": "2023-12-14T15:55:03.163Z", "last_fetch": "2023-12-14T15:55:03.301Z", "plaid_last_successful_update": "2023-12-13T15:51:37.490Z"}]}' - headers: - Access-Control-Allow-Credentials: - - 'true' - Connection: - - keep-alive - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 16 Nov 2021 01:56:23 GMT - Etag: - - W/"1450-Y89kn8ima647B5tEKBH83Vze8Mg" - 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 -- request: - body: - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Type: - - application/json - User-Agent: - - python-requests/2.26.0 - authorization: - - XXXXXXXXXX - method: GET - uri: https://dev.lunchmoney.app/v1/categories - response: - content: '{"categories":[{"id":443128,"name":"Groceries","description":null,"is_income":false,"exclude_from_budget":false,"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":null,"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":null,"archived":false,"archived_on":null,"order":null}]}' - headers: - Access-Control-Allow-Credentials: - - 'true' - Connection: - - keep-alive - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 16 Nov 2021 01:56:23 GMT - Etag: - - W/"1aa8-J6Eh1EZIPff3WsMWdh+K0M/hEm0" - 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 -- request: - body: - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Length: - - '0' - Content-Type: - - application/json - User-Agent: - - python-requests/2.26.0 - method: POST - uri: https://api.pushover.net/1/messages.json?message=This+is+a+test+notification+from+lunchable.&title=Test&token=XXXXXXXXXX&user=XXXXXXXXXX - response: - content: '{"status":1,"request":"cc6ffda3-e0b0-45d8-9795-738acb1fee76"}' - headers: - Access-Control-Allow-Headers: - - X-Requested-With, X-Prototype-Version, Origin, Accept, Content-Type, X-CSRF-Token, X-Pushover-App, Authorization - Access-Control-Allow-Methods: - - POST, OPTIONS - Access-Control-Allow-Origin: - - '*' - Access-Control-Max-Age: - - '1728000' - CF-Cache-Status: - - DYNAMIC - CF-RAY: - - 6aed1a426cadfbac-MCI - Cache-Control: - - max-age=0, private, must-revalidate - Connection: - - keep-alive - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 16 Nov 2021 01:56:24 GMT - ETag: - - W/"7202009d2b21057015b86aa2aeac5bee" - Expect-CT: - - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" - Referrer-Policy: - - strict-origin-when-cross-origin - Server: - - cloudflare - Strict-Transport-Security: - - max-age=31536000 - Transfer-Encoding: - - chunked - X-Content-Type-Options: - - nosniff - X-Download-Options: - - noopen - X-Frame-Options: - - SAMEORIGIN - - DENY - X-Limit-App-Limit: - - '10000' - X-Limit-App-Remaining: - - '9940' - X-Limit-App-Reset: - - '1638338400' - X-PO-H: - - a - X-Permitted-Cross-Domain-Policies: - - none - X-Request-Id: - - e48465f7-2544-4884-8e29-8d11bcdd2ef6 - X-Runtime: - - '0.066568' - X-XSS-Protection: - - 1; mode=block - http_version: HTTP/1.1 - status_code: 200 -version: 1 diff --git a/tests/plugins/pushlunch/test_pushlunch.py b/tests/plugins/pushlunch/test_pushlunch.py deleted file mode 100644 index 6d3671b6..00000000 --- a/tests/plugins/pushlunch/test_pushlunch.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Run Tests on the Pushover Plugin -""" - -import logging -from typing import List - -from lunchable.models import TransactionObject -from lunchable.plugins.pushlunch.pushover import PushLunch -from tests.conftest import lunchable_cassette - -logger = logging.getLogger(__name__) - - -@lunchable_cassette -def test_send_notification(): - """ - Send a Generic Notification - """ - pusher = PushLunch() - pusher.send_notification( - message="This is a test notification from lunchable.", title="Test" - ) - - -@lunchable_cassette -def test_post_transaction(test_transactions: List[TransactionObject]): - """ - Send - """ - pusher = PushLunch() - example_notification = test_transactions[0] - example_notification.payee = "Test" - example_notification.notes = "Example Test Notification from lunchable" - pusher.post_transaction(transaction=example_notification) diff --git a/tests/plugins/splitlunch/__init__.py b/tests/plugins/splitlunch/__init__.py deleted file mode 100644 index 87582a81..00000000 --- a/tests/plugins/splitlunch/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Splitwise Plugin Testing -""" diff --git a/tests/plugins/splitlunch/cassettes/test_update_balance.yaml b/tests/plugins/splitlunch/cassettes/test_update_balance.yaml deleted file mode 100644 index b4d216b8..00000000 --- a/tests/plugins/splitlunch/cassettes/test_update_balance.yaml +++ /dev/null @@ -1,296 +0,0 @@ -interactions: -- request: - body: - headers: - authorization: - - XXXXXXXXXX - method: GET - uri: https://secure.splitwise.com/api/v3.0/get_current_user - response: - body: - string: '{"user":{"id":1616678,"first_name":"Justin","last_name":"Flannery","picture":{"small":"https://s3.amazonaws.com/splitwise/uploads/user/default_avatars/avatar-orange20-50px.png","medium":"https://s3.amazonaws.com/splitwise/uploads/user/default_avatars/avatar-orange20-100px.png","large":"https://s3.amazonaws.com/splitwise/uploads/user/default_avatars/avatar-orange20-200px.png"},"custom_picture":false,"email":"juftin@gmail.com","registration_status":"confirmed","force_refresh_at":null,"locale":"en","country_code":"VN","date_format":"MM/DD/YYYY","default_currency":"USD","default_group_id":-1,"notifications_read":"2021-09-11T17:37:25Z","notifications_count":34,"notifications":{"added_as_friend":true,"added_to_group":true,"expense_added":false,"expense_updated":false,"bills":true,"payments":true,"monthly_summary":true,"announcements":true}}}' - headers: - Cache-Control: - - no-store - Connection: - - keep-alive - Content-Disposition: - - inline; filename="response.json" - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 16 Nov 2021 02:19:01 GMT - Etag: - - W/"e0c027c87558e7c4714a014dbf70ad90" - Expires: - - Fri, 01 Jan 1990 00:00:00 GMT - Pragma: - - no-cache - Referrer-Policy: - - origin - Server: - - nginx - Strict-Transport-Security: - - max-age=63072000 - Transfer-Encoding: - - chunked - Vary: - - Accept-Encoding - Via: - - 1.1 vegur - X-Content-Type-Options: - - nosniff - X-Download-Options: - - noopen - X-Frame-Options: - - SAMEORIGIN - X-Permitted-Cross-Domain-Policies: - - none - X-Request-Id: - - 3572656c-23d4-4248-8f00-fd8cd85bd9c1 - X-Runtime: - - '0.039383' - X-Xss-Protection: - - 1; mode=block - status: - code: 200 - message: OK -- request: - body: - headers: - authorization: - - XXXXXXXXXX - method: GET - uri: https://secure.splitwise.com/api/v3.0/get_friends - response: - body: - string: '{"friends":[{"id":10910565,"first_name":"XXXXXXXXXX","last_name":"❤️ XXXXXXXXXX","email":"XXXXXXXXXX@gmail.com","registration_status":"unsubscribed","picture":{"small":"https://s3.amazonaws.com/splitwise/uploads/user/default_avatars/avatar-ruby33-50px.png","medium":"https://s3.amazonaws.com/splitwise/uploads/user/default_avatars/avatar-ruby33-100px.png","large":"https://s3.amazonaws.com/splitwise/uploads/user/default_avatars/avatar-ruby33-200px.png"},"balance":[{"currency_code":"USD","amount":"176.67"}],"groups":[{"group_id":0,"balance":[{"currency_code":"USD","amount":"176.67"}]}],"updated_at":"2021-11-15T17:40:10Z"}]}' - headers: - Cache-Control: - - no-store - Connection: - - keep-alive - Content-Disposition: - - inline; filename="response.json" - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 16 Nov 2021 02:19:01 GMT - Etag: - - W/"b21fa77f5f18aa1852ea588667ab46c9" - Expires: - - Fri, 01 Jan 1990 00:00:00 GMT - Pragma: - - no-cache - Referrer-Policy: - - origin - Server: - - nginx - Strict-Transport-Security: - - max-age=63072000 - Transfer-Encoding: - - chunked - Vary: - - Accept-Encoding - Via: - - 1.1 vegur - X-Content-Type-Options: - - nosniff - X-Download-Options: - - noopen - X-Frame-Options: - - SAMEORIGIN - X-Permitted-Cross-Domain-Policies: - - none - X-Request-Id: - - 23d89aee-7935-44eb-85bc-09218f2863fe - X-Runtime: - - '0.037655' - X-Xss-Protection: - - 1; mode=block - status: - code: 200 - message: OK -- request: - body: - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Type: - - application/json - User-Agent: - - python-requests/2.26.0 - authorization: - - XXXXXXXXXX - method: GET - uri: https://dev.lunchmoney.app/v1/tags - response: - content: '[{"id":22215,"name":"Wedding","description":null},{"id":21559,"name":"test","description":null},{"id":24194,"name":"SplitLunchDirectImport","description":null},{"id":21639,"name":"SplitLunch","description":"Tag to Create Splitwise Transactions from Lunch Money"},{"id":23474,"name":"Splitwise","description":null},{"id":22178,"name":"SplitLunchImport","description":null}]' - headers: - Access-Control-Allow-Credentials: - - 'true' - Connection: - - keep-alive - Content-Length: - - '372' - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 16 Nov 2021 02:19:02 GMT - Etag: - - W/"174-NUxNLDDMbGDO7Da7jlNAmQyhgKo" - Server: - - Cowboy - Vary: - - Origin, Accept-Encoding - Via: - - 1.1 vegur - X-Powered-By: - - Express - http_version: HTTP/1.1 - status_code: 200 -- request: - body: - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Type: - - application/json - User-Agent: - - python-requests/2.26.0 - authorization: - - XXXXXXXXXX - method: GET - uri: https://dev.lunchmoney.app/v1/assets - response: - content: '{"assets":[{"id":21845,"type_name":"cash","subtype_name":"digital wallet (paypal, venmo)","name":"Splitwise Balance","display_name":"Splitwise","balance":"176.6700","balance_as_of":"2021-11-16T02:17:52.209Z","currency":"usd","closed_on":null,"institution_name":"Splitwise","created_at":"2021-08-28T16:06:02.701Z"},{"id":23043,"type_name":"cash","subtype_name":"digital wallet (paypal, venmo)","name":"Test Account","display_name":"Test Account","balance":"181.7100","balance_as_of":"2021-11-02T22:27:05.311Z","currency":"usd","closed_on":null,"institution_name":"Test","created_at":"2021-09-20T05:32:29.060Z"}]}' - headers: - Access-Control-Allow-Credentials: - - 'true' - Connection: - - keep-alive - Content-Length: - - '611' - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 16 Nov 2021 02:19:02 GMT - Etag: - - W/"263-O7f1qpDRxlhCsYNKHX/bMe83e1M" - Server: - - Cowboy - Vary: - - Origin, Accept-Encoding - Via: - - 1.1 vegur - X-Powered-By: - - Express - http_version: HTTP/1.1 - status_code: 200 -- request: - body: - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Type: - - application/json - User-Agent: - - python-requests/2.26.0 - authorization: - - XXXXXXXXXX - method: GET - uri: https://dev.lunchmoney.app/v1/categories - 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":null,"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":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-Type: - - application/json; charset=utf-8 - Date: - - Tue, 16 Nov 2021 02:19:02 GMT - Etag: - - W/"1aa8-J6Eh1EZIPff3WsMWdh+K0M/hEm0" - 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 -- request: - body: - headers: - authorization: - - XXXXXXXXXX - method: GET - uri: https://secure.splitwise.com/api/v3.0/get_groups - response: - body: - string: '{"groups":[{"id":0,"name":"Non-group expenses","created_at":"2015-01-31T09:04:03Z","updated_at":"2021-11-16T02:19:02Z","members":[{"id":1616678,"first_name":"Justin","last_name":"Flannery","picture":{"small":"https://s3.amazonaws.com/splitwise/uploads/user/default_avatars/avatar-orange20-50px.png","medium":"https://s3.amazonaws.com/splitwise/uploads/user/default_avatars/avatar-orange20-100px.png","large":"https://s3.amazonaws.com/splitwise/uploads/user/default_avatars/avatar-orange20-200px.png"},"custom_picture":false,"email":"juftin@gmail.com","registration_status":"confirmed","balance":[{"amount":"176.67","currency_code":"USD"}]}],"simplify_by_default":false,"original_debts":[{"from":10910565,"to":1616678,"amount":"176.67","currency_code":"USD"}],"simplified_debts":[{"from":10910565,"to":1616678,"amount":"176.67","currency_code":"USD"}],"avatar":{"small":"https://s3.amazonaws.com/splitwise/uploads/group/default_avatars/v2021/avatar-nongroup-50px.png","medium":"https://s3.amazonaws.com/splitwise/uploads/group/default_avatars/v2021/avatar-nongroup-100px.png","large":"https://s3.amazonaws.com/splitwise/uploads/group/default_avatars/v2021/avatar-nongroup-200px.png","xlarge":"https://s3.amazonaws.com/splitwise/uploads/group/default_avatars/v2021/avatar-nongroup-500px.png","xxlarge":"https://s3.amazonaws.com/splitwise/uploads/group/default_avatars/v2021/avatar-nongroup-1000px.png","original":null},"tall_avatar":{"xlarge":"https://s3.amazonaws.com/splitwise/uploads/group/default_tall_avatars/avatar-nongroup-288px.png","large":"https://s3.amazonaws.com/splitwise/uploads/group/default_tall_avatars/avatar-nongroup-192px.png"},"custom_avatar":false,"cover_photo":{"xxlarge":"https://s3.amazonaws.com/splitwise/uploads/group/default_cover_photos/coverphoto-nongroup-1000px.png","xlarge":"https://s3.amazonaws.com/splitwise/uploads/group/default_cover_photos/coverphoto-nongroup-500px.png"}}]}' - headers: - Cache-Control: - - no-store - Connection: - - keep-alive - Content-Disposition: - - inline; filename="response.json" - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 16 Nov 2021 02:19:02 GMT - Etag: - - W/"5582a2ffe738236da0345ad293182fb0" - Expires: - - Fri, 01 Jan 1990 00:00:00 GMT - Pragma: - - no-cache - Referrer-Policy: - - origin - Server: - - nginx - Strict-Transport-Security: - - max-age=63072000 - Transfer-Encoding: - - chunked - Vary: - - Accept-Encoding - Via: - - 1.1 vegur - X-Content-Type-Options: - - nosniff - X-Download-Options: - - noopen - X-Frame-Options: - - SAMEORIGIN - X-Permitted-Cross-Domain-Policies: - - none - X-Request-Id: - - 318c5cee-3ed4-4ae0-ac70-3eeb2e1c661d - X-Runtime: - - '0.025765' - X-Xss-Protection: - - 1; mode=block - status: - code: 200 - message: OK -version: 1 diff --git a/tests/plugins/splitlunch/data/splitwise_non_involved_expense.json b/tests/plugins/splitlunch/data/splitwise_non_involved_expense.json deleted file mode 100644 index 3e61c9f7..00000000 --- a/tests/plugins/splitlunch/data/splitwise_non_involved_expense.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "id": 2166292526, - "group_id": 374195, - "friendship_id": null, - "expense_bundle_id": null, - "description": "DoorDash", - "repeats": false, - "repeat_interval": null, - "email_reminder": false, - "email_reminder_in_advance": -1, - "next_repeat": null, - "details": null, - "comments_count": 0, - "payment": false, - "creation_method": null, - "transaction_method": "offline", - "transaction_confirmed": false, - "transaction_id": null, - "transaction_status": null, - "cost": "55.32", - "currency_code": "USD", - "repayments": [ - { - "from": 1234102, - "to": 370803, - "amount": "17.9" - } - ], - "date": "2023-02-04T21:04:21Z", - "created_at": "2023-02-04T21:05:40Z", - "created_by": { - "id": 370803, - "first_name": "XXXXXXXXXX", - "last_name": "XXXXXXXXXX", - "picture": { - "medium": "XXXXXXXXXX" - }, - "custom_picture": true - }, - "updated_at": "2023-02-04T21:05:40Z", - "updated_by": null, - "deleted_at": null, - "deleted_by": null, - "category": { - "id": 13, - "name": "Dining out" - }, - "receipt": { - "large": null, - "original": null - }, - "users": [ - { - "user": { - "id": 1234102, - "first_name": "XXXXXXXXXX", - "last_name": "XXXXXXXXXX", - "picture": { - "medium": "XXXXXXXXXX" - } - }, - "user_id": 1234102, - "paid_share": "0.0", - "owed_share": "17.9", - "net_balance": "-17.9" - }, - { - "user": { - "id": 370803, - "first_name": "XXXXXXXXXX", - "last_name": "XXXXXXXXXX", - "picture": { - "medium": "XXXXXXXXXX" - } - }, - "user_id": 370803, - "paid_share": "55.32", - "owed_share": "37.42", - "net_balance": "17.9" - } - ], - "comments": [] -} diff --git a/tests/plugins/splitlunch/data/splitwise_non_involved_transfer.json b/tests/plugins/splitlunch/data/splitwise_non_involved_transfer.json deleted file mode 100644 index 3aa08dab..00000000 --- a/tests/plugins/splitlunch/data/splitwise_non_involved_transfer.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "id": 2004096100, - "group_id": 374195, - "friendship_id": null, - "expense_bundle_id": null, - "description": "Payment", - "repeats": false, - "repeat_interval": null, - "email_reminder": false, - "email_reminder_in_advance": -1, - "next_repeat": null, - "details": null, - "comments_count": 1, - "payment": true, - "creation_method": "payment", - "transaction_method": "offline", - "transaction_confirmed": false, - "transaction_id": null, - "transaction_status": null, - "cost": "716.21", - "currency_code": "USD", - "repayments": [ - { - "from": 370803, - "to": 1234102, - "amount": "716.21" - } - ], - "date": "2022-11-06T01:20:22Z", - "created_at": "2022-11-06T01:20:25Z", - "created_by": { - "id": 1234102, - "first_name": "XXXXXXXXXX", - "last_name": "XXXXXXXXXX", - "picture": { - "medium": "XXXXXXXXXX" - }, - "custom_picture": false - }, - "updated_at": "2022-11-06T01:20:25Z", - "updated_by": null, - "deleted_at": null, - "deleted_by": null, - "category": { - "id": 18, - "name": "General" - }, - "receipt": { - "large": null, - "original": null - }, - "users": [ - { - "user": { - "id": 370803, - "first_name": "XXXXXXXXXX", - "last_name": "XXXXXXXXXX", - "picture": { - "medium": "XXXXXXXXXX" - } - }, - "user_id": 370803, - "paid_share": "0.0", - "owed_share": "716.21", - "net_balance": "-716.21" - }, - { - "user": { - "id": 1234102, - "first_name": "XXXXXXXXXX", - "last_name": "XXXXXXXXXX", - "picture": { - "medium": "XXXXXXXXXX" - } - }, - "user_id": 1234102, - "paid_share": "716.21", - "owed_share": "0.0", - "net_balance": "716.21" - } - ], - "comments": [] -} diff --git a/tests/plugins/splitlunch/data/splitwise_non_user_paid_expense.json b/tests/plugins/splitlunch/data/splitwise_non_user_paid_expense.json deleted file mode 100644 index cd2386d9..00000000 --- a/tests/plugins/splitlunch/data/splitwise_non_user_paid_expense.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "id": 2279028334, - "group_id": 374195, - "friendship_id": null, - "expense_bundle_id": null, - "description": "Toilet paper", - "repeats": false, - "repeat_interval": null, - "email_reminder": false, - "email_reminder_in_advance": -1, - "next_repeat": null, - "details": null, - "comments_count": 0, - "payment": false, - "creation_method": null, - "transaction_method": "offline", - "transaction_confirmed": false, - "transaction_id": null, - "transaction_status": null, - "cost": "29.97", - "currency_code": "USD", - "repayments": [ - { - "from": 1234059, - "to": 370803, - "amount": "9.99" - }, - { - "from": 1234102, - "to": 370803, - "amount": "9.99" - } - ], - "date": "2023-04-04T23:11:38Z", - "created_at": "2023-04-04T23:11:51Z", - "created_by": { - "id": 370803, - "first_name": "XXXXXXXXXX", - "last_name": "XXXXXXXXXX", - "picture": { - "medium": "XXXXXXXXXX" - }, - "custom_picture": true - }, - "updated_at": "2023-04-04T23:11:51Z", - "updated_by": null, - "deleted_at": null, - "deleted_by": null, - "category": { - "id": 14, - "name": "Household supplies" - }, - "receipt": { - "large": null, - "original": null - }, - "users": [ - { - "user": { - "id": 370803, - "first_name": "XXXXXXXXXX", - "last_name": "XXXXXXXXXX", - "picture": { - "medium": "XXXXXXXXXX" - } - }, - "user_id": 370803, - "paid_share": "29.97", - "owed_share": "9.99", - "net_balance": "19.98" - }, - { - "user": { - "id": 1234102, - "first_name": "XXXXXXXXXX", - "last_name": "XXXXXXXXXX", - "picture": { - "medium": "XXXXXXXXXX" - } - }, - "user_id": 1234102, - "paid_share": "0.0", - "owed_share": "9.99", - "net_balance": "-9.99" - }, - { - "user": { - "id": 1234059, - "first_name": "Evan", - "last_name": "Broder", - "picture": { - "medium": "https://splitwise.s3.amazonaws.com/uploads/user/avatar/1234059/medium_headshot.jpg" - } - }, - "user_id": 1234059, - "paid_share": "0.0", - "owed_share": "9.99", - "net_balance": "-9.99" - } - ] -} diff --git a/tests/plugins/splitlunch/data/splitwise_non_user_paid_transfer.json b/tests/plugins/splitlunch/data/splitwise_non_user_paid_transfer.json deleted file mode 100644 index aa3c6d29..00000000 --- a/tests/plugins/splitlunch/data/splitwise_non_user_paid_transfer.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "id": 2229699053, - "group_id": 374195, - "friendship_id": null, - "expense_bundle_id": null, - "description": "Payment", - "repeats": false, - "repeat_interval": null, - "email_reminder": false, - "email_reminder_in_advance": -1, - "next_repeat": null, - "details": null, - "comments_count": 1, - "payment": true, - "creation_method": "splitwise_p2p", - "transaction_method": "splitwise_p2p", - "transaction_confirmed": true, - "transaction_id": "4366d04a-68cf-42f5-a6de-48d0101191f3", - "transaction_status": "completed", - "cost": "523.84", - "currency_code": "USD", - "repayments": [ - { - "from": 1234059, - "to": 370803, - "amount": "523.84" - } - ], - "date": "2023-03-10T17:28:50Z", - "created_at": "2023-03-10T17:28:50Z", - "created_by": { - "id": 370803, - "first_name": "XXXXXXXXXX", - "last_name": "XXXXXXXXXX", - "picture": { - "medium": "XXXXXXXXXX" - }, - "custom_picture": true - }, - "updated_at": "2023-03-15T23:13:59Z", - "updated_by": { - "id": 370803, - "first_name": "XXXXXXXXXX", - "last_name": "XXXXXXXXXX", - "picture": { - "medium": "XXXXXXXXXX" - }, - "custom_picture": true - }, - "deleted_at": null, - "deleted_by": null, - "category": { - "id": 18, - "name": "General" - }, - "receipt": { - "large": null, - "original": null - }, - "users": [ - { - "user": { - "id": 370803, - "first_name": "XXXXXXXXXX", - "last_name": "XXXXXXXXXX", - "picture": { - "medium": "XXXXXXXXXX" - } - }, - "user_id": 370803, - "paid_share": "523.84", - "owed_share": "0.0", - "net_balance": "523.84" - }, - { - "user": { - "id": 1234059, - "first_name": "Evan", - "last_name": "Broder", - "picture": { - "medium": "https://splitwise.s3.amazonaws.com/uploads/user/avatar/1234059/medium_headshot.jpg" - } - }, - "user_id": 1234059, - "paid_share": "0.0", - "owed_share": "523.84", - "net_balance": "-523.84" - } - ], - "comments": [] -} diff --git a/tests/plugins/splitlunch/data/splitwise_user_paid_expense.json b/tests/plugins/splitlunch/data/splitwise_user_paid_expense.json deleted file mode 100644 index fe7e27b5..00000000 --- a/tests/plugins/splitlunch/data/splitwise_user_paid_expense.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "id": 2216325479, - "group_id": 374195, - "friendship_id": null, - "expense_bundle_id": null, - "description": "Bar", - "repeats": false, - "repeat_interval": null, - "email_reminder": false, - "email_reminder_in_advance": -1, - "next_repeat": null, - "details": null, - "comments_count": 0, - "payment": false, - "creation_method": "equal", - "transaction_method": "offline", - "transaction_confirmed": false, - "transaction_id": null, - "transaction_status": null, - "cost": "92.47", - "currency_code": "USD", - "repayments": [ - { - "from": 370803, - "to": 1234059, - "amount": "30.82" - }, - { - "from": 1234102, - "to": 1234059, - "amount": "30.83" - } - ], - "date": "2023-03-01T08:00:00Z", - "created_at": "2023-03-03T17:44:06Z", - "created_by": { - "id": 1234059, - "first_name": "Evan", - "last_name": "Broder", - "picture": { - "medium": "https://splitwise.s3.amazonaws.com/uploads/user/avatar/1234059/medium_headshot.jpg" - }, - "custom_picture": true - }, - "updated_at": "2023-03-03T17:44:06Z", - "updated_by": null, - "deleted_at": null, - "deleted_by": null, - "category": { - "id": 38, - "name": "Liquor" - }, - "receipt": { - "large": null, - "original": null - }, - "users": [ - { - "user": { - "id": 1234059, - "first_name": "Evan", - "last_name": "Broder", - "picture": { - "medium": "https://splitwise.s3.amazonaws.com/uploads/user/avatar/1234059/medium_headshot.jpg" - } - }, - "user_id": 1234059, - "paid_share": "92.47", - "owed_share": "30.82", - "net_balance": "61.65" - }, - { - "user": { - "id": 370803, - "first_name": "XXXXXXXXXX", - "last_name": "XXXXXXXXXX", - "picture": { - "medium": "XXXXXXXXXX" - } - }, - "user_id": 370803, - "paid_share": "0.0", - "owed_share": "30.82", - "net_balance": "-30.82" - }, - { - "user": { - "id": 1234102, - "first_name": "XXXXXXXXXX", - "last_name": "XXXXXXXXXX", - "picture": { - "medium": "XXXXXXXXXX" - } - }, - "user_id": 1234102, - "paid_share": "0.0", - "owed_share": "30.83", - "net_balance": "-30.83" - } - ], - "comments": [] -} diff --git a/tests/plugins/splitlunch/data/splitwise_user_paid_transfer.json b/tests/plugins/splitlunch/data/splitwise_user_paid_transfer.json deleted file mode 100644 index c0038705..00000000 --- a/tests/plugins/splitlunch/data/splitwise_user_paid_transfer.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "id": 1829903595, - "group_id": 34823819, - "friendship_id": null, - "expense_bundle_id": null, - "description": "Payment", - "repeats": false, - "repeat_interval": null, - "email_reminder": false, - "email_reminder_in_advance": -1, - "next_repeat": null, - "details": null, - "comments_count": 1, - "payment": true, - "creation_method": "payment", - "transaction_method": "offline", - "transaction_confirmed": false, - "transaction_id": null, - "transaction_status": null, - "cost": "431.92", - "currency_code": "USD", - "repayments": [ - { - "from": 45374, - "to": 1234059, - "amount": "431.92" - } - ], - "date": "2022-08-02T19:37:30Z", - "created_at": "2022-08-02T19:37:40Z", - "created_by": { - "id": 1234059, - "first_name": "Evan", - "last_name": "Broder", - "picture": { - "medium": "https://splitwise.s3.amazonaws.com/uploads/user/avatar/1234059/medium_headshot.jpg" - }, - "custom_picture": true - }, - "updated_at": "2022-08-02T19:37:40Z", - "updated_by": null, - "deleted_at": null, - "deleted_by": null, - "category": { - "id": 18, - "name": "General" - }, - "receipt": { - "large": null, - "original": null - }, - "users": [ - { - "user": { - "id": 1234059, - "first_name": "Evan", - "last_name": "Broder", - "picture": { - "medium": "https://splitwise.s3.amazonaws.com/uploads/user/avatar/1234059/medium_headshot.jpg" - } - }, - "user_id": 1234059, - "paid_share": "431.92", - "owed_share": "0.0", - "net_balance": "431.92" - }, - { - "user": { - "id": 45374, - "first_name": "XXXXXXXXXX", - "last_name": "XXXXXXXXXX", - "picture": { - "medium": "XXXXXXXXXX" - } - }, - "user_id": 45374, - "paid_share": "0.0", - "owed_share": "431.92", - "net_balance": "-431.92" - } - ], - "comments": [] -} diff --git a/tests/plugins/splitlunch/test_splitwise.py b/tests/plugins/splitlunch/test_splitwise.py deleted file mode 100644 index 457bd9d7..00000000 --- a/tests/plugins/splitlunch/test_splitwise.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -Run Tests on the Splitwise Plugin -""" - -import json -import logging -from os import path - -import pytest - -from tests.conftest import lunchable_cassette - -logger = logging.getLogger(__name__) - - -@pytest.mark.filterwarnings("ignore:datetime.datetime.utcfromtimestamp") -def test_import_splitwise(): - """ - Try to import splitwise and succeed since the extra is installed in tox - """ - test_case = True - try: - from lunchable.plugins.splitlunch.lunchmoney_splitwise import SplitLunch # noqa - except ImportError: - test_case = False - finally: - assert test_case is True - - -@pytest.mark.filterwarnings("ignore:datetime.datetime.utcfromtimestamp") -@lunchable_cassette -def test_update_balance(): - """ - Update the Balance - """ - from lunchable.plugins.splitlunch.lunchmoney_splitwise import SplitLunch - - lunch = SplitLunch() - lunch.update_splitwise_balance() - - -@pytest.mark.filterwarnings("ignore:datetime.datetime.utcfromtimestamp") -def test_financial_impact(): - """ - Test the financial impact algorithm - """ - import splitwise - - from lunchable.plugins.splitlunch.lunchmoney_splitwise import _get_splitwise_impact - - for [file, expected_self_paid, expected_impact] in [ - # For both expenses and transfers, when someone else pays, financial impact should be - # positive - ["splitwise_non_user_paid_expense.json", False, 9.99], - ["splitwise_non_user_paid_transfer.json", False, 523.84], - # When you pay, financial impact should be negative - ["splitwise_user_paid_expense.json", True, -61.65], - ["splitwise_user_paid_transfer.json", True, -431.92], - # And any transaction that doesn't involve you should have no impact - ["splitwise_non_involved_expense.json", False, 0], - ["splitwise_non_involved_transfer.json", False, 0], - ]: - with open(path.join(path.dirname(__file__), f"data/{file}")) as json_file: - expense = splitwise.Expense(json.load(json_file)) - financial_impact, self_paid = _get_splitwise_impact( - expense=expense, current_user_id=1234059 - ) - assert ( - self_paid is expected_self_paid - ), f"Expected {expected_self_paid} for {file}" - assert ( - financial_impact == expected_impact - ), f"Expected {expected_impact} for {file}" From dedc4c1de95e41daffc5043417b5fc837699dfd7 Mon Sep 17 00:00:00 2001 From: juftin Date: Thu, 25 Jan 2024 20:02:25 -0700 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A7=AA=20plugin=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_cli.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index 04e5fd3f..2a1d7d8a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -23,3 +23,16 @@ def test_main_succeeds(runner: CliRunner) -> None: result = runner.invoke(cli) assert result.exit_code == 0 + + +def test_registered_plugins(runner: CliRunner) -> None: + """ + Assert that all registered plugins are available. + """ + from lunchable._cli import cli + + builtin_plugins = ["primelunch", "splitlunch", "pushlunch"] + + for plugin in builtin_plugins: + result = runner.invoke(cli, ["plugins", plugin, "--help"]) + assert result.exit_code == 0