Adding Entities - AAraKKe/ynab-unlinked GitHub Wiki
Adding new Entities
YNAB Unlinked has been built to allow new Entities to be added by anyone that needs them. This means that, while it has the pontential to support any entity you need, support is limited to those that are supported at the moment.
To add support for a new Entity we just need:
- To create the new entity object
- To define its load command
The Entity protocol
Every supported entity in YNAB Unlinked is a class that must follow the Entity protocol. This means that the class must have the following methods:
def parse(self, input_file: Path, context: YnabUnlinkedContext) -> list[Transaction]
def name(self) -> str
Any class that implements those method is a valid entity. The name
method should return a string that has a human readable name of your entity. It is used for console output.
The parse method
The parse method is the one that contains the logic about how to turn any input file into a list of Trasaction
objects. A Transaction
object is just a dataclass the date of the transaction, the payee and the amount.
YNAB Unlinked works on this abstraction level to dettach itself from any details of any particular entity. As long as there is a class that can turn an input file into a list of transaction, that is all we need.
The method receives the YnabUnlinkedContext
object that contains the following information:
@dataclass
class YnabUnlinkedContext[T]:
config: Config
extras: T
show: bool = False
reconcile: bool = False
config
: the current YNAB Unlinked configurationshow
: whether the user wants to just show the transactions andn ot load them to YNAB.reconcile
: whether the user wants to reconcile the transactionsextras
: this is wehre any extra information from the command of this entity can be store. The type isT
defined when created so it can be properly typed on the parse command to be used apropriately.
Lets imagine I want to create an entity for MyOldBank
, the entity class would live in src/ynab_unlinked/entities/oldbank
. The name of the package will be used to name the command through which this entity is exposed. This old bank only processes mortgages payments and the date is always going to be the same, provided by the user. This is a very simple entity, but works fine as an example.
My entity class can look like:
# old_bank.py
import datetime as dt
from dataclasses import dataclass
from pathlib import Path
from ynab_unlinked.context_object import YnabUnlinkedContext
from ynab_unlinked.models import Transaction
@dataclass
class OldBankContext:
date: dt.date
class MyOldBank:
def parse(
self, input_file: Path, context: YnabUnlinkedContext[OldBankContext]
) -> list[Transaction]:
return [
Transaction(
date=context.extras.date, payee="Old Bank", amount=float(line.strip())
)
for line in input_file.read_text().splitlines()
]
def name(self) -> str:
return "Old Bank"
The entity command
Having the class representing your entity is the first step to be able to load information into YNAB. The next one is exposing it through the entity command. The command is a function that will be loaded as a Typer command. This means the arguments should be decorated accordingly for any option you want to expose through the command.
The context
and input_file
arguments must always be provided. The context is needed to process transactions (as we will see below) and the input file is needed to read transactions from. We will provide another one, date
that contains the date to be used when parsing the transactions.
Lets create the command now
# command.py
import datetime as dt
from pathlib import Path
import typer
from typing_extensions import Annotated
from ynab_unlinked.context_object import YnabUnlinkedContext
from ynab_unlinked.process import process_transactions
from .old_bank import MyOldBank, OldBankContext
def command(
context: typer.Context,
input_file: Annotated[
Path,
typer.Argument(exists=True, file_okay=True, dir_okay=False, readable=True),
],
date: Annotated[
dt.datetime,
typer.Option(
"-d",
"--date",
help="The date these mortgage payments refer to",
required=True,
),
] = dt.datetime.now(),
):
print("Running now my command for My Old Bank!")
# Get the object from the typer context needed to attach the date to it
ctx: YnabUnlinkedContext = context.obj
ctx.extras = OldBankContext(date=date)
# This method call kick-starts the processing
process_transactions(
entity=MyOldBank(),
input_file=input_file,
context=ctx,
)
The only thing missing is exporting the command
method in the oldbank
pacakge
# __init__.py
from .command import command
__all__ = ["command"]
And we can now execute our command!
hatch run yul load oldbank --help
Usage: yul load oldbank [OPTIONS] INPUT_FILE
╭─ Arguments ────────────────────────────────────────────────────────────────────────────────────────╮
│ * input_file FILE [default: None] [required] │
╰────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Options ──────────────────────────────────────────────────────────────────────────────────────────╮
│ --date -d [%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d The date these mortgage payments refer │
│ %H:%M:%S] to │
│ [default: 2025-05-02 17:54:01.275438] │
│ --help Show this message and exit. │
╰────────────────────────────────────────────────────────────────────────────────────────────────────╯