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
|
|
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."""
...
Functions related to the Book
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}")
Functions related to a Page
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 to cents.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
csv_file
|
str
|
A bank generated CSV file. Its format is imported from |
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
Functions related to Entries
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)