From 9a2f4033ccd5905925d44a37598bdac7c9f94ed5 Mon Sep 17 00:00:00 2001 From: Aleksei Atavin Date: Sat, 19 Aug 2023 09:29:20 +0300 Subject: [PATCH] Add quickstart command --- README.org | 50 +++++++++--- pyproject.toml | 3 + requirements.txt | 1 + ynab_sync/cli.py | 2 + ynab_sync/constants.py | 1 + ynab_sync/logic.py | 14 ++-- ynab_sync/quickstart.py | 177 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 230 insertions(+), 18 deletions(-) create mode 100644 ynab_sync/quickstart.py diff --git a/README.org b/README.org index 5bf5ce7..039263c 100644 --- a/README.org +++ b/README.org @@ -12,13 +12,13 @@ So these are things that may irritate you: - No proper packaging: docker is probably fine but has no way change date range properly (only by editing Dockerfile instead) - poor error handling (there are a lot of cases when you *will* encounter hard to understand errors because of a random bug, unexpected API response, etc..) -* Usage (Not that friendly yet, sorry!) +Currently in order to use the tool you need atleast Python version 3.11. -Currently in order to use the tool you need atleast Python version 3.11 and obtain these unique strings: +You also need to obtain credentials: - Secret ID and Secret Key from your GoCardless account (you can create one free of charge) -- Account ID from GoCardless API connected to your bank account (listed in Step 5 in [[https://developer.gocardless.com/bank-account-data/quick-start-guide][Quick Start Guide]] at GoCardless docs) - YNAB access token (you can grab it from https://app.ynab.com/settings/developer) -- YNAB Budget and Account IDs where you want to import data: https://api.ynab.com/v1#/Budgets/getBudgets and https://api.ynab.com/v1#/Accounts/getAccounts call could help with that (consult https://api.ynab.com/ if needed) + +* Quickstart Clone this repository: #+begin_src sh @@ -30,13 +30,42 @@ Install with pip: python -m pip install . #+end_src -After getting all what is needed and installing requirements (~pip install -r requirements.txt~) you can use ~upload~ -command in order to download transactions from GoCardless Account and upload them to YNAB budget/account: +Go through quickstart script in order to obtain GoCardless Account ID and YNAB Budget and Account ID needed for ~ynab-sync upload~ to work: +#+begin_src sh +ynab-sync quickstart +#+end_src + +Run suggested by script command to add environment variables (example): +#+begin_src sh +export GOCARDLESS_SECRET_ID=fa264...67322 +export GOCARDLESS_SECRET_KEY=96d43...25200 +export GOCARDLESS_COUNTRY=FI +export GOCARDLESS_ACCOUNT_ID=b62ca...fa9e2 +export YNAB_TOKEN=D5rS4..SICe8 +export YNAB_BUDGET_ID=6c4e2...bd8ad +export YNAB_ACCOUNT_ID=adf05...262e +#+end_src + +You can use ~upload~ command now in order to download transactions from GoCardless Account and upload them to YNAB budget/account: #+begin_src sh ynab-sync upload --ynab-token=$YNAB_TOKEN --ynab-budget-id=$YNAB_BUDGET_ID --ynab-account-id=$YNAB_ACCOUNT_ID --gocardless-secret-id=$GOCARDLESS_SECRET_ID --gocardless-secret-key=$GOCARDLESS_SECRET_KEY --gocardless-account-id=$GOCARDLESS_ACCOUNT_ID --date-from=`date -d '-7 day' '+%Y-%m-%d'` #+end_src - + + +** Commands + +- ~ynab-sync upload~ - grabs transactions from GoCardless account for desired period and uploads it to YNAB. +- ~ynab-sync quickstart~ - interactive CLI that will help you to get account/budgets id and configure GoCardless integration with your bank. Note! You need to run this once per bank connection. + +If you curious you can also use those (~ynab-sync quickstart~ should be enough to start) +- ~ynab-sync gocardless banks~ - list of banks that needed for next step (auth) +- ~ynab-sync gocardless generate_bank_auth_link~ - creates http link in order to auth with your bank +- ~ynab-sync gocardless list_requisition_account~ - get GOCARDLESS_ACCOUNT_ID that is nessesary for ~upload~ command +- ~ynab-sync ~ynab budgets~ - lists budgets in your YNAB account (YNAB_BUDGET_ID) +- ~ynab-sync ynab accounts~ - lists accoutns in your YNAB budget (YNAB_ACCOUNT_ID) + + ** Docker usage Probably fastest way to use it in any environment is to use docker container (that is how I currently use it for myself). @@ -57,12 +86,7 @@ docker run --env-file=sandbox.env --rm ynab-sync #+end_src ** Getting budget and account ids -There are three commands that can help you with getting nessesary identifiers (probably buggy, but still) -- ~gocardless banks~ - list of banks that needed for next step (auth) -- ~gocardless generate_bank_auth_link~ - creates http link in order to auth with your bank -- ~gocardless list_requisition_account~ - get GOCARDLESS_ACCOUNT_ID that is nessesary for ~upload~ command -- ~ynab budgets~ - lists budgets in your YNAB account (YNAB_BUDGET_ID) -- ~ynab accounts~ - lists accoutns in your YNAB budget (YNAB_ACCOUNT_ID) + * Development I have an e2e happy path test: feel free to submit a PR :) diff --git a/pyproject.toml b/pyproject.toml index 2dcd302..aef45e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "pydantic", "requests", "tabulate", + "bullet" ] [project.scripts] @@ -24,9 +25,11 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [tool.black] +line-length = 125 [tool.isort] profile = "black" +line-length = 125 [tool.pyright] include = ["ynab_sync"] diff --git a/requirements.txt b/requirements.txt index d6abf96..891db10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ appeal pydantic requests tabulate +bullet diff --git a/ynab_sync/cli.py b/ynab_sync/cli.py index 32c01b4..e2adaf9 100644 --- a/ynab_sync/cli.py +++ b/ynab_sync/cli.py @@ -21,6 +21,8 @@ import logging +from .quickstart import quickstart # noqa + logging.basicConfig( level=logging.INFO, format="%(asctime)s %(name)s [%(levelname)s] %(message)s", diff --git a/ynab_sync/constants.py b/ynab_sync/constants.py index f5213b6..bdbf1ff 100644 --- a/ynab_sync/constants.py +++ b/ynab_sync/constants.py @@ -4,3 +4,4 @@ ENV_GOCARDLESS_SECRET_ID = "GOCARDLESS_SECRET_ID" ENV_GOCARDLESS_SECRET_KEY = "GOCARDLESS_SECRET_KEY" ENV_GOCARDLESS_ACCOUNT_ID = "GOCARDLESS_ACCOUNT_ID" +ENV_GOCARDLESS_COUNTRY = "GOCARDLESS_COUNTRY" diff --git a/ynab_sync/logic.py b/ynab_sync/logic.py index 2646d50..4c5a700 100644 --- a/ynab_sync/logic.py +++ b/ynab_sync/logic.py @@ -5,13 +5,15 @@ from requests import HTTPError -from ynab_sync.gocardless.models import (GoCardlessBankAccountData, - GoCardlessRequisition) +from ynab_sync.gocardless.models import ( + GoCardlessBankAccountData, + GoCardlessInstitution, + GoCardlessRequisition, +) from .gocardless.api import GoCardLessAPI from .ynab.api import YnabAPI -from .ynab.models import (YNABAccount, YNABBudget, YNABTransaction, - YNABTransactions) +from .ynab.models import YNABAccount, YNABBudget, YNABTransaction, YNABTransactions def get_gocardless_transactions( @@ -110,7 +112,9 @@ def get_ynab_budget(token: str, budget_id: UUID) -> YNABBudget: return ynab_api.get_budget(budget_id=budget_id) -def get_gocardless_banks(secret_id: str, secret_key: str, country: str): +def get_gocardless_banks( + secret_id: str, secret_key: str, country: str +) -> list[GoCardlessInstitution]: gocardless_api = GoCardLessAPI(secret_id=secret_id, secret_key=secret_key) return gocardless_api.get_banks(country=country) diff --git a/ynab_sync/quickstart.py b/ynab_sync/quickstart.py new file mode 100644 index 0000000..e34e123 --- /dev/null +++ b/ynab_sync/quickstart.py @@ -0,0 +1,177 @@ +from bullet import Bullet, YesNo +import os + +from ynab_sync.constants import ( + ENV_GOCARDLESS_ACCOUNT_ID, + ENV_GOCARDLESS_SECRET_ID, + ENV_GOCARDLESS_SECRET_KEY, + ENV_GOCARDLESS_COUNTRY, + ENV_YNAB_ACCOUNT_ID, + ENV_YNAB_BUDGET_ID, + ENV_YNAB_TOKEN, +) +from ynab_sync.logic import ( + create_gocardless_requisition, + get_gocardless_banks, + get_gocardless_requisition, + get_ynab_budget, + get_ynab_budgets, +) + +from .cli import app + + +def strip_secret(secret: str) -> str: + return f"{secret[:5]}..{secret[-5:]}" + + +def default_value(value: str | None, strip: bool = False): + if value: + if strip: + value = strip_secret(value) + + return f"({value})" + return "" + + +def gocardless_prompt(debug: bool = False): + env_secret_id = os.environ.get(ENV_GOCARDLESS_SECRET_ID, "") + env_secret_key = os.environ.get(ENV_GOCARDLESS_SECRET_KEY, "") + env_country = os.environ.get(ENV_GOCARDLESS_COUNTRY, "") + + print( + "First, let's add GoCardless credentials and then I will help you ", + "to connect your GoCardless account with your bank instititution.\n", + ) + + secret_id = input(f"Enter GoCardless Secret ID {default_value(env_secret_id, strip=True)}: ") + secret_key = input(f"Enter GoCardless Secret Key {default_value(env_secret_key, strip=True)}: ") + country = input(f"Enter your bank's country ISO code {default_value(env_country)}: ") + + print() + + secret_id = secret_id or env_secret_id + secret_key = secret_key or env_secret_key + country = country or env_country + + print(f"Getting list of bank from GoCardless for country {country}...\n") + + available_banks = get_gocardless_banks( + secret_id=secret_id, + secret_key=secret_key, + country=country, + ) + + cli = Bullet( + prompt="Choose your bank: ", + choices=[bank.name for bank in available_banks], + return_index=True, + ) # type: ignore + _, bank_index = cli.launch() + bank = available_banks[bank_index] + + account_id = "" + account_selected = False + + while not account_selected: + print() + print("Generating authorisation link...\n") + + created_requisition = create_gocardless_requisition( + secret_id=secret_id, + secret_key=secret_key, + redirect="http://localhost", + institution_id=bank.id, + ) + + print( + f"Open this link in your browser and proceed with authorisation:\n", + ) + print(created_requisition.link) + print() + input("Press Enter when you are ready.") + print() + + print("Getting Account ID for your connection...\n") + requisition = get_gocardless_requisition( + secret_id=secret_id, + secret_key=secret_key, + requisition_id=created_requisition.id, + ) + + if not requisition.accounts: + print("No Account ID was returned from GoCardless API. Regenerate link?\n") + cli = YesNo("Regenerate the link? ") + answer = cli.launch() + if not answer: + account_selected = True + elif len(requisition.accounts) > 1: + cli = Bullet( + prompt="Looks like you have multiple accounts that you can use, choose one:", + choices=requisition.accounts, + ) # type: ignore + account_id = cli.launch() + account_selected = True + else: + account_id = requisition.accounts[0] + account_selected = True + + return secret_id, secret_key, account_id + + +def ynab_prompt(debug: bool = False): + print("\nNow let's configure YNAB credentials and I will help you get Budget and Account IDs\n") + + env_token = os.environ.get(ENV_YNAB_TOKEN) + token = input(f"Enter YNAB access token {default_value(env_token, strip=True)}:") or env_token + print() + print("Getting YNAB budgets list...") + + budgets = get_ynab_budgets(token=token) + + cli = Bullet( + prompt="Choose into which budget you want to upload transactions: ", + choices=[budget.name for budget in budgets], + return_index=True, + ) # type: ignore + _, budget_index = cli.launch() + + budget = budgets[budget_index] + + print() + print(f"Getting account list for budget {budget.name}") + + budget = get_ynab_budget(token=token, budget_id=budget.id) + + cli = Bullet( + prompt="Choose into which budget you want to upload transactions: ", + choices=[account.name for account in budget.accounts], + return_index=True, + ) # type: ignore + _, account_index = cli.launch() + + account = budget.accounts[account_index] + + return token, budget.id, account.id + + +@app.command() +def quickstart(*, debug: bool = False): + print( + "This tool will help you to generate .env file that ", + "is nessesarry for `ynab-sync` upload to work.\n", + ) + + gocardless_secret_id, gocardless_secret_key, gocardless_account_id = gocardless_prompt(debug=debug) + + ynab_token, ynab_budget_id, ynab_account_id = ynab_prompt(debug=debug) + + print() + print("These are environment variables that you can use in `upload` command") + print() + print(f"export {ENV_GOCARDLESS_SECRET_ID}={gocardless_secret_id}") + print(f"export {ENV_GOCARDLESS_SECRET_KEY}={gocardless_secret_key}") + print(f"export {ENV_GOCARDLESS_ACCOUNT_ID}={gocardless_account_id}") + print(f"export {ENV_YNAB_TOKEN}={ynab_token}") + print(f"export {ENV_YNAB_BUDGET_ID}={ynab_budget_id}") + print(f"export {ENV_YNAB_ACCOUNT_ID}={ynab_account_id}")