From ff1d06b06c0ce7f9c99747e4a1edb80b6991858d Mon Sep 17 00:00:00 2001 From: juftin Date: Wed, 24 Jan 2024 22:45:18 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20plugin=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plugins/index.md | 204 ++++++++++++++++++++++++++++- docs/plugins/primelunch.md | 2 +- docs/plugins/pushlunch.md | 2 +- docs/plugins/splitlunch.md | 4 +- mkdocs.yaml | 2 +- requirements.txt | 10 ++ requirements/requirements-lint.txt | 2 +- 7 files changed, 215 insertions(+), 11 deletions(-) diff --git a/docs/plugins/index.md b/docs/plugins/index.md index ae715937..ad635d48 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -1,10 +1,204 @@ # Plugins -lunchable supports plugins with other, external, services. 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 install all the known plugins, and their dependencies, install +lunchable with the `plugins` extra: -### [PushLunch](pushlunch.md): Push Notifications via Pushover +```shell +pipx install "lunchable[plugins]" +``` -### [SplitLunch](splitlunch.md): Splitwise Integration +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: -### [PrimeLunch](primelunch.md): Amazon Transaction Updater +- [PushLunch](pushlunch.md): Push Notifications via Pushover +- [SplitLunch](splitlunch.md): Splitwise Integration +- [PrimeLunch](primelunch.md): Amazon Transaction Updater + +## LunchableApp + +Lunchable provides a [LunchableApp](#lunchable.plugins.LunchableApp) +class that can be used to easily build plugins, apps, +and more. Notice a few of the main attributes / methods of the `LunchableApp` class: + + attribute / method | description | type +------------------------------------------------------------------------------|--------------------------------------------------------------------------------|------------------------------------------------------- + **`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` + +> ¹ 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`. +> These attributes (except for `user`) are `dict[int, LunchableModel]` objects, where the key is +> the ID of the object and the value is the object itself. + +> ² 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. + +> ³ This the same as running `app.data.transactions.clear()` + +### An Example App + +```python +from __future__ import annotations + +from typing import Any + +from lunchable.models import AssetsObject, TransactionUpdateObject +from lunchable.plugins import LunchableApp + + +class MyCustomApp(LunchableApp): + """ + My Custom App + """ + + def do_something_with_assets(self) -> None: + """ + Do something with the assets + """ + if not self.data.assets: + # If the data hasn't been loaded yet, load it + # The following method loads all of the data besides Transactions + # (Assets, Categories, Plaid Accounts, Tags, Crypto, User) + self.refresh_data() + for asset_id, asset in self.data.assets.items(): + # Do something with the asset + print(asset_id, asset) + + def do_something_with_transactions(self) -> None: + """ + Do something with the transactions + """ + if not self.data.transactions: + # If the transactions haven't been loaded yet, load them + self.refresh_transactions(start_date="2021-01-01", end_date="2021-01-31") + # Refresh the latest assets + latest_assets: dict[int, AssetsObject] = self.refresh(model=AssetsObject) + for transaction_id, transaction in self.data.transactions.items(): + if transaction.asset_id: + asset = latest_assets[transaction.asset_id] + print(transaction_id, transaction, asset) + + def update_transaction(self, transaction_id: int, payee: str) -> dict[str, Any]: + """ + You can do anything you want with the `self + """ + update_transaction = TransactionUpdateObject(payee=payee) + response = self.lunch.update_transaction(transaction_id=transaction_id, + transaction=update_transaction) + return response + + +if __name__ == "__main__": + app = MyCustomApp(access_token="xxxxxxxx") + app.do_something_with_assets() + app.do_something_with_transactions() + app.update_transaction(transaction_id=12345, payee="New Payee") +``` + +#### Choose a subset of data to load + +If you don't want to load all of the data, you can specify which data you want to load by +specifying the `lunchable_models` attribute of the `LunchableApp` class. The following example +will only sync the `assets` and `plaid_accounts` data when the `refresh_data()` method is called: + +```python +from __future__ import annotations + +from typing import ClassVar + +from lunchable.models import AssetsObject, PlaidAccountObject, LunchableModel + +from lunchable.plugins import LunchableApp + + +class CustomApp(LunchableApp): + """ + Custom Lunchable App + + This app syncs Plaid Accounts and Assets when its `refresh_data` method + is called. + """ + + lunchable_models: ClassVar[list[type[LunchableModel]]] = [ + PlaidAccountObject, + AssetsObject, + ] + + def do_something_with_assets(self) -> None: + """ + Do something with the assets + """ + if not self.data.plaid_accounts: + self.refresh_data() + for plaid_account_id, plaid_account in self.data.plaid_accounts.items(): + print(plaid_account_id, plaid_account) +``` + +## Building a Plugin + +Plugins are built separate Python packages and are detected by lunchable via +the `lunchable.cli` entrypoint, these are +[click](https://github.com/pallets/click/) command line applications. +To add your own plugin to lunchable you'll need to add a new entrypoint to +your package. The below example shows how to do this with hatch, a modern, +standards-based Python package manager: + +```python +import click + + +@click.group +def plugin_name(): + """ + Plugin description + """ + pass + + +@plugin_name.command +def command(): + """ + Plugin description + """ + pass +``` + +```toml +[project.entry-points."lunchable.cli"] +your-package = "your_package.cli:plugin_name" +``` + +The above example will add a new `command` / `group` to the lunchable `plugins` CLI. When +your package is installed into the same environment as lunchable, your plugin will be +accessible via the `lunchable plugins` command: + +```shell +lunchable plugins plugin-name command +``` + +## API Documentation + +::: lunchable.plugins.LunchableApp + handler: python + options: + show_bases: false + allow_inspection: true + inherited_members: true + group_by_category: true + heading_level: 3 + show_source: false + +::: lunchable.plugins.app.LunchableData + handler: python + options: + show_bases: false + allow_inspection: true + group_by_category: true + heading_level: 3 + show_source: false diff --git a/docs/plugins/primelunch.md b/docs/plugins/primelunch.md index f31e4a07..071b6511 100644 --- a/docs/plugins/primelunch.md +++ b/docs/plugins/primelunch.md @@ -96,7 +96,7 @@ lunchable plugins primelunch run \ ## Command Line Documentation ::: mkdocs-click - :module: lunchable._cli + :module: lunchable.plugins.primelunch.cli :command: run_primelunch :prog_name: lunchable plugins primelunch run :style: table diff --git a/docs/plugins/pushlunch.md b/docs/plugins/pushlunch.md index 1e498676..fe96821e 100644 --- a/docs/plugins/pushlunch.md +++ b/docs/plugins/pushlunch.md @@ -43,7 +43,7 @@ docker run --rm \ from lunchable.plugins.pushlunch import PushLunch ``` -::: lunchable.plugins.pushlunch.PushLunch +::: lunchable.plugins.pushlunch.pushover.PushLunch handler: python options: show_bases: false diff --git a/docs/plugins/splitlunch.md b/docs/plugins/splitlunch.md index e184ec1a..654132e1 100644 --- a/docs/plugins/splitlunch.md +++ b/docs/plugins/splitlunch.md @@ -103,10 +103,10 @@ docker run \ ## Run via Python ```python -from lunchable.plugins.splitlunch import SplitLunch +from lunchable.plugins.splitlunch.lunchmoney_splitwise import SplitLunch ``` -::: lunchable.plugins.splitlunch.SplitLunch +::: lunchable.plugins.splitlunch.lunchmoney_splitwise.SplitLunch handler: python options: show_bases: false diff --git a/mkdocs.yaml b/mkdocs.yaml index 7916f84b..6a8276a0 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -5,7 +5,7 @@ nav: - Home 🏠: index.md - Usage 📖: usage.md - LunchMoney 🍽️: interacting.md -- Plugins 🧩: +- Apps and Plugins 🧩: - plugins/index.md - PushLunch 📲: plugins/pushlunch.md - SplitLunch 🍱: plugins/splitlunch.md diff --git a/requirements.txt b/requirements.txt index a078801f..9f277e65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,10 @@ # # This file is autogenerated by hatch-pip-compile with Python 3.11 # +# - click-plugins>=1.1.1 # - click>=8.0.1 # - httpx +# - importlib-metadata>=3.6 # - pydantic<3,>=2 # - rich>=10.0.0 # - pandas @@ -22,6 +24,10 @@ certifi==2023.11.17 charset-normalizer==3.3.2 # via requests click==8.1.7 + # via + # hatch.envs.default + # click-plugins +click-plugins==1.1.1 # via hatch.envs.default h11==0.14.0 # via httpcore @@ -34,6 +40,8 @@ idna==3.6 # anyio # httpx # requests +importlib-metadata==7.0.1 + # via hatch.envs.default markdown-it-py==3.0.0 # via rich mdurl==0.1.2 @@ -80,3 +88,5 @@ tzdata==2023.3 # via pandas urllib3==2.1.0 # via requests +zipp==3.17.0 + # via importlib-metadata diff --git a/requirements/requirements-lint.txt b/requirements/requirements-lint.txt index f158596b..f8acc070 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: 1d7f43c7c1ecbc074ece84ddfddf590007473fce44e813c9dca4481292391f67) +# [constraints] requirements.txt (SHA256: fab1a8fdd5601fab426b875234fb9b2fa894f0e59667df5182eb30bc9d261f69) # # - mypy>=1.6.1 # - ruff~=0.1.7