Function Reference

This is a docstrings based and automatically generated Python function reference of Bankr.

The mandatory and optional Figures are:

# Init fixed Bankr parameters
MANDATORIES = ["date", "iban", "cents", "currency", "offset", "category", "tag", "source"]
OPTIONALS = ["valuta", "name", "process", "description", "creditor", "mandate", "customer"]

The Command Line Interface

The CLI is described briefly in this documentation under Home. However, the preferable source of help is bankr [COMMAND] -h. The CLI itself is in English language, however, the output of Bankr is translated, currently to English or German.

The commands of Bankr's command line interface.

add(iban='', tag='', asset=False)

Add an Entry to the Book.

Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("iban")
@click.option("-a", "--asset", is_flag=True, help="Asset mode: Calculate entry from balances")
@click.option("-t", "--tag", type=click.STRING, help="Tag to apply to entries")
def add(iban: str = "", tag: str = "", asset: bool = False) -> None:
    """Add an Entry to the Book."""

    bh.title("Add an Entry")
    iban = bh.sanitize_iban(iban)
    if not iban:
        sys.exit("Bankr Error - No IBAN given.")
    entry = by.edit(by.empty(iban), asset_mode=asset, tag=tag)
    book = bk.unpickle()
    book = bp.concat_pages(entry, book)
    by.show(entry)
    bh.info("Add this Entry?")
    if bh.confirm_save():
        bk.pickle(book)
    else:
        bh.info("Entry not added.")

app(cmd)

Start or stop the Bankr App.

Parameters:

Name Type Description Default
cmd str

start (default) or stop the Dashboard.

required
Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("cmd", default="start")
def app(cmd: str) -> None:
    """Start or stop the Bankr App.

    Args:
        cmd (str): `start` (default) or `stop` the Dashboard.
    """
    if cmd.lower() == "start":
        bh.info("Starting App (http://localhost:5006)...")
        ba.start_app()
    elif cmd.lower() == "stop":
        ba.stop_app()
        bh.info("App stopped")
    else:
        bh.info("Bankr does not understand")

cat(auto, entry_id)

Categorize Entries of a Page or the Book.

Basis of the automatic categorization is filters.yaml. Manual and auto cat work only on entries without a category.

Source code in src/bankr/cli/commands.py
@click.command()
@click.option("-a", "--auto", is_flag=True, help="Auto categorize the Book")
@click.option("-e", "--entry_id", type=click.STRING, help="Categorize an Entry ID")
def cat(auto: bool, entry_id: str):
    """Categorize Entries of a Page or the Book.

    Basis of the automatic categorization is `filters.yaml`. Manual and auto cat work only
    on entries without a category.
    """

    book = bk.unpickle()
    if entry_id:
        try:
            entry = book.loc[["entry_id"]]
        except KeyError:
            sys.exit("Bankr Error - Invalid Entry ID")
        bh.title("Categorizing Entry ID")
        by.show(entry, details=True)
        selection = bh.select_category()
        if selection:
            book.loc[entry_id, "category"] = selection
            bh.info(f"{i18n.t('cli.entry_id')} {entry_id} {i18n.t('cli.categorized')}")
    elif auto:
        bh.title("Auto Categorizing")
        bp.categorize(book)
        bh.info(f"{i18n.t('cli.book_categorized')} - {sum(book['category'] == 'none')} {i18n.t('cli.wo_cat')}")
    else:
        bh.title("Categorizing")
        bp.categorize_manually(book)
        bh.info(f"{i18n.t('cli.book_categorized')} - {sum(book['category'] == 'none')} {i18n.t('cli.wo_cat')}")
    if bh.confirm_save():
        bk.pickle(book)

delete(entry_id)

Delete an Entry from the Book.

Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("entry_id")
def delete(entry_id: str) -> None:
    """Delete an Entry from the Book."""

    bh.title("Delete an Entry")
    book = bk.unpickle()
    entry = bh.locate_entry(book, entry_id)
    book = book.drop(entry_id)
    by.show(entry)
    bh.info("Delete this Entry?")
    if bh.confirm_save():
        bk.pickle(book)
    else:
        bh.info("Entry not deleted.")

edit(entry_id, bulk, tag)

Edit an Entry of the Book or perform bulk edits on multiple entries.

Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("entry_id", required=False)
@click.option("-b", "--bulk", type=click.STRING, help="Comma-separated entry IDs for bulk editing")
@click.option("-t", "--tag", type=click.STRING, default="none", help="Tag to apply to entries")
def edit(entry_id: str, bulk: str, tag: str):
    """Edit an Entry of the Book or perform bulk edits on multiple entries."""
    bh.title("Edit Entries")
    book = bk.unpickle()

    # Handle bulk editing
    if bulk:
        if entry_id:
            bh.info("Ignoring entry_id parameter when using --bulk option")
            entry_id = None

        # Sanitize entry IDs
        entry_ids = bh.sanitize_entry_ids(bulk)
        if not entry_ids:
            sys.exit("Bankr Error - No valid entry IDs provided for bulk editing.")

        # Sanitize tag
        sanitized_tag = bh.sanitize_tag(tag)
        if not sanitized_tag:
            sys.exit("Bankr Error - Invalid tag provided.")

        # Perform bulk edit
        not_found = by.bulk_edit(book, entry_ids, tag=sanitized_tag)

        # Provide feedback
        if not_found:
            bh.info(f"Bankr Warning - {len(not_found)} entry IDs not found: {', '.join(not_found)}")
        else:
            bh.info(f"Successfully updated {len(entry_ids)} entries with tag '{bh.describe_tag(sanitized_tag)}'")

        # Confirm save
        if bh.confirm_save():
            bk.pickle(book)
        else:
            bh.info("Bulk edit not saved.")

    # Handle single entry editing
    elif entry_id:
        entry = bh.locate_entry(book, entry_id)
        entry = by.edit(entry)
        book = bp.concat_pages(book.drop(entry_id), entry)
        by.show(entry)
        bh.info("Edit this Entry?")
        if bh.confirm_save():
            bk.pickle(book)
        else:
            bh.info("Entry not edited.")

    else:
        sys.exit("Bankr Error - Either provide an entry_id or use --bulk option")

entry(entry_id, details)

Show an Entry of the Book.

Source code in src/bankr/cli/commands.py
@click.command()
@click.option("-d", "--details", is_flag=True, help="Show the details (optionals) of an Entry.")
@click.argument("entry_id")
def entry(entry_id: str, details: bool) -> None:
    """Show an Entry of the Book."""

    book = bk.unpickle()
    try:
        entry = book.loc[[entry_id]]
    except KeyError:
        sys.exit("Bankr Error - Entry not found.")
    by.show(entry, details=details, header=True)

iban(all)

Show the Accounts of the Book.

Source code in src/bankr/cli/commands.py
@click.command()
@click.option("-a", "--all", is_flag=True, help="Show all Accounts of the Book")
def iban(all: bool):
    """Show the Accounts of the Book."""

    book = bk.unpickle()
    active = not all
    bp.show_accounts(book, active=active)

new()

Create a new Book in DATA_PATH, see bankr.yaml.

Creates an almost empty Book, which contains a zero value "Initial Entry" for each account in accounts.yaml.

Source code in src/bankr/cli/commands.py
@click.command()
def new():
    """Create a new Book in DATA_PATH, see `bankr.yaml`.

    Creates an almost empty Book, which contains a zero value "Initial Entry" for each
    account in `accounts.yaml`.
    """

    bh.title("Create new Book")
    book = bk.create_new_book()
    if bh.confirm_save():
        bk.pickle(book)
        bh.info("Return to a backup, if processed erroneously.")

parse(csv_file, add, verbose)

Parse CSV for a Page, and eventually add it to the Book.

A summary of the Transactions of the Page, and the expectable changes of the Book are presented. The Book is not changed in Parse Mode. If in Add Mode, an auto categorized Page is added to the Book.

Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("csv_file")
@click.option("-a", "--add", is_flag=True, help="Add the auto-cat Page to the Book")
@click.option("-v", "--verbose", is_flag=True, help="Provide info about processing steps")
def parse(csv_file: str, add: bool, verbose: bool):
    """Parse CSV for a Page, and eventually add it to the Book.

    A summary of the Transactions of the Page, and the expectable changes of the Book are presented.
    The Book is not changed in Parse Mode. If in Add Mode, an auto categorized Page is added to the Book.
    """

    subtitle = "Parse Mode"
    if add:
        subtitle = "Add Mode"
    bh.title(subtitle)

    # Parse CSV to Page
    page = bp.parse(csv_file)
    book = bk.unpickle()

    # Auto-categorize Page
    bp.categorize(page, verbose=True)

    if add:
        # Import Page to Book, and save it
        book = bp.concat_pages(page, book)
        bk.pickle(book)
    else:
        # Print statistics of the relevant IBAN
        iban = csv_file.split("-")[0]
        click.echo(f"- {i18n.t('cli.page')}")
        page_cents = bp.show_iban_statistics(page, iban)
        click.echo(f"- {i18n.t('cli.book')}")
        book_cents = bp.show_iban_statistics(book, iban)
        new_balance = f"{(book_cents + page_cents):.2f}{ANCHOR_CURRENCY}"
        click.echo(f"  {i18n.t('cli.new_balance'):<32}{new_balance:>10}\n")

        # Ask if entries of page should be shown
        selection = input("Bankr Action - Show Entries of the created Page (y/n)? ")
        if selection.lower() != "y":
            return

        # Check if page has too many entries
        if page.shape[0] > 500:
            bh.info("Maximum number of Entries is 500.")
            return

        # Show entries and ask for specific entry
        bp.show(page, enum=True)
        entry_no_input = input("Bankr Action - Show # of Entry? ")

        # Validate and show specific entry
        try:
            entry_no = int(entry_no_input)
            if 0 <= entry_no < page.shape[0]:
                entry = page.iloc[[entry_no]]
                by.show(entry)
        except ValueError:
            pass

show(period, category, entry_id, iban, enum, check_external)

Show filtered Entries of the Book (max 500).

Source code in src/bankr/cli/commands.py
@click.command()
@click.option("-p", "--period", default="-", type=click.STRING, help="Period to show")
@click.option("-c", "--category", default="", type=click.STRING, help="Category to show")
@click.option("-e", "--entry_id", is_flag=True, help="Show Entry IDs")
@click.option("-i", "--iban", default="", type=click.STRING, help="IBAN to show")
@click.option("-n", "--enum", is_flag=True, help="Enumerate Entries")
@click.option("-x", "--check_external", is_flag=True, help="Check if offset is external")
def show(period: str, category: str, entry_id: bool, iban: str, enum: bool, check_external: bool):
    """Show filtered Entries of the Book (max 500)."""

    # Sanitize parameters and collect internal accounts
    bounds = bh.sanitize_period(period)
    category = bh.sanitize_category(category)
    iban = bh.sanitize_iban(iban)
    offsets = []
    if check_external:
        offsets = [account["iban"] for account in ACCOUNTS]

    # Load Book, extract and show Page
    bh.title("Page Mode")
    book = bk.unpickle()
    # page = bk.extract_from_book(book, bounds=bounds, category=category, iban=iban, offsets=offsets)
    page = bp.extract(book, bounds=bounds, category=category, iban=iban, offsets=offsets)
    if page.shape[0] > 500:
        bh.info("Maximum number of Transactions is 500.")
    else:
        bp.show(page, entry_id=entry_id, enum=enum, header=False)

stats(epoch, quarter, half, year, nototals)

Statistics of a Page or the Book per Period.

Source code in src/bankr/cli/commands.py
@click.command()
@click.option("-e", "--epoch", type=click.STRING, default="-", help="Epoch to show")
@click.option("-q", "--quarter", is_flag=True, help="Show quarters")
@click.option("-h", "--half", is_flag=True, help="Show half-years/terms")
@click.option("-y", "--year", is_flag=True, help="Show years")
@click.option("-T", "--nototals", is_flag=True, default=False, help="Block totals")
def stats(epoch: str, quarter: bool, half: bool, year: bool, nototals: bool):
    """Statistics of a Page or the Book per Period."""
    book = bk.unpickle()
    bounds: list[datetime] = bh.sanitize_period(epoch)
    period = "m"
    if quarter:
        period = "quarter"
    if half:
        period = "term"
    if year:
        period = "year"
    page = bp.extract(book, bounds=bounds)
    amounts = bp.amounts(page, period=period, pivot=True)
    if not nototals:
        amounts = bk.append_totals(amounts)
    bp.show_amounts(amounts)

test()

For development purposes only.

Source code in src/bankr/cli/commands.py
@click.command()
# @click.argument("csv_file")
def test():
    """For development purposes only."""

Book module bk for Bankr.

This module provides functions on the Book, especially reading and saving it.

append_totals(amounts)

Calculate incomes/expenses/balances per row, and column totals.

It expects an "amounts-type" dataframe. Firstly, it appends it with three columns, the total incomes per time interval (row), the total expenses, and the balance (sum). Secondly, it adds a row with column totals.

Source code in src/bankr/book.py
def append_totals(amounts: pd.DataFrame) -> pd.DataFrame:
    """Calculate incomes/expenses/balances per row, and column totals.

    It expects an "amounts-type" dataframe. Firstly, it appends it with three columns,
    the total incomes per time interval (row), the total expenses, and the balance (sum).
    Secondly, it adds a row with column totals.
    """

    # TODO Works, but needs refactoring.

    amounts.loc["total"] = amounts.sum(axis=0)
    income = amounts.mask(amounts < 0, 0)
    income["internal"] = 0
    expense = amounts.mask(amounts > 0, 0)
    expense["internal"] = 0
    amounts["income"] = income.sum(axis=1)
    amounts["expense"] = expense.sum(axis=1)
    amounts["balance"] = amounts["income"] + amounts["expense"]

    return amounts  # TODO Remove categories, return totals only

create_new_book()

tbc

Source code in src/bankr/book.py
def create_new_book() -> Page:
    """tbc"""

    ibans = [account["iban"] for account in ACCOUNTS]
    book = by.empty(ibans[0])
    ibans = ibans[1:]
    for iban in ibans:
        book = bp.concat_pages(book, by.empty(iban))
    book["source"] = "Initial Entry"
    book["description"] = "Initial Entry"

    return book

get_assets(iban, **kwargs)

Calculate assets for an investment iban in cents.

Parameters:

Name Type Description Default
iban str

The IBAN of the account to calculate the balance for

required
**kwargs

Additional optional parameters: - page (Page | None): The Book or a Page, see project wiki. If None (default), loads Book from disk using unpickle(). TODO Other kwargs (filter parameters) may be added in future versions.

{}

Returns:

Type Description
dict

The current balance in cents (integer)

Source code in src/bankr/book.py
def get_assets(iban: str, **kwargs) -> dict:
    """Calculate assets for an investment iban in cents.

    Args:
        iban: The IBAN of the account to calculate the balance for
        **kwargs: Additional optional parameters:
            - page (Page | None): The Book or a Page, see project wiki.
              If None (default), loads Book from disk using unpickle().
              TODO Other kwargs (filter parameters) may be added in future versions.

    Returns:
        The current balance in cents (integer)
    """
    # Prepare mandatory (unique fragments of) IBAN
    iban = bh.sanitize_iban(iban)

    # Prepare optional kwargs
    params = {"page": None, "tag": ""}
    params |= kwargs
    page = params["page"]
    tag = params["tag"]

    if tag:
        tag = bh.sanitize_tag(tag)

    # If page is None, use the validated Book
    if page is None:
        page = bp.validate(unpickle())

    page = bp.validate(page)

    # Filter entries for the given IBAN and category/tag, calculate sum
    invests = page[(page["iban"] == iban) & (page["category"] == "invest")]
    assets = page[(page["iban"] == iban) & (page["category"] == "asset")]
    if tag:
        invest_cents = - invests[invests["tag"] == tag]["cents"].sum()
        asset_cents = assets[assets["tag"] == tag]["cents"].sum()
    else:
        invest_cents = - invests["cents"].sum()
        asset_cents = assets["cents"].sum()

    # Convert from numpy.int64 to Python int
    return {"invest": int(invest_cents), "asset": int(asset_cents)}

list_years_with_transactions(book)

bla

Source code in src/bankr/book.py
def list_years_with_transactions(book: pd.DataFrame) -> list[str]:
    """bla"""

    years: list[str] = []
    book["year"] = book["date"].dt.year
    for year in range(1900, 2100):
        if (book["year"] == year).sum() > 0:
            years.append(str(year))

    return years

pickle(book)

Save the Book as book-v3.pickle.

Before pickling, drop unused Optionals and categories, and create a backup of book-v3.pickle, if present.

Source code in src/bankr/book.py
def pickle(book: Page) -> None:
    """Save the Book as `book-v3.pickle`.

    Before pickling, drop unused Optionals and categories, and create a backup
    of `book-v3.pickle`, if present.
    """

    # Drop unused Optionals
    drops = set(OPTIONALS) - set(BOOK.keys())
    if drops:
        book.drop(columns=drops, inplace=True)

    # Remove unused categories
    book["iban"] = book["iban"].cat.remove_unused_categories()
    book["currency"] = book["currency"].cat.remove_unused_categories()
    book["category"] = book["category"].cat.remove_unused_categories()
    book["tag"] = book["tag"].cat.remove_unused_categories()

    # Create backup, if needed
    pickle_path = DATA_PATH / "book-v3.pickle"
    if (pickle_path).is_file():
        pickle_path.rename(DATA_PATH / ("book-v3_" + datetime.now().strftime("%Y%m%d-%H%M%S") + ".pickle"))

    # And create "book-v3.pickle"
    try:
        book.to_pickle(pickle_path)
    except RuntimeError as e:
        sys.exit(f"Bankr Error - Book could not be saved.\n{e}")

    # Book saved
    bh.info(f"{i18n.t('book.book')} {i18n.t('book.saved')}")

unpickle()

Read the Book from book-v3.pickle.

Raise error, if the pickled Book is not found.

Source code in src/bankr/book.py
def unpickle() -> Page:
    """Read the Book from `book-v3.pickle`.

    Raise error, if the pickled Book is not found.
    """
    try:
        book: Page = pd.read_pickle(DATA_PATH / "book-v3.pickle")
        book = bp.validate(book)
        return book
    except FileNotFoundError as e:
        sys.exit(f"Bankr Error - Book not found.\n{e}")

Page module for Bankr.

This module provides functions for importing, manipulating, and validating Pages.

accounts(page)

tbc

Source code in src/bankr/page.py
def accounts(page: Page) -> pd.DataFrame:
    """tbc"""
    # Calculate the data of all accounts
    accounts: list[dict] = []
    for account in ACCOUNTS:
        account["entries"] = sum(page["iban"] == account["iban"])
        account["cents"] = page[page["iban"] == account["iban"]]["cents"].sum()
        account["first_entry"] = ""
        account["last_entry"] = ""
        if account["entries"] > 0:
            account["first_entry"] = page[page["iban"] == account["iban"]].date.min().strftime("%d.%m.%Y")
            account["last_entry"] = page[page["iban"] == account["iban"]].date.max().strftime("%d.%m.%Y")
        accounts.append(account)
    accounts = pd.DataFrame(accounts)
    accounts["balance"] = (accounts["cents"] / 100.0).astype(float).apply(lambda x: f"{x:.2f}") + "€"

    return accounts

amounts(page, **kwargs)

Calculate the sum of cents per groupby, e.g. category (default), tag, or iban, and per period.

The period is given as string YYYY-mm (months, default), YYYY-Qn (quarters), YYYY-Tn (terms), or YYYY.

Parameters:

Name Type Description Default
period kwarg

The string "month" (default), "quarter", "term", or "year".

required
pivot kwarg

True if pivoted (default), otherwise stacked

required
groupby kwarg

Field to group by. 'category' [default], or, e.g., 'tag', or 'iban'

required

Returns:

Type Description
DataFrame

A stacked or pivoted version of the sums in cents.

DataFrame

The stacked data consists of columns period, category/tag/iban, and cents.

DataFrame

The pivoted data is period in rows, and the group_field in columns.

Source code in src/bankr/page.py
def amounts(page: Page, **kwargs) -> pd.DataFrame:
    """Calculate the sum of cents per groupby, e.g. category (default), tag, or iban, and per period.

    The period is given as string YYYY-mm (months, default), YYYY-Qn (quarters), YYYY-Tn (terms), or YYYY.

    Args:
        period (kwarg, optional): The string  "month" (default), "quarter", "term", or "year".
        pivot (kwarg, optional): True if pivoted (default), otherwise stacked
        groupby (kwarg, optional): Field to group by. 'category' [default], or, e.g., 'tag', or 'iban'

    Returns:
        A stacked or pivoted version of the sums in cents.
        The stacked data consists of columns period, category/tag/iban, and cents.
        The pivoted data is period in rows, and the group_field in columns.
    """
    # Update params (defaults) with kwargs
    params = {"period": "month", "pivot": False, "groupby": "category"}
    params |= kwargs

    # Work on a copy and remove unnecessary columns
    copy = page.copy()

    # Create copy.period from copy.date
    copy["year"] = copy["date"].dt.year.astype("str")
    copy["period"] = copy["date"].dt.strftime("%Y-%m")
    if params["period"] == "quarter":
        quarter = copy["date"].dt.quarter.astype("str")
        copy["period"] = copy["year"].str.cat(others=quarter, sep="-Q")
    elif params["period"] == "term":
        term = copy["date"].dt.quarter.apply(lambda x: (x + 1) // 2).astype("str")
        copy["period"] = copy["year"].str.cat(others=term, sep="-T")
    elif params["period"] == "year":
        copy["period"] = copy["year"]

    # Calculate sums grouped by period and param["groupby"]
    try:
        amounts = copy.groupby(["period", params["groupby"]], as_index=False, observed=False)["cents"].sum()
    except KeyError:
        amounts = copy.groupby(["period", "category"], as_index=False, observed=False)["cents"].sum()

    if params["pivot"]:
        return amounts.pivot(index="period", columns=params["groupby"], values="cents")
    else:
        return amounts

categorize(page, **kwargs)

Auto-categorize the Entries in a Page or the Book in place.

Applies only to Entries with cat == "none", since other, potential manually overruled categories must NOT be changed.

Source code in src/bankr/page.py
def categorize(page: Page, **kwargs) -> None:
    """Auto-categorize the Entries in a Page or the Book in place.

    Applies only to Entries with cat == "none", since other, potential manually overruled
    categories must NOT be changed.
    """
    # *Technical remarks*
    # (1) return not needed due to inplace operations.
    # (2) Tilde operator ~ is an elementwise Not (needed, see "where" operator).
    # (3) catcon["nones"] limits replacement to page["category"] = "none". Index reset needed for "catcon" setup.
    # (4) catcon["keys"] limits replacement, where the filter is fullfilled.

    params = {"verbose": False}
    params |= kwargs

    catcon = pd.DataFrame(columns=["nones", "keys"], index=range(page.shape[0]))
    page.reset_index(inplace=True)
    catcon["nones"] = page["category"].str.contains("none")
    for filtr in FILTERS:
        category = filtr["category"]
        column = filtr["column"]
        keys = filtr["keys"]
        catcon["keys"] = page[column].str.contains(keys, case=False)
        page["category"] = page["category"].where(~catcon.all(axis=1), other=category)
    page.set_index("entry_id", inplace=True)

    # Verbose message
    bh.info(f"{i18n.t('page.page')} {i18n.t('page.categorized')}", verbose=params["verbose"])

categorize_manually(book)

Manually categorize the Book inplace.

Loop over all Entries with category == "none", and offer a manual change of the category. The manual categorization can be stopped at any time. In this case, the function will return an empty string to the caller.

Source code in src/bankr/page.py
def categorize_manually(book: Page) -> None:
    """Manually categorize the Book inplace.

    Loop over all Entries with `category == "none"`, and offer a manual change of the category.
    The manual categorization can be stopped at any time. In this case, the function will return an empty string
    to the caller.
    """
    nones = sum(book["category"] == "none")
    none = 1

    for n in range(book.shape[0]):
        transaction = book.iloc[n]
        if transaction["category"] != "none":
            continue
        print("━" * 120)
        print(f"{i18n.t('entry.entry')} {none}/{nones}")
        by.show(book.iloc[[n]], header=False)
        selection = bh.select_category()
        if not selection:
            return
        book.loc[transaction.name, "category"] = selection
        print(f"{i18n.t('page.new_cat')}: {i18n.t('cats.' + selection)}", "\n")
        none += 1
        time.sleep(0.5)

concat_pages(page1, page2, **kwargs)

Concat Pages or a Page to the Book.

Source code in src/bankr/page.py
def concat_pages(page1: Page, page2: Page, **kwargs) -> Page:
    """Concat Pages or a Page to the Book."""
    params = {"verbose": False}
    params |= kwargs

    page = pd.concat([page1, page2], axis=0, ignore_index=False)
    sort_page(page)

    # Verbose message
    bh.info(f"{i18n.t('page.page')} {i18n.t('page.imported')}", verbose=params["verbose"])

    return page

extract(page, **kwargs)

Extract Entries from a Page or the Book

The extraction is done based on the category of the Entries, the iban and bounds (date interval), or its offset account. All parameters are assumed to be sanitized.

offsets in the given list are considered as valid, and are therefore removed from the Page. All other parameters are treated as inclusive, if given. The defaults below mean unfiltered, respectively.

Parameters:

Name Type Description Default
page Page

The validated Page

required

Returns:

Type Description
Page

The extracted Page

Source code in src/bankr/page.py
def extract(page: Page, **kwargs) -> Page:
    """Extract Entries from a Page or the Book

    The extraction is done based on the category of the Entries, the `iban` and `bounds` (date interval),
    or its offset account. All parameters are assumed to be sanitized.

    `offsets` in the given list are considered as valid, and are therefore removed from the Page. All other parameters
    are treated as inclusive, if given. The defaults below mean unfiltered, respectively.

    Args:
        page (Page): The validated Page

    Returns:
        The extracted Page
    """

    # Update default params
    params = {"category": "", "iban": "", "bounds": [], "offsets": []}
    params |= kwargs

    # Extract Page from Book
    copy = page.copy()
    bounds = params["bounds"]
    if len(bounds) > 0:
        copy = copy[copy["date"].between(pd.to_datetime(bounds[0]), pd.to_datetime(bounds[1]), inclusive="both")]
    if params["category"]:
        copy = copy[copy["category"] == params["category"]]
    if params["iban"]:
        copy = copy[copy["iban"] == params["iban"]]
    copy = copy.loc[~(copy["offset"].isin(params["offsets"]))]  # TODO Does this work?
    return copy

parse(csv_file, **kwargs)

Parse CSV and create a Page.

Parse the CSV, defined by <bank_code>.yaml, and validate the parsed data. Before the validation, two pretty critical data transformations take place: 1. The estimation of the dates for date and valuta, which needs proper adjustment in .yaml. 2. Even more important is transfer of the value of the entry to cents, using to cents.

Parameters:

Name Type Description Default
csv_file str

A bank generated CSV file. Its format is imported from .yaml.

required
verbose bool

Creates a completion message if True.

required

Returns:

Type Description
Page

A validated Page

Source code in src/bankr/page.py
def parse(csv_file: str, **kwargs) -> Page:
    """Parse CSV and create a Page.

    Parse the CSV, defined by `<bank_code>.yaml`, and validate the parsed data. Before the validation,
    two pretty critical data transformations take place:
        1. The estimation of the dates for date and valuta, which needs proper adjustment in <bank_code>.yaml.
        2. Even more important is transfer of the value of the entry to cents, using `to cents`.

    Args:
        csv_file: A bank generated CSV file. Its format is imported from <bank_code>.yaml.
        verbose (bool, optional): Creates a completion message if True.

    Returns:
        A validated Page
    """
    params = {"verbose": False}
    params |= kwargs

    # Analyse filename, esp. the IBAN
    iban = csv_file.split("-")[0]
    source = csv_file.replace(".", "-").split("-")[1]
    if not IBAN(iban).is_valid:
        sys.exit("Bankr Error - Filename has invalid IBAN.")

    # Read CSV
    csv_format: dict = read_bankr_yaml(DATA_PATH / ("csv/" + IBAN(iban).bank_code + ".yaml"))
    try:
        csv = pd.read_csv(
            DATA_PATH / ("csv/" + csv_file),
            sep=csv_format["sep"],
            encoding=csv_format["encoding"],
            dtype="str",
            skiprows=csv_format["skiprows"],
            header=0,
            index_col=False,
            names=csv_format["names"],
        )
    except BaseException as e:  # Catch all exceptions
        sys.exit(f"Bankr Error - Page could not be read from CSV.\n{e}")

    # Add iban and source   TODO Improve using try ... except
    csv["iban"] = iban
    csv["iban"] = csv["iban"].astype("str")
    csv["source"] = source
    csv["source"] = csv["source"].astype("str")
    # csv["currency"] = csv_format["currency"]
    csv["cents"] = csv["cents"].apply(bh.to_cents).astype(int)
    csv["date"] = pd.to_datetime(csv["date"], format=csv_format["dateformat"])
    csv["valuta"] = pd.to_datetime(csv["valuta"], format=csv_format["dateformat"])

    # Verbose message
    bh.info(f"{csv_file} {i18n.t('page.parsed')}", verbose=params["verbose"])

    return validate(csv)

show(page, **kwargs)

tbc

Source code in src/bankr/page.py
def show(page: Page, **kwargs) -> None:
    """tbc"""
    params = {"details": False, "enum": True, "entry_id": False, "header": True}
    params |= kwargs

    # Do not change page
    page = page.copy()
    page.reset_index(inplace=True)

    # Prepare Figures to show
    figures = []
    if params["entry_id"]:
        figures += ["entry_id"]
        params["details"] = False
    figures += MANDATORIES
    figures.remove("currency")
    figures = ["euros" if figure == "cents" else figure for figure in figures]
    if params["details"]:
        try:
            figures += list(BOOK.keys())
        except AttributeError:
            pass

    # Format dates
    page["date"] = page["date"].dt.strftime("%d.%m.%Y")
    if "valuta" in page.columns:
        page["valuta"] = page["valuta"].dt.strftime("%d.%m.%Y")

    # Format amount
    page["euros"] = bh.to_euros(page["cents"], page["currency"])
    page.drop(["currency"], axis=1, inplace=True)

    # Filter the data
    show_page = page[[*figures]]

    # I18n of Figures
    headers: list = []
    if params["enum"]:
        headers += ["#"]
    for figure in figures:
        headers.append(i18n.t(f"entry.{figure}"))

    # i18n of category
    show_page["category"] = page["category"].apply(lambda x: i18n.t(f"cats.{x}"))

    # Show tag description if different from "none"
    show_page["tag"] = page["tag"].apply(lambda x: "" if x == "none" else bh.describe_tag(x))

    # Alignment of Figures
    colalign: list = []
    if params["enum"]:
        colalign += ["right"]
    if params["entry_id"]:
        colalign += ["center"]
    colalign += ["center", "left", "right", "left", "center", "center", "center"]

    if params["header"]:
        bh.title(i18n.t("page.entries"))
    print(f"{i18n.t('page.total')}: {page['cents'].sum() / 100.0:.2f}€ ", end="")
    print(f"({page.shape[0]} {i18n.t('page.entries')})\n")
    print(
        tabulate(show_page, headers=headers, showindex=params["enum"], tablefmt="rounded_outline", colalign=colalign),
        "\n",
    )

show_accounts(page, **kwargs)

Show a summary of the accounts in the Page.

It includes the IBAN of the accounts, the number of Entries, first and last Entry dates, and a balance.

Parameters:

Name Type Description Default
page Page

The Page containing the account data to be displayed.

required
active bool

If True (default), shows only active accounts

required
details bool

If True, displays detailed information about each account. Defaults to False.

required

Returns:

Type Description
None

None, table for CLI.

Source code in src/bankr/page.py
def show_accounts(page: Page, **kwargs) -> None:
    """Show a summary of the accounts in the Page.

    It includes the IBAN of the accounts, the number of Entries, first and last Entry dates, and a balance.

    Args:
        page (Page): The Page containing the account data to be displayed.
        active (bool, optional): If True (default), shows only active accounts
        details (bool, optional): If True, displays detailed information about each account. Defaults to False.

    Returns:
        None, table for CLI.
    """
    param = {"active": True, "details": False}
    param |= kwargs

    # Calculate the data of all accounts
    accounts: list[dict] = []
    for account in ACCOUNTS:
        account["entries"] = sum(page["iban"] == account["iban"])
        account["cents"] = page[page["iban"] == account["iban"]]["cents"].sum()
        account["first_entry"] = ""
        account["last_entry"] = ""
        if account["entries"] > 0:
            account["first_entry"] = page[page["iban"] == account["iban"]].date.min().strftime("%d.%m.%Y")
            account["last_entry"] = page[page["iban"] == account["iban"]].date.max().strftime("%d.%m.%Y")
        if param["active"] and account["active"] == "True":
            accounts.append(account)
        if not param["active"]:
            accounts.append(account)
    accounts = pd.DataFrame(accounts)
    accounts["balance"] = (accounts["cents"] / 100.0).astype(float).apply(lambda x: f"{x:.2f}") + "€"

    # Filter the data of accounts
    figures = ["iban", "entries", "first_entry", "last_entry", "balance", "description"]
    colalign = ["left", "right", "center", "center", "right", "left"]
    if param["details"]:
        figures += ["owner", "group", "type", "active"]
        colalign += ["center", "center", "left", "center"]
    accounts_to_show = accounts[[*figures]]

    # I18n of the Figures
    headers: list = []
    for figure in figures:
        headers.append(i18n.t(f"page.{figure}"))

    # Show accounts
    bh.title(i18n.t("page.accounts"))
    print(f"{i18n.t('page.total')}: {accounts['cents'].sum() / 100.0:.2f}€ ", end="")
    print(f"({accounts['entries'].sum()} {i18n.t('page.entries')})\n")
    print(
        tabulate(accounts_to_show, headers=headers, showindex=False, tablefmt="rounded_outline", colalign=colalign),
        "\n",
    )

show_amounts(amounts, **kwargs)

Show the Amounts per Period.

Displays the categories and balance (columns) per interval (rows) for all categories except "internal".

Parameters:

Name Type Description Default
amounts DataFrame

A DataFrame containing the amounts to be displayed.

required

Returns:

Type Description
None

None, table for CLI.

Source code in src/bankr/page.py
def show_amounts(amounts: pd.DataFrame, **kwargs) -> None:
    """Show the Amounts per Period.

    Displays the categories and balance (columns) per interval (rows) for all categories except "internal".

    Args:
        amounts (pd.DataFrame): A DataFrame containing the amounts to be displayed.

    Returns:
        None, table for CLI.
    """
    params = {"internal": False, "period": "month", "pivot": False, "groupby": "category"}
    params |= kwargs

    if not params["internal"]:
        amounts.drop(["internal"], axis=1, inplace=True)

    # Round Cents to Euros
    amounts = (amounts + 50) // 100

    # I18n of the relevant categories
    headers: list = []
    for category in amounts.columns:
        headers.append(i18n.t(f"cats.{category}")[0:8])
    # TODO Tags

    bh.title(f"{i18n.t('cli.statistics')} in {ANCHOR_CURRENCY}")
    print(tabulate(amounts, headers=headers, showindex=True, tablefmt="rounded_outline"), "\n")

show_iban_statistics(page, iban)

Statistics of an Account.

It provides the number of Entries, the first and last Entry date, and the balance of the Account.

Source code in src/bankr/page.py
def show_iban_statistics(page: Page, iban: str) -> float:
    """Statistics of an Account.

    It provides the number of Entries, the first and last Entry date, and the balance of the Account.
    """
    try:
        first_entry = page[page["iban"] == iban].date.min().strftime("%d.%m.%Y")
    except:  # noqa: E722
        first_entry = f"{i18n.t('page.none'):>10}"
    try:
        last_entry = page[page["iban"] == iban].date.max().strftime("%d.%m.%Y")
    except:  # noqa: E722
        last_entry = f"{i18n.t('page.none'):>10}"
    try:
        value = page[page["iban"] == iban]["cents"].sum() / 100.0
    except:  # noqa: E722
        value = 0.0

    # Print IBAN statistics
    value_str = f"{value:.2f}{ANCHOR_CURRENCY}"
    print(f"    {i18n.t('page.entry_no'):<30}{len(page[page.iban == iban]):>10}")
    print(f"    {i18n.t('page.first_entry_long'):<30}{first_entry}")
    print(f"    {i18n.t('page.last_entry_long'):<30}{last_entry}")
    print(f"    {i18n.t('page.total'):<30}{value_str:>10}\n")

    return value

sort_page(page)

Sort a Page by descending date and ascending iban

Source code in src/bankr/page.py
def sort_page(page: Page) -> None:
    """Sort a Page by descending date and ascending iban"""
    page.sort_values(by=["date", "iban"], ascending=[False, True], inplace=True)

stringify(page)

Convert a Page into a Pandas DataFrame containing only string columns.

Parameters:

Name Type Description Default
page Page

A validated Page

required

Returns:

Type Description
DataFrame

pd.DataFrame: An identically shaped DataFrame with all values transferred to strings.

Source code in src/bankr/page.py
def stringify(page: Page) -> pd.DataFrame:
    """
    Convert a Page into a Pandas DataFrame containing only string columns.

    Args:
        page (Page): A validated Page

    Returns:
        pd.DataFrame: An identically shaped DataFrame with all values transferred to strings.
    """
    # Do not change page
    page = page.copy()

    # Calculate currency
    page["cents"] = (page["cents"].astype(float) / 100).map("{:.2f}".format)
    page["cents"] += page["currency"].astype(str)

    # Prepare date formats
    page["date"] = page["date"].dt.strftime(DATE_FORMAT)
    page["valuta"] = page["valuta"].dt.strftime(DATE_FORMAT)

    # Provide the i18n version of "category" and "tag"
    page["category"] = page["category"].apply(lambda x: i18n.t(f"cats.{x}"))
    page["tag"] = page["tag"].apply(lambda x: bh.describe_tag(x))

    # Transfer all columns to strings
    string_schema = pa.DataFrameSchema(
        {
            "date": pa.Column("string"),
            "iban": pa.Column("string"),
            "cents": pa.Column("string"),
            "currency": pa.Column("string"),
            "offset": pa.Column("string"),
            "category": pa.Column("string"),
            "tag": pa.Column("string"),
            "source": pa.Column("string"),
            "valuta": pa.Column("string"),
            "name": pa.Column("string"),
            "process": pa.Column("string"),
            "description": pa.Column("string"),
            "creditor": pa.Column("string"),
            "mandate": pa.Column("string"),
            "customer": pa.Column("string"),
        },
        coerce=True,
    )

    return string_schema.validate(page)

validate(raw_page)

Parse and validate a Raw Page into a Page.

The most important values are date/valuta, and cents. Te latter is kept as is before validation (Int64 therefore preferred). Dates, preferrably datetime64[ns] are assumed to follow DATE_FORMAT, ifstr`.

Parameters:

Name Type Description Default
raw_page Page

The Raw Page to validate.

required

Returns:

Type Description
Page

The validated Page.

Source code in src/bankr/page.py
def validate(raw_page: Page) -> Page:
    """Parse and validate a Raw Page into a Page.

    The most important values are date/valuta, and cents. Te latter is kept as is before validation
    (Int64 therefore preferred). Dates, preferrably `datetime64[ns] are assumed to follow DATE_FORMAT, if `str`.

    Args:
        raw_page: The Raw Page to validate.

    Returns:
        The validated Page.
    """
    # YAML defined column categories
    ibans = [account["iban"] for account in ACCOUNTS]
    currencies = [currency["currency"] for currency in CURRENCIES]
    categories = [category["category"] for category in CATEGORIES]
    tags = [tag["tag"] for tag in TAGS]

    # Define Pandera Page schema
    schema = pa.DataFrameSchema(
        {
            "date": pa.Column("datetime64[ns]"),
            "iban": pa.Column("category", pa.Check.isin(ibans)),
            "cents": pa.Column("Int64"),
            "currency": pa.Column("category", pa.Check.isin(currencies), default=ANCHOR_CURRENCY),
            "offset": pa.Column("string", default=""),
            "category": pa.Column("category", pa.Check.isin(categories), default="none"),
            "tag": pa.Column("category", pa.Check.isin(tags), default="none"),
            "source": pa.Column("string", default=""),
            "valuta": pa.Column("datetime64[ns]"),
            "name": pa.Column("string", default=""),
            "process": pa.Column("string", default=""),
            "description": pa.Column("string", default=""),
            "creditor": pa.Column("string", default=""),
            "mandate": pa.Column("string", default=""),
            "customer": pa.Column("string", default=""),
        },
        index=pa.Index("string"),
        add_missing_columns=True,
        strict="filter",
        coerce=True,
    )

    # Create index = entry_id if needed
    if raw_page.index.name != "entry_id":
        raw_page["entry_id"] = raw_page.apply(lambda _: uuid.uuid4(), axis=1)
        raw_page.set_index("entry_id", inplace=True)

    # Format date, if a string, assuming the string is in DATE_FORMAT
    if raw_page.dtypes["date"] == "str":
        try:
            raw_page["date"] = pd.to_datetime(raw_page["date"], format=DATE_FORMAT)
        except ValueError:
            sys.exit("Bankr Error - Date invalid")

    # If valuta is missing in Raw Page, set valuta to date
    if "valuta" not in raw_page.columns:
        if "date" in raw_page.columns:
            raw_page["valuta"] = raw_page["date"]

    # Format valuta, if a string, assuming the string is in DATE_FORMAT
    if raw_page.dtypes["valuta"] == "str":
        try:
            raw_page["valuta"] = pd.to_datetime(raw_page["valuta"], format=DATE_FORMAT)
        except ValueError:
            sys.exit("Bankr Error - Date invalid")

    # Validate Page gracefully using Pandera
    try:
        page: Page = schema.validate(raw_page, lazy=True)
    except pa.errors.SchemaErrors as exc:
        print(f"\nBankr Error - Page schema showed problems:\n\n{exc.failure_cases}\n")
        sys.exit()

    # Add categories to Categoricals
    page["iban"] = page["iban"].cat.add_categories(set(ibans) - set(page["iban"].cat.categories))
    page["currency"] = page["currency"].cat.add_categories(set(currencies) - set(page["currency"].cat.categories))
    page["category"] = page["category"].cat.add_categories(set(categories) - set(page["category"].cat.categories))
    page["tag"] = page["tag"].cat.add_categories(set(tags) - set(page["tag"].cat.categories))

    # Sort the columns and rows in Page
    page = page[[*(MANDATORIES + OPTIONALS)]]
    sort_page(page)

    return page

Entry module 'by' for Bankr.

This module provides functions for manipulating Entries.

bulk_edit(page, entry_ids, **kwargs)

Bulk edit entries by setting tags based on entry IDs.

Parameters:

Name Type Description Default
page Page

The Page containing entries to edit

required
entry_ids list

List of entry IDs to find and edit

required
**kwargs

Optional parameters like tag="tag_value"

{}

Returns:

Type Description
list

List of entry IDs that were not found in the page

Source code in src/bankr/entry.py
def bulk_edit(page: Page, entry_ids: list, **kwargs) -> list:
    """Bulk edit entries by setting tags based on entry IDs.

    Args:
        page: The Page containing entries to edit
        entry_ids: List of entry IDs to find and edit
        **kwargs: Optional parameters like tag="tag_value"

    Returns:
        List of entry IDs that were not found in the page
    """
    params = {"tag": "none"}
    params |= kwargs

    not_found = []

    # Process each entry ID
    for entry_id in entry_ids:
        try:
            # Check if entry exists in page
            if entry_id in page.index:
                # Update the specified field (default: tag)
                if "tag" in params:
                    page.loc[entry_id, "tag"] = params["tag"]
            else:
                not_found.append(entry_id)
        except KeyError:
            not_found.append(entry_id)

    return not_found

edit(entry, asset_mode=False, tag='none')

Edit the Figures of an Entry.

Parameters:

Name Type Description Default
entry Entry

The validated Entry to edit.

required
balance_mode

If True, calculate amount from target balance (default: False)

required

Returns:

Type Description
Entry

The edited and validated Entry.

Source code in src/bankr/entry.py
def edit(entry: Entry, asset_mode: bool = False, tag: str = "none") -> Entry:
    """Edit the Figures of an Entry.

    Args:
        entry: The validated Entry to edit.
        balance_mode: If True, calculate amount from target balance (default: False)

    Returns:
        The edited and validated Entry.
    """

    # Extract choices from env
    iban_choices = [account["iban"] for account in ACCOUNTS]
    currency_choices = [currency["currency"] for currency in CURRENCIES]
    category_choices = [category["category"] for category in CATEGORIES]
    tag_choices = [tag["tag"] for tag in TAGS]

    # Get the first row and stringify it
    row = bp.stringify(entry.iloc[[0]])  # TODO Is this a good solution?
    entry_id = entry.index[0]
    iban = row.at[entry_id, "iban"]

    # Handle balance mode  TODO Does not work correctly!
    if asset_mode:
        balances = bk.get_assets(iban, tag=tag)
        invest_euros = balances["invest"] / 100.0
        asset_euros = balances["asset"] / 100.0
        current_euros = invest_euros + asset_euros
        if balances["invest"] == 0:
            bh.info("Invalid balance input or not an investment. Using direct amount mode.")
        else:
            bh.info(f"Balance of the Book: {current_euros:.2f}{ANCHOR_CURRENCY}")
            bh.info(f" Investment: {invest_euros:.2f}{ANCHOR_CURRENCY}")
            bh.info(f" Asset: {asset_euros:.2f}{ANCHOR_CURRENCY}")
            target_balance_input = input("Enter target balance: ")
            target_balance_cents = int(bh.to_cents(target_balance_input))
            cents = target_balance_cents - balances["invest"] - balances["asset"]
            euros = cents / 100.0
            bh.info(f"Amount of Asset: {euros:.2f}{ANCHOR_CURRENCY}")
            row.at[entry_id, "cents"] = f"{euros:.2f}{ANCHOR_CURRENCY}"
            row.at[entry_id, "category"] = "asset"
            row.at[entry_id, "tag"] = tag

    # Build questions for each column
    ask = []
    for col in row.columns:
        default = row.at[entry_id, col]

        # Special handling for category columns
        if col == "iban":
            ask.append(iq.List(col, message=i18n.t("entry.iban"), choices=iban_choices, default=default))
        elif col == "cents":
            default = default[:-1]  # Remove currency symbol, TODO Is this a good solution?
            ask.append(iq.Text(col, message=i18n.t("entry.euros"), default=default))
        elif col == "currency":
            ask.append(iq.List(col, message=i18n.t("entry.currency"), choices=currency_choices, default=default))
        elif col == "category":
            ask.append(iq.List(col, message=i18n.t("entry.category"), choices=category_choices, default=default))
        elif col == "tag":
            ask.append(iq.List(col, message=i18n.t("entry.tag"), choices=tag_choices, default=default))
        else:
            ask.append(iq.Text(col, message=i18n.t(f"entry.{col}"), default=default))

    # Prompt user
    editions = iq.prompt(ask)

    # Add entry_id and process cents
    editions["entry_id"] = entry_id
    if "cents" in editions:
        editions["cents"] = bh.to_cents(editions["cents"])

    # Create new entry
    new_entry = pd.DataFrame([editions])
    new_entry.set_index("entry_id", inplace=True)

    return bp.validate(new_entry)

empty(iban)

Create an empty, validated Entry for the given IBAN.

Parameters:

Name Type Description Default
iban str

The IBAN for the entry.

required

Returns:

Type Description
Entry

An empty, validated Entry with today's date as date and valuta.

Source code in src/bankr/entry.py
def empty(iban: str) -> Entry:
    """Create an empty, validated Entry for the given IBAN.

    Args:
        iban: The IBAN for the entry.

    Returns:
        An empty, validated Entry with today's date as date and valuta.
    """

    today = date.today()
    raw_entry = pd.DataFrame({"iban": [iban], "date": [today], "valuta": [today], "cents": ["0"]})
    return bp.validate(raw_entry)

show(entry, **kwargs)

Show the Figures of an Entry in a tabular view.

Parameters:

Name Type Description Default
entry Page

An Entry, or the first row of a Page.

required
details

If True, show all fields. If False, show only mandatory fields.

required
enum

If True, enumerate the lines for reference.

required
Source code in src/bankr/entry.py
def show(entry: Page, **kwargs) -> None:
    """Show the Figures of an Entry in a tabular view.

    Args:
        entry: An Entry, or the first row of a Page.
        details: If True, show all fields. If False, show only mandatory fields.
        enum: If True, enumerate the lines for reference.
    """

    params = {"details": True, "enum": False, "header": True}
    params |= kwargs

    # Stringify deep copy of entry,
    entry = bp.stringify(entry.copy().iloc[[0]])

    # Figures to show
    figures = []  # New object needed
    figures += MANDATORIES
    if params["details"]:
        try:  # List might be empty
            figures += list(BOOK.keys())
        except AttributeError:
            pass
    figures.remove("currency")

    # Show the Entry
    if params["header"]:
        bh.title("Entry Mode")
    print("━" * 120)
    print(f" {i18n.t('entry.entry_id')}   {entry.index[0]}")
    print("─" * 120)
    for i, figure in enumerate(figures, 1):
        if params["enum"]:
            print(f"  {i:2d}.", end="")
        value = entry[figure].iloc[0]
        print(f"  {i18n.t(f'entry.{figure}'):<20} {value:.94}")
    print("━" * 120, "\n")

Helper Functions

Helper module bh for Bankr.

This module provides helper functions for TUI messages and data sanitizing.

confirm_save()

Confirmation dialog before the Book gets saved/pickled.

Source code in src/bankr/helpers.py
def confirm_save() -> bool:
    """Confirmation dialog before the Book gets saved/pickled."""
    while True:
        selection = input("Bankr Action - Save the Book (y/n)? ")
        if selection == "y":
            return True
        elif selection == "n":
            return False

describe_tag(tag)

Receive the description of a tag

Source code in src/bankr/helpers.py
def describe_tag(tag: str) -> str:
    """Receive the description of a tag"""
    description = ""
    for t in TAGS:
        if t["tag"] == tag:
            description = t["description"]

    return description

info(info='', **kwargs)

Bankr Info

Print a short info, except verbose=False

Parameters:

Name Type Description Default
info str

Info. Defaults to "".

''
Source code in src/bankr/helpers.py
def info(info: str = "", **kwargs) -> None:
    """Bankr Info

    Print a short info, except `verbose=False`

    Args:
        info (str): Info. Defaults to "".
    """
    params = {"verbose": True}
    params |= kwargs

    if params["verbose"]:
        print(f"Bankr {i18n.t('helpers.info')} - {info}")

locate_entry(book, entry_id)

Locate an entry in the Book or a Page.

Source code in src/bankr/helpers.py
def locate_entry(book: Page, entry_id: str) -> Page:
    """Locate an entry in the Book or a Page."""
    try:
        entry = book.loc[[entry_id]]
    except KeyError:
        sys.exit("Bankr Error - Entry not found in the Book.")

    return entry

sanitize_category(category='')

Sanitize the given category.

Provides a valid category, if the category information could be identified uniquely in categories.yaml or its current translation.

Parameters:

Name Type Description Default
category str

Category information to be sanitized. Defaults to "".

''

Returns:

Name Type Description
str str

Sanitized (internal) category as given in categories.yaml or empty string

Source code in src/bankr/helpers.py
def sanitize_category(category: str = "") -> str:
    """Sanitize the given category.

    Provides a valid `category`, if the category information could be identified uniquely in `categories.yaml`
    or its current translation.

    Args:
        category (str, optional): Category information to be sanitized. Defaults to "".

    Returns:
        str: Sanitized (internal) category as given in categories.yaml or empty string
    """
    candidates: list[str] = []
    for value in CATEGORIES:
        if (category.lower() in value["category"]) or (category in i18n.t("cats." + value["category"])):
            candidates.append(value["category"])
    if len(candidates) == 1:
        return candidates[0]
    else:
        return ""

sanitize_entry_ids(entry_ids_str)

Sanitize comma-separated entry IDs string into a list.

Parameters:

Name Type Description Default
entry_ids_str str

Comma-separated string of entry IDs

required

Returns:

Type Description
list

List of sanitized entry IDs

Source code in src/bankr/helpers.py
def sanitize_entry_ids(entry_ids_str: str) -> list:
    """Sanitize comma-separated entry IDs string into a list.

    Args:
        entry_ids_str: Comma-separated string of entry IDs

    Returns:
        List of sanitized entry IDs
    """
    if not entry_ids_str:
        return []

    # Split by comma and strip whitespace
    entry_ids = [id.strip() for id in entry_ids_str.split(",") if id.strip()]
    return entry_ids

sanitize_iban(iban='')

Sanitize a given IBAN or an IBAN fragment.

The string iban is checked against the IBANs within ACCOUNTS or accounts.yaml. If the IBAN could be identified uniquely, it is returned. Unique fragements of IBANs are sufficient.

Parameters:

Name Type Description Default
iban str

IBAN or IBAN fragment. Defaults to "".

''

Returns:

Name Type Description
str str

Identified IBAN or empty string

Source code in src/bankr/helpers.py
def sanitize_iban(iban: str = "") -> str:
    """Sanitize a given IBAN or an IBAN fragment.

    The string `iban` is checked against the IBANs within `ACCOUNTS` or `accounts.yaml`.
    If the IBAN could be identified uniquely, it is returned. Unique fragements of IBANs are sufficient.

    Args:
        iban (str, optional): IBAN or IBAN fragment. Defaults to "".

    Returns:
        str: Identified IBAN or empty string
    """
    candidates: list[str] = []
    for value in ACCOUNTS:
        if iban.upper() in value["iban"]:
            candidates.append(value["iban"])
    if len(candidates) == 1:
        return candidates[0]
    else:
        return ""

sanitize_period(period='-')

Sanitize period string.

Converts a string into a bounds list (datetime of start and end date), empty if invalid. - YYYY-MM is a month from 1900-01 to 2099-12. YYYY is a year from 1900 to 2099. - YYYY-Qx (x = 1...4) is a corresponding quarter, YYYY-Hx a halve-year or term. - YYYY+n (n=0...99) is 1...100 years beginning with YYYY. - yy leads to 20yy.

TODO Accept two digit years (separator year?) and quarters/terms.

Parameters:

Name Type Description Default
period str

A string defining a time period. Defaults to "-", "" not allowed.

'-'

Returns:

Type Description
list[datetime]

list[datetime]: Bounds of the period, empty if invalid

Source code in src/bankr/helpers.py
def sanitize_period(period: str = "-") -> list[datetime]:
    """Sanitize period string.

    Converts a string into a bounds list (datetime of start and end date), empty if invalid.
    - YYYY-MM is a month from 1900-01 to 2099-12. YYYY is a year from 1900 to 2099.
    - YYYY-Qx (x = 1...4) is a corresponding quarter, YYYY-Hx a halve-year or term.
    - YYYY+n (n=0...99) is 1...100 years beginning with YYYY.
    - yy leads to 20yy.

    TODO Accept two digit years (separator year?) and quarters/terms.

    Args:
        period (str): A string defining a time period. Defaults to "-", "" not allowed.

    Returns:
        list[datetime]: Bounds of the period, empty if invalid
    """
    # No bounds
    bounds: list = []

    # period has to start with "19" or "20". If not, prepend "20"
    if not re.match(r"^(19|20)", period):
        period = "20" + period
    # period = month, from 1900-01 to 2099-12
    if re.match(r"^(19|20)?\d{2}-(0[1-9]|1[0-2])$", period):
        period += "-01"
        start = datetime.strptime(period, "%Y-%m-%d")
        bounds = [start, start + relativedelta(day=31)]
    # period = quarter or halve-year
    if re.match(r"^(19|20)?\d{2}-(Q[1-4]|H[1-2])$", period):
        year = int(period[0:4])
        first = 3 * int(period[-1]) - 2
        delta = 2
        if period[-2] == "H":
            first = 6 * int(period[-1]) - 5
            delta = 5
        bounds = [datetime(year, first, 1), datetime(year, first + delta, 1) + relativedelta(day=31)]
    # period = year, from 1900 to 2099
    if re.match(r"^(19|20)?\d{2}$", period):
        bounds = [datetime(int(period), 1, 1), datetime(int(period), 12, 31)]
    # period = years (<first year>+<further years>)
    if re.match(r"^(19|20)?\d{2}\+\d{1,2}$", period):
        year = int(period[0:4])
        delta = int(period[-2:])
        bounds = [datetime(year, 1, 1), datetime(year + delta, 12, 31)]

    return bounds

sanitize_tag(tag='none')

Sanitize the given tag.

Provides a valid tag, if the tag information could be identified uniquely in tags.yaml.

Parameters:

Name Type Description Default
tag str

tag information to be sanitized. Defaults to "none".

'none'

Returns:

Name Type Description
str str

Sanitized (internal) tag as given in categories.yaml or empty string

Source code in src/bankr/helpers.py
def sanitize_tag(tag: str = "none") -> str:
    """Sanitize the given tag.

    Provides a valid `tag`, if the tag information could be identified uniquely in `tags.yaml`.

    Args:
        tag (str, optional): tag information to be sanitized. Defaults to "none".

    Returns:
        str: Sanitized (internal) tag as given in categories.yaml or empty string
    """
    candidates: list[str] = []
    for value in TAGS:
        if (tag.lower() in value["tag"]) or (tag.lower() in describe_tag(value["tag"]).lower()):
            candidates.append(value["tag"])
    if len(candidates) == 1:
        return candidates[0]
    else:
        return "none"

select_category()

Provide CATEGORIES for manual selection.

The selection is enumerated as they show up in the parameter CATEGORIES (imported from categories.yaml).

Returns:

Type Description
str

Selected category, or empty string for quit.

Source code in src/bankr/helpers.py
def select_category() -> str:
    """Provide CATEGORIES for manual selection.

    The selection is enumerated as they show up in the parameter CATEGORIES (imported from `categories.yaml`).

    Args:
        None.

    Returns:
        Selected category, or empty string for quit.
    """
    # Display categories with enumeration
    for index, category in enumerate(CATEGORIES):
        translated_category = i18n.t("cats." + category["category"])
        truncated_description = category["description"][:58]
        print(f"[{index:>2}] - {translated_category:<15}{truncated_description}")

    print("─" * 80)

    # Get valid user selection
    while True:
        selection_input = input("Bankr Action - Select a category by [number] or (q)uit. ")
        if selection_input == "q":
            return ""
        try:
            selection = int(selection_input)
            if 0 <= selection < len(CATEGORIES):
                break
        except ValueError:
            pass

    return CATEGORIES[selection]["category"]

title(subtitle='')

Clear CLI and print Bankr title

Parameters:

Name Type Description Default
subtitle str

Additional subtitle. Defaults to "".

''
Source code in src/bankr/helpers.py
def title(subtitle: str = "") -> None:
    """Clear CLI and print Bankr title

    Args:
        subtitle (str, optional): Additional subtitle. Defaults to "".
    """
    # Clear CLI
    if os.name == "nt":  # On Windblows
        os.system("cls")
    else:  # On valuable and expensive Linux
        os.system("clear")

    # Print title
    print(Figlet().renderText("Bankr"))
    if subtitle:
        print(f"### {i18n.t('helpers.title')} ### - {subtitle}\n")
    else:
        print(f"### {i18n.t('helpers.title')} ###\n")

to_cents(x)

Filter an "amount of money" string in a CSV.

Strings indicating a certain amount of money can be quite pathological in CSV files of banks. Therefore, these need to be filtered, before being converted to integers: 1. Limit to acceptable characters "0123456789.,+- ". 2. Only dots as separators of cents and thousands. 3. "" and "-" give "0". 4. Minus sign as first character, no plus sign.

"000" can happen as filter result, but this is fine for integers.

Source code in src/bankr/helpers.py
def to_cents(x: str) -> str:
    """Filter an "amount of money" string in a CSV.

    Strings indicating a certain amount of money can be quite pathological in CSV files of banks.
    Therefore, these need to be filtered, before being converted to integers:
    1. Limit to acceptable characters `"0123456789.,+- "`.
    2. Only dots as separators of cents and thousands.
    3. "" and "-" give "0".
    4. Minus sign as first character, no plus sign.

    "000" can happen as filter result, but this is fine for integers.
    """
    # Return 0, if invalid digits, check sign
    accept = set("0123456789.,+- ")
    check = set(x)
    if not check.issubset(accept):
        return "0"
    x = x.replace(",", ".").replace("+", "").replace(" ", "")
    if "-" in x:
        x = "-" + x.replace("-", "")
    if len(x) == 0 or x == "-":
        return "0"

    # Normal cases: *.??
    if len(x) > 2 and x[-3] == ".":
        return x.replace(".", "")

    # Pathological cases
    x = x + "00"
    if len(x) == 3:
        return x  # Single digit
    if len(x) >= 4:
        if x[-4] == ".":
            x = x[:-1]  # One digit after separator
        return x.replace(".", "")

to_euros(cents, currency)

Return a string representation of euros (major currency unit).

Parameters:

Name Type Description Default
cents Series

A Pandas Series containing amounts in cents (minor currency unit).

required
currency Series

A Pandas Series containing currency symbols (typ. Categorical).

required

Returns:

Type Description
Series

pd.Series: A pandas Series with values converted to euros and formatted as strings.

Source code in src/bankr/helpers.py
def to_euros(cents: pd.Series, currency: pd.Series) -> pd.Series:
    """Return a string representation of euros (major currency unit).

    Args:
        cents (pd.Series): A Pandas Series containing amounts in cents (minor currency unit).
        currency (pd.Series): A Pandas Series containing currency symbols (typ. Categorical).

    Returns:
        pd.Series: A pandas Series with values converted to euros and formatted as strings.
    """
    return (cents / 100.0).apply(lambda x: f"{x:.2f}") + currency.astype(str)