Skip to content

Commit

Permalink
Add quickstart command
Browse files Browse the repository at this point in the history
  • Loading branch information
axeoman committed Aug 24, 2023
1 parent 4bf224b commit 9a2f403
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 18 deletions.
50 changes: 37 additions & 13 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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).

Expand All @@ -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 :)
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies = [
"pydantic",
"requests",
"tabulate",
"bullet"
]

[project.scripts]
Expand All @@ -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"]
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ appeal
pydantic
requests
tabulate
bullet
2 changes: 2 additions & 0 deletions ynab_sync/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions ynab_sync/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
14 changes: 9 additions & 5 deletions ynab_sync/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)

Expand Down
177 changes: 177 additions & 0 deletions ynab_sync/quickstart.py
Original file line number Diff line number Diff line change
@@ -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}")

0 comments on commit 9a2f403

Please sign in to comment.