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='')

Add an Entry to the Book.

Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("iban")
def add(iban: str = "") -> 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))
    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_entries(book)
        bh.info(f"{i18n.t('cli.book_categorized')} - {sum(book['category'] == 'none')} {i18n.t('cli.wo_cat')}")
    else:
        bh.title("Categorizing")
        bp.categorize_entries_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)

Edit an Entry of the Book.

Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("entry_id")
def edit(entry_id: str):
    """Edit an Entry of the Book."""

    bh.title("Edit an Entry")
    book = bk.unpickle()
    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.")

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_entries(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 the 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 the 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)
    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(year, span, quarter, term, full)

Statistics of the Book or per time interval.

Statistics of the Book: The number of transactions per IBAN, its current account balance, and the date of the first/last transaction are shown.

Transaction Statistics: Amounts per category and time interval.

Source code in src/bankr/cli/commands.py
@click.command()
@click.option("-y", "--year", type=click.IntRange(min=1900, max=2100, clamp=True), help="First year to show")
@click.option("-s", "--span", type=click.IntRange(min=1, max=10, clamp=True), help="Number of years to show")
@click.option("-q", "--quarter", is_flag=True, help="Show quarters")
@click.option("-t", "--term", is_flag=True, help="Show terms/half-years")
@click.option("-f", "--full", is_flag=True, help="Show full years")
def stats(year: int, span: int, quarter: bool, term: bool, full: bool):
    """Statistics of the Book or per time interval.

    Statistics of the Book:
    The number of transactions per IBAN, its current account balance, and the date of the
    first/last transaction are shown.

    Transaction Statistics:
    Amounts per category and time interval.
    """

    # TODO The options need improvement.

    book = bk.unpickle()
    if not year:
        year = 1900
    if not span:
        span = 200
    period = "m"
    if quarter:
        period = "q"
    if term:
        period = "t"
    if full:
        period = "f"
    bh.title(i18n.t("cli.statistics"))
    amounts = bk.sum_per_category_period(book, year, span, period=period, pivot=True)
    amounts = bk.append_totals(amounts)
    bp.show_balance_per_interval(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

extract_from_book(book, **kwargs)

Extract a Page from the Book, based on different conditions.

The extraction is done based on the category cat of the Transaction, its iban or 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
book Page

The validated Book (or a Page)

required

Returns:

Type Description
Page

The extracted Page

Source code in src/bankr/book.py
def extract_from_book(book: Page, **kwargs) -> Page:
    """Extract a Page from the Book, based on different conditions.

    The extraction is done based on the category `cat` of the Transaction, its `iban` or `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:
        book (Page): The validated Book (or a Page)

    Returns:
        The extracted Page
    """

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

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

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')}")

sum_per_category_period(book, year, span, **kwargs)

Calculate the sum of cents per category and per period. Returns a stacked or pivoted ("amounts-type") version of the sums. The sums are given in the main currency unit.

The period is given as string YYYY-mm (months), YYYY-Qn (quarters), YYYY-Tn (terms), and YYYY. The stacked data consists of 3 columns time, cat, and main. The pivoted data is time in rows, and cat in columns.

Parameters: - year: starting year of the total time considered - span: years to consider - period: 1-char string to mark (m)onth [default], (q)uarter, (t)erm, or (f)ull year, kwarg - pivot: True if pivoted, otherwise stacked, kwarg

Source code in src/bankr/book.py
def sum_per_category_period(book: Page, year: int, span: int, **kwargs) -> pd.DataFrame:
    """Calculate the sum of cents per category and per period. Returns a stacked or pivoted
    ("amounts-type") version of the sums. The sums are given in the main currency unit.

    The period is given as string YYYY-mm (months), YYYY-Qn (quarters), YYYY-Tn (terms), and YYYY.
    The stacked data consists of 3 columns time, cat, and main. The pivoted data is time in rows,
    and cat in columns.

    Parameters:
    - year: starting year of the total time considered
    - span: years to consider
    - period: 1-char string to mark (m)onth [default], (q)uarter, (t)erm, or (f)ull year, kwarg
    - pivot: True if pivoted, otherwise stacked, kwarg
    """

    # TODO Works, but needs refactoring.

    # Update params (defaults) with kwargs
    params = {"period": "m", "pivot": False}
    params |= kwargs

    # Limit Page to [year, year + span) if year and span > 0, otherwise take the Book
    # Exit, if there are no Entries in Page
    book["year"] = book["date"].dt.year
    page = book
    if year > 0 and span > 0:
        page = book[(book["year"] >= year) & (book["year"] < (year + span))]
    if not page.shape[0]:
        sys.exit(f"Bankr Error - No values in {year}!")

    # Create the Series time and concat it with the Page
    time = page["date"].dt.strftime("%Y-%m").rename("time")
    if params["period"] != "m":
        time = page["year"].astype(str).rename("time")
    if params["period"] == "q":
        quarter = page["date"].dt.quarter.astype(str)
        time = time.str.cat(others=quarter, sep="-Q")
    if params["period"] == "t":
        term = page["date"].dt.month.apply(_month2term).astype(str)
        time = time.str.cat(others=term, sep="-T")
    page = pd.concat([page, time], axis=1, copy=False)

    # Calculate sums and return it stacked or pivoted
    stacked = page.groupby(["time", "category"], as_index=False, observed=False)["cents"].sum()
    stacked["main"] = stacked["cents"].apply(_cents2euro)
    stacked.drop("cents", axis=1, inplace=True)
    if params["pivot"]:
        return stacked.pivot(index="time", columns="category", values="main")
    else:
        return stacked

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

categorize_entries(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_entries(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_entries_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_entries_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

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")
    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["cents"] = bh.to_euros(page["cents"], page["currency"])
    page.drop(["currency"], axis=1, inplace=True)

    # Filter the data
    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 and tag
    page["category"] = page["category"].apply(lambda x: i18n.t(f"cats.{x}"))
    page["tag"] = page["tag"].apply(lambda x: i18n.t(f"cats.{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(
        tabulate(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_balance_per_interval(amounts, **kwargs)

Show the balance per time interval.

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_balance_per_interval(amounts: pd.DataFrame, **kwargs) -> None:
    """Show the balance per time interval.

    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.
    """

    amounts.drop(["internal"], axis=1, inplace=True)

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

    bh.title(i18n.t("cli.statistics"))
    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="unknown"),
            "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.

edit(entry)

Edit the Figures of an Entry.

Parameters:

Name Type Description Default
entry Entry

The validated Entry to edit.

required

Returns:

Type Description
Entry

The edited and validated Entry.

Source code in src/bankr/entry.py
def edit(entry: Entry) -> Entry:
    """Edit the Figures of an Entry.

    Args:
        entry: The validated Entry to edit.

    Returns:
        The edited and validated Entry.
    """

    from bankr.helpers import to_cents

    # 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]].fillna(""))
    entry_id = entry.index[0]

    # 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(f"entry.{col}"), choices=iban_choices, default=default))
        elif col == "cents":
            default = default[:-1]  # Remove currency symbol
            ask.append(iq.Text(col, message=i18n.t(f"entry.{col}"), default=default))
        elif col == "currency":
            ask.append(iq.List(col, message=i18n.t(f"entry.{col}"), choices=currency_choices, default=default))
        elif col == "category":
            ask.append(iq.List(col, message=i18n.t(f"entry.{col}"), choices=category_choices, default=default))
        elif col == "tag":
            ask.append(iq.List(col, message=i18n.t(f"entry.{col}"), 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_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.

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.

    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 = 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 = 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)]
    return bounds

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)