Function Reference
This is a docstrings based and automatically generated Python function reference of Bankr.
The mandatory and optional Figures are:
# Init fixed Bankr parameters
MANDATORIES = ["date", "iban", "cents", "currency", "offset", "category", "tag", "source"]
OPTIONALS = ["valuta", "name", "process", "description", "creditor", "mandate", "customer"]
The Command Line Interface
The CLI is described briefly in this documentation under Home. However, the preferable source of help is bankr [COMMAND] -h. The CLI itself is in English language, however, the output of Bankr is translated, currently to English or German.
The commands of Bankr's command line interface.
add(iban='', tag='', asset=False)
Add an Entry to the Book.
Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("iban")
@click.option("-a", "--asset", is_flag=True, help="Asset mode: Calculate entry from balances")
@click.option("-t", "--tag", type=click.STRING, help="Tag to apply to entries")
def add(iban: str = "", tag: str = "", asset: bool = False) -> None:
"""Add an Entry to the Book."""
bh.title("Add an Entry")
iban = bh.sanitize_iban(iban)
if not iban:
sys.exit("Bankr Error - No IBAN given.")
entry = by.edit(by.empty(iban), asset_mode=asset, tag=tag)
book = bk.unpickle()
book = bp.concat_pages(entry, book)
by.show(entry)
bh.info("Add this Entry?")
if bh.confirm_save():
bk.pickle(book)
else:
bh.info("Entry not added.")
app(cmd)
Start or stop the Bankr App.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cmd
|
str
|
|
required |
Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("cmd", default="start")
def app(cmd: str) -> None:
"""Start or stop the Bankr App.
Args:
cmd (str): `start` (default) or `stop` the Dashboard.
"""
if cmd.lower() == "start":
bh.info("Starting App (http://localhost:5006)...")
ba.start_app()
elif cmd.lower() == "stop":
ba.stop_app()
bh.info("App stopped")
else:
bh.info("Bankr does not understand")
cat(auto, entry_id)
Categorize Entries of a Page or the Book.
Basis of the automatic categorization is filters.yaml. Manual and auto cat work only
on entries without a category.
Source code in src/bankr/cli/commands.py
@click.command()
@click.option("-a", "--auto", is_flag=True, help="Auto categorize the Book")
@click.option("-e", "--entry_id", type=click.STRING, help="Categorize an Entry ID")
def cat(auto: bool, entry_id: str):
"""Categorize Entries of a Page or the Book.
Basis of the automatic categorization is `filters.yaml`. Manual and auto cat work only
on entries without a category.
"""
book = bk.unpickle()
if entry_id:
try:
entry = book.loc[["entry_id"]]
except KeyError:
sys.exit("Bankr Error - Invalid Entry ID")
bh.title("Categorizing Entry ID")
by.show(entry, details=True)
selection = bh.select_category()
if selection:
book.loc[entry_id, "category"] = selection
bh.info(f"{i18n.t('cli.entry_id')} {entry_id} {i18n.t('cli.categorized')}")
elif auto:
bh.title("Auto Categorizing")
bp.categorize(book)
bh.info(f"{i18n.t('cli.book_categorized')} - {sum(book['category'] == 'none')} {i18n.t('cli.wo_cat')}")
else:
bh.title("Categorizing")
bp.categorize_manually(book)
bh.info(f"{i18n.t('cli.book_categorized')} - {sum(book['category'] == 'none')} {i18n.t('cli.wo_cat')}")
if bh.confirm_save():
bk.pickle(book)
delete(entry_id)
Delete an Entry from the Book.
Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("entry_id")
def delete(entry_id: str) -> None:
"""Delete an Entry from the Book."""
bh.title("Delete an Entry")
book = bk.unpickle()
entry = bh.locate_entry(book, entry_id)
book = book.drop(entry_id)
by.show(entry)
bh.info("Delete this Entry?")
if bh.confirm_save():
bk.pickle(book)
else:
bh.info("Entry not deleted.")
edit(entry_id, bulk, tag)
Edit an Entry of the Book or perform bulk edits on multiple entries.
Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("entry_id", required=False)
@click.option("-b", "--bulk", type=click.STRING, help="Comma-separated entry IDs for bulk editing")
@click.option("-t", "--tag", type=click.STRING, default="none", help="Tag to apply to entries")
def edit(entry_id: str, bulk: str, tag: str):
"""Edit an Entry of the Book or perform bulk edits on multiple entries."""
bh.title("Edit Entries")
book = bk.unpickle()
# Handle bulk editing
if bulk:
if entry_id:
bh.info("Ignoring entry_id parameter when using --bulk option")
entry_id = None
# Sanitize entry IDs
entry_ids = bh.sanitize_entry_ids(bulk)
if not entry_ids:
sys.exit("Bankr Error - No valid entry IDs provided for bulk editing.")
# Sanitize tag
sanitized_tag = bh.sanitize_tag(tag)
if not sanitized_tag:
sys.exit("Bankr Error - Invalid tag provided.")
# Perform bulk edit
not_found = by.bulk_edit(book, entry_ids, tag=sanitized_tag)
# Provide feedback
if not_found:
bh.info(f"Bankr Warning - {len(not_found)} entry IDs not found: {', '.join(not_found)}")
else:
bh.info(f"Successfully updated {len(entry_ids)} entries with tag '{bh.describe_tag(sanitized_tag)}'")
# Confirm save
if bh.confirm_save():
bk.pickle(book)
else:
bh.info("Bulk edit not saved.")
# Handle single entry editing
elif entry_id:
entry = bh.locate_entry(book, entry_id)
entry = by.edit(entry)
book = bp.concat_pages(book.drop(entry_id), entry)
by.show(entry)
bh.info("Edit this Entry?")
if bh.confirm_save():
bk.pickle(book)
else:
bh.info("Entry not edited.")
else:
sys.exit("Bankr Error - Either provide an entry_id or use --bulk option")
entry(entry_id, details)
Show an Entry of the Book.
Source code in src/bankr/cli/commands.py
@click.command()
@click.option("-d", "--details", is_flag=True, help="Show the details (optionals) of an Entry.")
@click.argument("entry_id")
def entry(entry_id: str, details: bool) -> None:
"""Show an Entry of the Book."""
book = bk.unpickle()
try:
entry = book.loc[[entry_id]]
except KeyError:
sys.exit("Bankr Error - Entry not found.")
by.show(entry, details=details, header=True)
iban(all)
Show the Accounts of the Book.
Source code in src/bankr/cli/commands.py
@click.command()
@click.option("-a", "--all", is_flag=True, help="Show all Accounts of the Book")
def iban(all: bool):
"""Show the Accounts of the Book."""
book = bk.unpickle()
active = not all
bp.show_accounts(book, active=active)
new()
Create a new Book in DATA_PATH, see bankr.yaml.
Creates an almost empty Book, which contains a zero value "Initial Entry" for each
account in accounts.yaml.
Source code in src/bankr/cli/commands.py
@click.command()
def new():
"""Create a new Book in DATA_PATH, see `bankr.yaml`.
Creates an almost empty Book, which contains a zero value "Initial Entry" for each
account in `accounts.yaml`.
"""
bh.title("Create new Book")
book = bk.create_new_book()
if bh.confirm_save():
bk.pickle(book)
bh.info("Return to a backup, if processed erroneously.")
parse(csv_file, add, verbose)
Parse CSV for a Page, and eventually add it to the Book.
A summary of the Transactions of the Page, and the expectable changes of the Book are presented. The Book is not changed in Parse Mode. If in Add Mode, an auto categorized Page is added to the Book.
Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("csv_file")
@click.option("-a", "--add", is_flag=True, help="Add the auto-cat Page to the Book")
@click.option("-v", "--verbose", is_flag=True, help="Provide info about processing steps")
def parse(csv_file: str, add: bool, verbose: bool):
"""Parse CSV for a Page, and eventually add it to the Book.
A summary of the Transactions of the Page, and the expectable changes of the Book are presented.
The Book is not changed in Parse Mode. If in Add Mode, an auto categorized Page is added to the Book.
"""
subtitle = "Parse Mode"
if add:
subtitle = "Add Mode"
bh.title(subtitle)
# Parse CSV to Page
page = bp.parse(csv_file)
book = bk.unpickle()
# Auto-categorize Page
bp.categorize(page, verbose=True)
if add:
# Import Page to Book, and save it
book = bp.concat_pages(page, book)
bk.pickle(book)
else:
# Print statistics of the relevant IBAN
iban = csv_file.split("-")[0]
click.echo(f"- {i18n.t('cli.page')}")
page_cents = bp.show_iban_statistics(page, iban)
click.echo(f"- {i18n.t('cli.book')}")
book_cents = bp.show_iban_statistics(book, iban)
new_balance = f"{(book_cents + page_cents):.2f}{ANCHOR_CURRENCY}"
click.echo(f" {i18n.t('cli.new_balance'):<32}{new_balance:>10}\n")
# Ask if entries of page should be shown
selection = input("Bankr Action - Show Entries of the created Page (y/n)? ")
if selection.lower() != "y":
return
# Check if page has too many entries
if page.shape[0] > 500:
bh.info("Maximum number of Entries is 500.")
return
# Show entries and ask for specific entry
bp.show(page, enum=True)
entry_no_input = input("Bankr Action - Show # of Entry? ")
# Validate and show specific entry
try:
entry_no = int(entry_no_input)
if 0 <= entry_no < page.shape[0]:
entry = page.iloc[[entry_no]]
by.show(entry)
except ValueError:
pass
show(period, category, entry_id, iban, enum, check_external)
Show filtered Entries of the Book (max 500).
Source code in src/bankr/cli/commands.py
@click.command()
@click.option("-p", "--period", default="-", type=click.STRING, help="Period to show")
@click.option("-c", "--category", default="", type=click.STRING, help="Category to show")
@click.option("-e", "--entry_id", is_flag=True, help="Show Entry IDs")
@click.option("-i", "--iban", default="", type=click.STRING, help="IBAN to show")
@click.option("-n", "--enum", is_flag=True, help="Enumerate Entries")
@click.option("-x", "--check_external", is_flag=True, help="Check if offset is external")
def show(period: str, category: str, entry_id: bool, iban: str, enum: bool, check_external: bool):
"""Show filtered Entries of the Book (max 500)."""
# Sanitize parameters and collect internal accounts
bounds = bh.sanitize_period(period)
category = bh.sanitize_category(category)
iban = bh.sanitize_iban(iban)
offsets = []
if check_external:
offsets = [account["iban"] for account in ACCOUNTS]
# Load Book, extract and show Page
bh.title("Page Mode")
book = bk.unpickle()
# page = bk.extract_from_book(book, bounds=bounds, category=category, iban=iban, offsets=offsets)
page = bp.extract(book, bounds=bounds, category=category, iban=iban, offsets=offsets)
if page.shape[0] > 500:
bh.info("Maximum number of Transactions is 500.")
else:
bp.show(page, entry_id=entry_id, enum=enum, header=False)
stats(epoch, quarter, half, year, nototals)
Statistics of a Page or the Book per Period.
Source code in src/bankr/cli/commands.py
@click.command()
@click.option("-e", "--epoch", type=click.STRING, default="-", help="Epoch to show")
@click.option("-q", "--quarter", is_flag=True, help="Show quarters")
@click.option("-h", "--half", is_flag=True, help="Show half-years/terms")
@click.option("-y", "--year", is_flag=True, help="Show years")
@click.option("-T", "--nototals", is_flag=True, default=False, help="Block totals")
def stats(epoch: str, quarter: bool, half: bool, year: bool, nototals: bool):
"""Statistics of a Page or the Book per Period."""
book = bk.unpickle()
bounds: list[datetime] = bh.sanitize_period(epoch)
period = "m"
if quarter:
period = "quarter"
if half:
period = "term"
if year:
period = "year"
page = bp.extract(book, bounds=bounds)
amounts = bp.amounts(page, period=period, pivot=True)
if not nototals:
amounts = bk.append_totals(amounts)
bp.show_amounts(amounts)
test()
For development purposes only.
Source code in src/bankr/cli/commands.py
@click.command()
# @click.argument("csv_file")
def test():
"""For development purposes only."""
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
get_assets(iban, **kwargs)
Calculate assets for an investment iban in cents.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
iban
|
str
|
The IBAN of the account to calculate the balance for |
required |
**kwargs
|
Additional optional parameters: - page (Page | None): The Book or a Page, see project wiki. If None (default), loads Book from disk using unpickle(). TODO Other kwargs (filter parameters) may be added in future versions. |
{}
|
Returns:
| Type | Description |
|---|---|
dict
|
The current balance in cents (integer) |
Source code in src/bankr/book.py
def get_assets(iban: str, **kwargs) -> dict:
"""Calculate assets for an investment iban in cents.
Args:
iban: The IBAN of the account to calculate the balance for
**kwargs: Additional optional parameters:
- page (Page | None): The Book or a Page, see project wiki.
If None (default), loads Book from disk using unpickle().
TODO Other kwargs (filter parameters) may be added in future versions.
Returns:
The current balance in cents (integer)
"""
# Prepare mandatory (unique fragments of) IBAN
iban = bh.sanitize_iban(iban)
# Prepare optional kwargs
params = {"page": None, "tag": ""}
params |= kwargs
page = params["page"]
tag = params["tag"]
if tag:
tag = bh.sanitize_tag(tag)
# If page is None, use the validated Book
if page is None:
page = bp.validate(unpickle())
page = bp.validate(page)
# Filter entries for the given IBAN and category/tag, calculate sum
invests = page[(page["iban"] == iban) & (page["category"] == "invest")]
assets = page[(page["iban"] == iban) & (page["category"] == "asset")]
if tag:
invest_cents = - invests[invests["tag"] == tag]["cents"].sum()
asset_cents = assets[assets["tag"] == tag]["cents"].sum()
else:
invest_cents = - invests["cents"].sum()
asset_cents = assets["cents"].sum()
# Convert from numpy.int64 to Python int
return {"invest": int(invest_cents), "asset": int(asset_cents)}
list_years_with_transactions(book)
bla
Source code in src/bankr/book.py
def list_years_with_transactions(book: pd.DataFrame) -> list[str]:
"""bla"""
years: list[str] = []
book["year"] = book["date"].dt.year
for year in range(1900, 2100):
if (book["year"] == year).sum() > 0:
years.append(str(year))
return years
pickle(book)
Save the Book as book-v3.pickle.
Before pickling, drop unused Optionals and categories, and create a backup
of book-v3.pickle, if present.
Source code in src/bankr/book.py
def pickle(book: Page) -> None:
"""Save the Book as `book-v3.pickle`.
Before pickling, drop unused Optionals and categories, and create a backup
of `book-v3.pickle`, if present.
"""
# Drop unused Optionals
drops = set(OPTIONALS) - set(BOOK.keys())
if drops:
book.drop(columns=drops, inplace=True)
# Remove unused categories
book["iban"] = book["iban"].cat.remove_unused_categories()
book["currency"] = book["currency"].cat.remove_unused_categories()
book["category"] = book["category"].cat.remove_unused_categories()
book["tag"] = book["tag"].cat.remove_unused_categories()
# Create backup, if needed
pickle_path = DATA_PATH / "book-v3.pickle"
if (pickle_path).is_file():
pickle_path.rename(DATA_PATH / ("book-v3_" + datetime.now().strftime("%Y%m%d-%H%M%S") + ".pickle"))
# And create "book-v3.pickle"
try:
book.to_pickle(pickle_path)
except RuntimeError as e:
sys.exit(f"Bankr Error - Book could not be saved.\n{e}")
# Book saved
bh.info(f"{i18n.t('book.book')} {i18n.t('book.saved')}")
unpickle()
Read the Book from book-v3.pickle.
Raise error, if the pickled Book is not found.
Source code in src/bankr/book.py
def unpickle() -> Page:
"""Read the Book from `book-v3.pickle`.
Raise error, if the pickled Book is not found.
"""
try:
book: Page = pd.read_pickle(DATA_PATH / "book-v3.pickle")
book = bp.validate(book)
return book
except FileNotFoundError as e:
sys.exit(f"Bankr Error - Book not found.\n{e}")
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
amounts(page, **kwargs)
Calculate the sum of cents per groupby, e.g. category (default), tag, or iban, and per period.
The period is given as string YYYY-mm (months, default), YYYY-Qn (quarters), YYYY-Tn (terms), or YYYY.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
period
|
kwarg
|
The string "month" (default), "quarter", "term", or "year". |
required |
pivot
|
kwarg
|
True if pivoted (default), otherwise stacked |
required |
groupby
|
kwarg
|
Field to group by. 'category' [default], or, e.g., 'tag', or 'iban' |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
A stacked or pivoted version of the sums in cents. |
DataFrame
|
The stacked data consists of columns period, category/tag/iban, and cents. |
DataFrame
|
The pivoted data is period in rows, and the group_field in columns. |
Source code in src/bankr/page.py
def amounts(page: Page, **kwargs) -> pd.DataFrame:
"""Calculate the sum of cents per groupby, e.g. category (default), tag, or iban, and per period.
The period is given as string YYYY-mm (months, default), YYYY-Qn (quarters), YYYY-Tn (terms), or YYYY.
Args:
period (kwarg, optional): The string "month" (default), "quarter", "term", or "year".
pivot (kwarg, optional): True if pivoted (default), otherwise stacked
groupby (kwarg, optional): Field to group by. 'category' [default], or, e.g., 'tag', or 'iban'
Returns:
A stacked or pivoted version of the sums in cents.
The stacked data consists of columns period, category/tag/iban, and cents.
The pivoted data is period in rows, and the group_field in columns.
"""
# Update params (defaults) with kwargs
params = {"period": "month", "pivot": False, "groupby": "category"}
params |= kwargs
# Work on a copy and remove unnecessary columns
copy = page.copy()
# Create copy.period from copy.date
copy["year"] = copy["date"].dt.year.astype("str")
copy["period"] = copy["date"].dt.strftime("%Y-%m")
if params["period"] == "quarter":
quarter = copy["date"].dt.quarter.astype("str")
copy["period"] = copy["year"].str.cat(others=quarter, sep="-Q")
elif params["period"] == "term":
term = copy["date"].dt.quarter.apply(lambda x: (x + 1) // 2).astype("str")
copy["period"] = copy["year"].str.cat(others=term, sep="-T")
elif params["period"] == "year":
copy["period"] = copy["year"]
# Calculate sums grouped by period and param["groupby"]
try:
amounts = copy.groupby(["period", params["groupby"]], as_index=False, observed=False)["cents"].sum()
except KeyError:
amounts = copy.groupby(["period", "category"], as_index=False, observed=False)["cents"].sum()
if params["pivot"]:
return amounts.pivot(index="period", columns=params["groupby"], values="cents")
else:
return amounts
categorize(page, **kwargs)
Auto-categorize the Entries in a Page or the Book in place.
Applies only to Entries with cat == "none", since other, potential manually overruled categories must NOT be changed.
Source code in src/bankr/page.py
def categorize(page: Page, **kwargs) -> None:
"""Auto-categorize the Entries in a Page or the Book in place.
Applies only to Entries with cat == "none", since other, potential manually overruled
categories must NOT be changed.
"""
# *Technical remarks*
# (1) return not needed due to inplace operations.
# (2) Tilde operator ~ is an elementwise Not (needed, see "where" operator).
# (3) catcon["nones"] limits replacement to page["category"] = "none". Index reset needed for "catcon" setup.
# (4) catcon["keys"] limits replacement, where the filter is fullfilled.
params = {"verbose": False}
params |= kwargs
catcon = pd.DataFrame(columns=["nones", "keys"], index=range(page.shape[0]))
page.reset_index(inplace=True)
catcon["nones"] = page["category"].str.contains("none")
for filtr in FILTERS:
category = filtr["category"]
column = filtr["column"]
keys = filtr["keys"]
catcon["keys"] = page[column].str.contains(keys, case=False)
page["category"] = page["category"].where(~catcon.all(axis=1), other=category)
page.set_index("entry_id", inplace=True)
# Verbose message
bh.info(f"{i18n.t('page.page')} {i18n.t('page.categorized')}", verbose=params["verbose"])
categorize_manually(book)
Manually categorize the Book inplace.
Loop over all Entries with category == "none", and offer a manual change of the category.
The manual categorization can be stopped at any time. In this case, the function will return an empty string
to the caller.
Source code in src/bankr/page.py
def categorize_manually(book: Page) -> None:
"""Manually categorize the Book inplace.
Loop over all Entries with `category == "none"`, and offer a manual change of the category.
The manual categorization can be stopped at any time. In this case, the function will return an empty string
to the caller.
"""
nones = sum(book["category"] == "none")
none = 1
for n in range(book.shape[0]):
transaction = book.iloc[n]
if transaction["category"] != "none":
continue
print("━" * 120)
print(f"{i18n.t('entry.entry')} {none}/{nones}")
by.show(book.iloc[[n]], header=False)
selection = bh.select_category()
if not selection:
return
book.loc[transaction.name, "category"] = selection
print(f"{i18n.t('page.new_cat')}: {i18n.t('cats.' + selection)}", "\n")
none += 1
time.sleep(0.5)
concat_pages(page1, page2, **kwargs)
Concat Pages or a Page to the Book.
Source code in src/bankr/page.py
def concat_pages(page1: Page, page2: Page, **kwargs) -> Page:
"""Concat Pages or a Page to the Book."""
params = {"verbose": False}
params |= kwargs
page = pd.concat([page1, page2], axis=0, ignore_index=False)
sort_page(page)
# Verbose message
bh.info(f"{i18n.t('page.page')} {i18n.t('page.imported')}", verbose=params["verbose"])
return page
extract(page, **kwargs)
Extract Entries from a Page or the Book
The extraction is done based on the category of the Entries, the iban and bounds (date interval),
or its offset account. All parameters are assumed to be sanitized.
offsets in the given list are considered as valid, and are therefore removed from the Page. All other parameters
are treated as inclusive, if given. The defaults below mean unfiltered, respectively.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
page
|
Page
|
The validated Page |
required |
Returns:
| Type | Description |
|---|---|
Page
|
The extracted Page |
Source code in src/bankr/page.py
def extract(page: Page, **kwargs) -> Page:
"""Extract Entries from a Page or the Book
The extraction is done based on the category of the Entries, the `iban` and `bounds` (date interval),
or its offset account. All parameters are assumed to be sanitized.
`offsets` in the given list are considered as valid, and are therefore removed from the Page. All other parameters
are treated as inclusive, if given. The defaults below mean unfiltered, respectively.
Args:
page (Page): The validated Page
Returns:
The extracted Page
"""
# Update default params
params = {"category": "", "iban": "", "bounds": [], "offsets": []}
params |= kwargs
# Extract Page from Book
copy = page.copy()
bounds = params["bounds"]
if len(bounds) > 0:
copy = copy[copy["date"].between(pd.to_datetime(bounds[0]), pd.to_datetime(bounds[1]), inclusive="both")]
if params["category"]:
copy = copy[copy["category"] == params["category"]]
if params["iban"]:
copy = copy[copy["iban"] == params["iban"]]
copy = copy.loc[~(copy["offset"].isin(params["offsets"]))] # TODO Does this work?
return copy
parse(csv_file, **kwargs)
Parse CSV and create a Page.
Parse the CSV, defined by <bank_code>.yaml, and validate the parsed data. Before the validation,
two pretty critical data transformations take place:
1. The estimation of the dates for date and valuta, which needs proper adjustment in 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")
figures = ["euros" if figure == "cents" else figure for figure in figures]
if params["details"]:
try:
figures += list(BOOK.keys())
except AttributeError:
pass
# Format dates
page["date"] = page["date"].dt.strftime("%d.%m.%Y")
if "valuta" in page.columns:
page["valuta"] = page["valuta"].dt.strftime("%d.%m.%Y")
# Format amount
page["euros"] = bh.to_euros(page["cents"], page["currency"])
page.drop(["currency"], axis=1, inplace=True)
# Filter the data
show_page = page[[*figures]]
# I18n of Figures
headers: list = []
if params["enum"]:
headers += ["#"]
for figure in figures:
headers.append(i18n.t(f"entry.{figure}"))
# i18n of category
show_page["category"] = page["category"].apply(lambda x: i18n.t(f"cats.{x}"))
# Show tag description if different from "none"
show_page["tag"] = page["tag"].apply(lambda x: "" if x == "none" else bh.describe_tag(x))
# Alignment of Figures
colalign: list = []
if params["enum"]:
colalign += ["right"]
if params["entry_id"]:
colalign += ["center"]
colalign += ["center", "left", "right", "left", "center", "center", "center"]
if params["header"]:
bh.title(i18n.t("page.entries"))
print(f"{i18n.t('page.total')}: {page['cents'].sum() / 100.0:.2f}€ ", end="")
print(f"({page.shape[0]} {i18n.t('page.entries')})\n")
print(
tabulate(show_page, headers=headers, showindex=params["enum"], tablefmt="rounded_outline", colalign=colalign),
"\n",
)
show_accounts(page, **kwargs)
Show a summary of the accounts in the Page.
It includes the IBAN of the accounts, the number of Entries, first and last Entry dates, and a balance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
page
|
Page
|
The Page containing the account data to be displayed. |
required |
active
|
bool
|
If True (default), shows only active accounts |
required |
details
|
bool
|
If True, displays detailed information about each account. Defaults to False. |
required |
Returns:
| Type | Description |
|---|---|
None
|
None, table for CLI. |
Source code in src/bankr/page.py
def show_accounts(page: Page, **kwargs) -> None:
"""Show a summary of the accounts in the Page.
It includes the IBAN of the accounts, the number of Entries, first and last Entry dates, and a balance.
Args:
page (Page): The Page containing the account data to be displayed.
active (bool, optional): If True (default), shows only active accounts
details (bool, optional): If True, displays detailed information about each account. Defaults to False.
Returns:
None, table for CLI.
"""
param = {"active": True, "details": False}
param |= kwargs
# Calculate the data of all accounts
accounts: list[dict] = []
for account in ACCOUNTS:
account["entries"] = sum(page["iban"] == account["iban"])
account["cents"] = page[page["iban"] == account["iban"]]["cents"].sum()
account["first_entry"] = ""
account["last_entry"] = ""
if account["entries"] > 0:
account["first_entry"] = page[page["iban"] == account["iban"]].date.min().strftime("%d.%m.%Y")
account["last_entry"] = page[page["iban"] == account["iban"]].date.max().strftime("%d.%m.%Y")
if param["active"] and account["active"] == "True":
accounts.append(account)
if not param["active"]:
accounts.append(account)
accounts = pd.DataFrame(accounts)
accounts["balance"] = (accounts["cents"] / 100.0).astype(float).apply(lambda x: f"{x:.2f}") + "€"
# Filter the data of accounts
figures = ["iban", "entries", "first_entry", "last_entry", "balance", "description"]
colalign = ["left", "right", "center", "center", "right", "left"]
if param["details"]:
figures += ["owner", "group", "type", "active"]
colalign += ["center", "center", "left", "center"]
accounts_to_show = accounts[[*figures]]
# I18n of the Figures
headers: list = []
for figure in figures:
headers.append(i18n.t(f"page.{figure}"))
# Show accounts
bh.title(i18n.t("page.accounts"))
print(f"{i18n.t('page.total')}: {accounts['cents'].sum() / 100.0:.2f}€ ", end="")
print(f"({accounts['entries'].sum()} {i18n.t('page.entries')})\n")
print(
tabulate(accounts_to_show, headers=headers, showindex=False, tablefmt="rounded_outline", colalign=colalign),
"\n",
)
show_amounts(amounts, **kwargs)
Show the Amounts per Period.
Displays the categories and balance (columns) per interval (rows) for all categories except "internal".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
amounts
|
DataFrame
|
A DataFrame containing the amounts to be displayed. |
required |
Returns:
| Type | Description |
|---|---|
None
|
None, table for CLI. |
Source code in src/bankr/page.py
def show_amounts(amounts: pd.DataFrame, **kwargs) -> None:
"""Show the Amounts per Period.
Displays the categories and balance (columns) per interval (rows) for all categories except "internal".
Args:
amounts (pd.DataFrame): A DataFrame containing the amounts to be displayed.
Returns:
None, table for CLI.
"""
params = {"internal": False, "period": "month", "pivot": False, "groupby": "category"}
params |= kwargs
if not params["internal"]:
amounts.drop(["internal"], axis=1, inplace=True)
# Round Cents to Euros
amounts = (amounts + 50) // 100
# I18n of the relevant categories
headers: list = []
for category in amounts.columns:
headers.append(i18n.t(f"cats.{category}")[0:8])
# TODO Tags
bh.title(f"{i18n.t('cli.statistics')} in {ANCHOR_CURRENCY}")
print(tabulate(amounts, headers=headers, showindex=True, tablefmt="rounded_outline"), "\n")
show_iban_statistics(page, iban)
Statistics of an Account.
It provides the number of Entries, the first and last Entry date, and the balance of the Account.
Source code in src/bankr/page.py
def show_iban_statistics(page: Page, iban: str) -> float:
"""Statistics of an Account.
It provides the number of Entries, the first and last Entry date, and the balance of the Account.
"""
try:
first_entry = page[page["iban"] == iban].date.min().strftime("%d.%m.%Y")
except: # noqa: E722
first_entry = f"{i18n.t('page.none'):>10}"
try:
last_entry = page[page["iban"] == iban].date.max().strftime("%d.%m.%Y")
except: # noqa: E722
last_entry = f"{i18n.t('page.none'):>10}"
try:
value = page[page["iban"] == iban]["cents"].sum() / 100.0
except: # noqa: E722
value = 0.0
# Print IBAN statistics
value_str = f"{value:.2f}{ANCHOR_CURRENCY}"
print(f" {i18n.t('page.entry_no'):<30}{len(page[page.iban == iban]):>10}")
print(f" {i18n.t('page.first_entry_long'):<30}{first_entry}")
print(f" {i18n.t('page.last_entry_long'):<30}{last_entry}")
print(f" {i18n.t('page.total'):<30}{value_str:>10}\n")
return value
sort_page(page)
Sort a Page by descending date and ascending iban
Source code in src/bankr/page.py
def sort_page(page: Page) -> None:
"""Sort a Page by descending date and ascending iban"""
page.sort_values(by=["date", "iban"], ascending=[False, True], inplace=True)
stringify(page)
Convert a Page into a Pandas DataFrame containing only string columns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
page
|
Page
|
A validated Page |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: An identically shaped DataFrame with all values transferred to strings. |
Source code in src/bankr/page.py
def stringify(page: Page) -> pd.DataFrame:
"""
Convert a Page into a Pandas DataFrame containing only string columns.
Args:
page (Page): A validated Page
Returns:
pd.DataFrame: An identically shaped DataFrame with all values transferred to strings.
"""
# Do not change page
page = page.copy()
# Calculate currency
page["cents"] = (page["cents"].astype(float) / 100).map("{:.2f}".format)
page["cents"] += page["currency"].astype(str)
# Prepare date formats
page["date"] = page["date"].dt.strftime(DATE_FORMAT)
page["valuta"] = page["valuta"].dt.strftime(DATE_FORMAT)
# Provide the i18n version of "category" and "tag"
page["category"] = page["category"].apply(lambda x: i18n.t(f"cats.{x}"))
page["tag"] = page["tag"].apply(lambda x: bh.describe_tag(x))
# Transfer all columns to strings
string_schema = pa.DataFrameSchema(
{
"date": pa.Column("string"),
"iban": pa.Column("string"),
"cents": pa.Column("string"),
"currency": pa.Column("string"),
"offset": pa.Column("string"),
"category": pa.Column("string"),
"tag": pa.Column("string"),
"source": pa.Column("string"),
"valuta": pa.Column("string"),
"name": pa.Column("string"),
"process": pa.Column("string"),
"description": pa.Column("string"),
"creditor": pa.Column("string"),
"mandate": pa.Column("string"),
"customer": pa.Column("string"),
},
coerce=True,
)
return string_schema.validate(page)
validate(raw_page)
Parse and validate a Raw Page into a Page.
The most important values are date/valuta, and cents. Te latter is kept as is before validation
(Int64 therefore preferred). Dates, preferrably datetime64[ns] are assumed to follow DATE_FORMAT, ifstr`.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raw_page
|
Page
|
The Raw Page to validate. |
required |
Returns:
| Type | Description |
|---|---|
Page
|
The validated Page. |
Source code in src/bankr/page.py
def validate(raw_page: Page) -> Page:
"""Parse and validate a Raw Page into a Page.
The most important values are date/valuta, and cents. Te latter is kept as is before validation
(Int64 therefore preferred). Dates, preferrably `datetime64[ns] are assumed to follow DATE_FORMAT, if `str`.
Args:
raw_page: The Raw Page to validate.
Returns:
The validated Page.
"""
# YAML defined column categories
ibans = [account["iban"] for account in ACCOUNTS]
currencies = [currency["currency"] for currency in CURRENCIES]
categories = [category["category"] for category in CATEGORIES]
tags = [tag["tag"] for tag in TAGS]
# Define Pandera Page schema
schema = pa.DataFrameSchema(
{
"date": pa.Column("datetime64[ns]"),
"iban": pa.Column("category", pa.Check.isin(ibans)),
"cents": pa.Column("Int64"),
"currency": pa.Column("category", pa.Check.isin(currencies), default=ANCHOR_CURRENCY),
"offset": pa.Column("string", default=""),
"category": pa.Column("category", pa.Check.isin(categories), default="none"),
"tag": pa.Column("category", pa.Check.isin(tags), default="none"),
"source": pa.Column("string", default=""),
"valuta": pa.Column("datetime64[ns]"),
"name": pa.Column("string", default=""),
"process": pa.Column("string", default=""),
"description": pa.Column("string", default=""),
"creditor": pa.Column("string", default=""),
"mandate": pa.Column("string", default=""),
"customer": pa.Column("string", default=""),
},
index=pa.Index("string"),
add_missing_columns=True,
strict="filter",
coerce=True,
)
# Create index = entry_id if needed
if raw_page.index.name != "entry_id":
raw_page["entry_id"] = raw_page.apply(lambda _: uuid.uuid4(), axis=1)
raw_page.set_index("entry_id", inplace=True)
# Format date, if a string, assuming the string is in DATE_FORMAT
if raw_page.dtypes["date"] == "str":
try:
raw_page["date"] = pd.to_datetime(raw_page["date"], format=DATE_FORMAT)
except ValueError:
sys.exit("Bankr Error - Date invalid")
# If valuta is missing in Raw Page, set valuta to date
if "valuta" not in raw_page.columns:
if "date" in raw_page.columns:
raw_page["valuta"] = raw_page["date"]
# Format valuta, if a string, assuming the string is in DATE_FORMAT
if raw_page.dtypes["valuta"] == "str":
try:
raw_page["valuta"] = pd.to_datetime(raw_page["valuta"], format=DATE_FORMAT)
except ValueError:
sys.exit("Bankr Error - Date invalid")
# Validate Page gracefully using Pandera
try:
page: Page = schema.validate(raw_page, lazy=True)
except pa.errors.SchemaErrors as exc:
print(f"\nBankr Error - Page schema showed problems:\n\n{exc.failure_cases}\n")
sys.exit()
# Add categories to Categoricals
page["iban"] = page["iban"].cat.add_categories(set(ibans) - set(page["iban"].cat.categories))
page["currency"] = page["currency"].cat.add_categories(set(currencies) - set(page["currency"].cat.categories))
page["category"] = page["category"].cat.add_categories(set(categories) - set(page["category"].cat.categories))
page["tag"] = page["tag"].cat.add_categories(set(tags) - set(page["tag"].cat.categories))
# Sort the columns and rows in Page
page = page[[*(MANDATORIES + OPTIONALS)]]
sort_page(page)
return page
Functions related to Entries
Entry module 'by' for Bankr.
This module provides functions for manipulating Entries.
bulk_edit(page, entry_ids, **kwargs)
Bulk edit entries by setting tags based on entry IDs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
page
|
Page
|
The Page containing entries to edit |
required |
entry_ids
|
list
|
List of entry IDs to find and edit |
required |
**kwargs
|
Optional parameters like tag="tag_value" |
{}
|
Returns:
| Type | Description |
|---|---|
list
|
List of entry IDs that were not found in the page |
Source code in src/bankr/entry.py
def bulk_edit(page: Page, entry_ids: list, **kwargs) -> list:
"""Bulk edit entries by setting tags based on entry IDs.
Args:
page: The Page containing entries to edit
entry_ids: List of entry IDs to find and edit
**kwargs: Optional parameters like tag="tag_value"
Returns:
List of entry IDs that were not found in the page
"""
params = {"tag": "none"}
params |= kwargs
not_found = []
# Process each entry ID
for entry_id in entry_ids:
try:
# Check if entry exists in page
if entry_id in page.index:
# Update the specified field (default: tag)
if "tag" in params:
page.loc[entry_id, "tag"] = params["tag"]
else:
not_found.append(entry_id)
except KeyError:
not_found.append(entry_id)
return not_found
edit(entry, asset_mode=False, tag='none')
Edit the Figures of an Entry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entry
|
Entry
|
The validated Entry to edit. |
required |
balance_mode
|
If True, calculate amount from target balance (default: False) |
required |
Returns:
| Type | Description |
|---|---|
Entry
|
The edited and validated Entry. |
Source code in src/bankr/entry.py
def edit(entry: Entry, asset_mode: bool = False, tag: str = "none") -> Entry:
"""Edit the Figures of an Entry.
Args:
entry: The validated Entry to edit.
balance_mode: If True, calculate amount from target balance (default: False)
Returns:
The edited and validated Entry.
"""
# Extract choices from env
iban_choices = [account["iban"] for account in ACCOUNTS]
currency_choices = [currency["currency"] for currency in CURRENCIES]
category_choices = [category["category"] for category in CATEGORIES]
tag_choices = [tag["tag"] for tag in TAGS]
# Get the first row and stringify it
row = bp.stringify(entry.iloc[[0]]) # TODO Is this a good solution?
entry_id = entry.index[0]
iban = row.at[entry_id, "iban"]
# Handle balance mode TODO Does not work correctly!
if asset_mode:
balances = bk.get_assets(iban, tag=tag)
invest_euros = balances["invest"] / 100.0
asset_euros = balances["asset"] / 100.0
current_euros = invest_euros + asset_euros
if balances["invest"] == 0:
bh.info("Invalid balance input or not an investment. Using direct amount mode.")
else:
bh.info(f"Balance of the Book: {current_euros:.2f}{ANCHOR_CURRENCY}")
bh.info(f" Investment: {invest_euros:.2f}{ANCHOR_CURRENCY}")
bh.info(f" Asset: {asset_euros:.2f}{ANCHOR_CURRENCY}")
target_balance_input = input("Enter target balance: ")
target_balance_cents = int(bh.to_cents(target_balance_input))
cents = target_balance_cents - balances["invest"] - balances["asset"]
euros = cents / 100.0
bh.info(f"Amount of Asset: {euros:.2f}{ANCHOR_CURRENCY}")
row.at[entry_id, "cents"] = f"{euros:.2f}{ANCHOR_CURRENCY}"
row.at[entry_id, "category"] = "asset"
row.at[entry_id, "tag"] = tag
# Build questions for each column
ask = []
for col in row.columns:
default = row.at[entry_id, col]
# Special handling for category columns
if col == "iban":
ask.append(iq.List(col, message=i18n.t("entry.iban"), choices=iban_choices, default=default))
elif col == "cents":
default = default[:-1] # Remove currency symbol, TODO Is this a good solution?
ask.append(iq.Text(col, message=i18n.t("entry.euros"), default=default))
elif col == "currency":
ask.append(iq.List(col, message=i18n.t("entry.currency"), choices=currency_choices, default=default))
elif col == "category":
ask.append(iq.List(col, message=i18n.t("entry.category"), choices=category_choices, default=default))
elif col == "tag":
ask.append(iq.List(col, message=i18n.t("entry.tag"), choices=tag_choices, default=default))
else:
ask.append(iq.Text(col, message=i18n.t(f"entry.{col}"), default=default))
# Prompt user
editions = iq.prompt(ask)
# Add entry_id and process cents
editions["entry_id"] = entry_id
if "cents" in editions:
editions["cents"] = bh.to_cents(editions["cents"])
# Create new entry
new_entry = pd.DataFrame([editions])
new_entry.set_index("entry_id", inplace=True)
return bp.validate(new_entry)
empty(iban)
Create an empty, validated Entry for the given IBAN.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
iban
|
str
|
The IBAN for the entry. |
required |
Returns:
| Type | Description |
|---|---|
Entry
|
An empty, validated Entry with today's date as date and valuta. |
Source code in src/bankr/entry.py
def empty(iban: str) -> Entry:
"""Create an empty, validated Entry for the given IBAN.
Args:
iban: The IBAN for the entry.
Returns:
An empty, validated Entry with today's date as date and valuta.
"""
today = date.today()
raw_entry = pd.DataFrame({"iban": [iban], "date": [today], "valuta": [today], "cents": ["0"]})
return bp.validate(raw_entry)
show(entry, **kwargs)
Show the Figures of an Entry in a tabular view.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entry
|
Page
|
An Entry, or the first row of a Page. |
required |
details
|
If True, show all fields. If False, show only mandatory fields. |
required | |
enum
|
If True, enumerate the lines for reference. |
required |
Source code in src/bankr/entry.py
def show(entry: Page, **kwargs) -> None:
"""Show the Figures of an Entry in a tabular view.
Args:
entry: An Entry, or the first row of a Page.
details: If True, show all fields. If False, show only mandatory fields.
enum: If True, enumerate the lines for reference.
"""
params = {"details": True, "enum": False, "header": True}
params |= kwargs
# Stringify deep copy of entry,
entry = bp.stringify(entry.copy().iloc[[0]])
# Figures to show
figures = [] # New object needed
figures += MANDATORIES
if params["details"]:
try: # List might be empty
figures += list(BOOK.keys())
except AttributeError:
pass
figures.remove("currency")
# Show the Entry
if params["header"]:
bh.title("Entry Mode")
print("━" * 120)
print(f" {i18n.t('entry.entry_id')} {entry.index[0]}")
print("─" * 120)
for i, figure in enumerate(figures, 1):
if params["enum"]:
print(f" {i:2d}.", end="")
value = entry[figure].iloc[0]
print(f" {i18n.t(f'entry.{figure}'):<20} {value:.94}")
print("━" * 120, "\n")
Helper Functions
Helper module bh for Bankr.
This module provides helper functions for TUI messages and data sanitizing.
confirm_save()
Confirmation dialog before the Book gets saved/pickled.
Source code in src/bankr/helpers.py
def confirm_save() -> bool:
"""Confirmation dialog before the Book gets saved/pickled."""
while True:
selection = input("Bankr Action - Save the Book (y/n)? ")
if selection == "y":
return True
elif selection == "n":
return False
describe_tag(tag)
Receive the description of a tag
Source code in src/bankr/helpers.py
def describe_tag(tag: str) -> str:
"""Receive the description of a tag"""
description = ""
for t in TAGS:
if t["tag"] == tag:
description = t["description"]
return description
info(info='', **kwargs)
Bankr Info
Print a short info, except verbose=False
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
info
|
str
|
Info. Defaults to "". |
''
|
Source code in src/bankr/helpers.py
def info(info: str = "", **kwargs) -> None:
"""Bankr Info
Print a short info, except `verbose=False`
Args:
info (str): Info. Defaults to "".
"""
params = {"verbose": True}
params |= kwargs
if params["verbose"]:
print(f"Bankr {i18n.t('helpers.info')} - {info}")
locate_entry(book, entry_id)
Locate an entry in the Book or a Page.
Source code in src/bankr/helpers.py
def locate_entry(book: Page, entry_id: str) -> Page:
"""Locate an entry in the Book or a Page."""
try:
entry = book.loc[[entry_id]]
except KeyError:
sys.exit("Bankr Error - Entry not found in the Book.")
return entry
sanitize_category(category='')
Sanitize the given category.
Provides a valid category, if the category information could be identified uniquely in categories.yaml
or its current translation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
category
|
str
|
Category information to be sanitized. Defaults to "". |
''
|
Returns:
| Name | Type | Description |
|---|---|---|
str |
str
|
Sanitized (internal) category as given in categories.yaml or empty string |
Source code in src/bankr/helpers.py
def sanitize_category(category: str = "") -> str:
"""Sanitize the given category.
Provides a valid `category`, if the category information could be identified uniquely in `categories.yaml`
or its current translation.
Args:
category (str, optional): Category information to be sanitized. Defaults to "".
Returns:
str: Sanitized (internal) category as given in categories.yaml or empty string
"""
candidates: list[str] = []
for value in CATEGORIES:
if (category.lower() in value["category"]) or (category in i18n.t("cats." + value["category"])):
candidates.append(value["category"])
if len(candidates) == 1:
return candidates[0]
else:
return ""
sanitize_entry_ids(entry_ids_str)
Sanitize comma-separated entry IDs string into a list.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entry_ids_str
|
str
|
Comma-separated string of entry IDs |
required |
Returns:
| Type | Description |
|---|---|
list
|
List of sanitized entry IDs |
Source code in src/bankr/helpers.py
def sanitize_entry_ids(entry_ids_str: str) -> list:
"""Sanitize comma-separated entry IDs string into a list.
Args:
entry_ids_str: Comma-separated string of entry IDs
Returns:
List of sanitized entry IDs
"""
if not entry_ids_str:
return []
# Split by comma and strip whitespace
entry_ids = [id.strip() for id in entry_ids_str.split(",") if id.strip()]
return entry_ids
sanitize_iban(iban='')
Sanitize a given IBAN or an IBAN fragment.
The string iban is checked against the IBANs within ACCOUNTS or accounts.yaml.
If the IBAN could be identified uniquely, it is returned. Unique fragements of IBANs are sufficient.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
iban
|
str
|
IBAN or IBAN fragment. Defaults to "". |
''
|
Returns:
| Name | Type | Description |
|---|---|---|
str |
str
|
Identified IBAN or empty string |
Source code in src/bankr/helpers.py
def sanitize_iban(iban: str = "") -> str:
"""Sanitize a given IBAN or an IBAN fragment.
The string `iban` is checked against the IBANs within `ACCOUNTS` or `accounts.yaml`.
If the IBAN could be identified uniquely, it is returned. Unique fragements of IBANs are sufficient.
Args:
iban (str, optional): IBAN or IBAN fragment. Defaults to "".
Returns:
str: Identified IBAN or empty string
"""
candidates: list[str] = []
for value in ACCOUNTS:
if iban.upper() in value["iban"]:
candidates.append(value["iban"])
if len(candidates) == 1:
return candidates[0]
else:
return ""
sanitize_period(period='-')
Sanitize period string.
Converts a string into a bounds list (datetime of start and end date), empty if invalid. - YYYY-MM is a month from 1900-01 to 2099-12. YYYY is a year from 1900 to 2099. - YYYY-Qx (x = 1...4) is a corresponding quarter, YYYY-Hx a halve-year or term. - YYYY+n (n=0...99) is 1...100 years beginning with YYYY. - yy leads to 20yy.
TODO Accept two digit years (separator year?) and quarters/terms.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
period
|
str
|
A string defining a time period. Defaults to "-", "" not allowed. |
'-'
|
Returns:
| Type | Description |
|---|---|
list[datetime]
|
list[datetime]: Bounds of the period, empty if invalid |
Source code in src/bankr/helpers.py
def sanitize_period(period: str = "-") -> list[datetime]:
"""Sanitize period string.
Converts a string into a bounds list (datetime of start and end date), empty if invalid.
- YYYY-MM is a month from 1900-01 to 2099-12. YYYY is a year from 1900 to 2099.
- YYYY-Qx (x = 1...4) is a corresponding quarter, YYYY-Hx a halve-year or term.
- YYYY+n (n=0...99) is 1...100 years beginning with YYYY.
- yy leads to 20yy.
TODO Accept two digit years (separator year?) and quarters/terms.
Args:
period (str): A string defining a time period. Defaults to "-", "" not allowed.
Returns:
list[datetime]: Bounds of the period, empty if invalid
"""
# No bounds
bounds: list = []
# period has to start with "19" or "20". If not, prepend "20"
if not re.match(r"^(19|20)", period):
period = "20" + period
# period = month, from 1900-01 to 2099-12
if re.match(r"^(19|20)?\d{2}-(0[1-9]|1[0-2])$", period):
period += "-01"
start = datetime.strptime(period, "%Y-%m-%d")
bounds = [start, start + relativedelta(day=31)]
# period = quarter or halve-year
if re.match(r"^(19|20)?\d{2}-(Q[1-4]|H[1-2])$", period):
year = int(period[0:4])
first = 3 * int(period[-1]) - 2
delta = 2
if period[-2] == "H":
first = 6 * int(period[-1]) - 5
delta = 5
bounds = [datetime(year, first, 1), datetime(year, first + delta, 1) + relativedelta(day=31)]
# period = year, from 1900 to 2099
if re.match(r"^(19|20)?\d{2}$", period):
bounds = [datetime(int(period), 1, 1), datetime(int(period), 12, 31)]
# period = years (<first year>+<further years>)
if re.match(r"^(19|20)?\d{2}\+\d{1,2}$", period):
year = int(period[0:4])
delta = int(period[-2:])
bounds = [datetime(year, 1, 1), datetime(year + delta, 12, 31)]
return bounds
sanitize_tag(tag='none')
Sanitize the given tag.
Provides a valid tag, if the tag information could be identified uniquely in tags.yaml.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tag
|
str
|
tag information to be sanitized. Defaults to "none". |
'none'
|
Returns:
| Name | Type | Description |
|---|---|---|
str |
str
|
Sanitized (internal) tag as given in categories.yaml or empty string |
Source code in src/bankr/helpers.py
def sanitize_tag(tag: str = "none") -> str:
"""Sanitize the given tag.
Provides a valid `tag`, if the tag information could be identified uniquely in `tags.yaml`.
Args:
tag (str, optional): tag information to be sanitized. Defaults to "none".
Returns:
str: Sanitized (internal) tag as given in categories.yaml or empty string
"""
candidates: list[str] = []
for value in TAGS:
if (tag.lower() in value["tag"]) or (tag.lower() in describe_tag(value["tag"]).lower()):
candidates.append(value["tag"])
if len(candidates) == 1:
return candidates[0]
else:
return "none"
select_category()
Provide CATEGORIES for manual selection.
The selection is enumerated as they show up in the parameter CATEGORIES (imported from categories.yaml).
Returns:
| Type | Description |
|---|---|
str
|
Selected category, or empty string for quit. |
Source code in src/bankr/helpers.py
def select_category() -> str:
"""Provide CATEGORIES for manual selection.
The selection is enumerated as they show up in the parameter CATEGORIES (imported from `categories.yaml`).
Args:
None.
Returns:
Selected category, or empty string for quit.
"""
# Display categories with enumeration
for index, category in enumerate(CATEGORIES):
translated_category = i18n.t("cats." + category["category"])
truncated_description = category["description"][:58]
print(f"[{index:>2}] - {translated_category:<15}{truncated_description}")
print("─" * 80)
# Get valid user selection
while True:
selection_input = input("Bankr Action - Select a category by [number] or (q)uit. ")
if selection_input == "q":
return ""
try:
selection = int(selection_input)
if 0 <= selection < len(CATEGORIES):
break
except ValueError:
pass
return CATEGORIES[selection]["category"]
title(subtitle='')
Clear CLI and print Bankr title
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subtitle
|
str
|
Additional subtitle. Defaults to "". |
''
|
Source code in src/bankr/helpers.py
def title(subtitle: str = "") -> None:
"""Clear CLI and print Bankr title
Args:
subtitle (str, optional): Additional subtitle. Defaults to "".
"""
# Clear CLI
if os.name == "nt": # On Windblows
os.system("cls")
else: # On valuable and expensive Linux
os.system("clear")
# Print title
print(Figlet().renderText("Bankr"))
if subtitle:
print(f"### {i18n.t('helpers.title')} ### - {subtitle}\n")
else:
print(f"### {i18n.t('helpers.title')} ###\n")
to_cents(x)
Filter an "amount of money" string in a CSV.
Strings indicating a certain amount of money can be quite pathological in CSV files of banks.
Therefore, these need to be filtered, before being converted to integers:
1. Limit to acceptable characters "0123456789.,+- ".
2. Only dots as separators of cents and thousands.
3. "" and "-" give "0".
4. Minus sign as first character, no plus sign.
"000" can happen as filter result, but this is fine for integers.
Source code in src/bankr/helpers.py
def to_cents(x: str) -> str:
"""Filter an "amount of money" string in a CSV.
Strings indicating a certain amount of money can be quite pathological in CSV files of banks.
Therefore, these need to be filtered, before being converted to integers:
1. Limit to acceptable characters `"0123456789.,+- "`.
2. Only dots as separators of cents and thousands.
3. "" and "-" give "0".
4. Minus sign as first character, no plus sign.
"000" can happen as filter result, but this is fine for integers.
"""
# Return 0, if invalid digits, check sign
accept = set("0123456789.,+- ")
check = set(x)
if not check.issubset(accept):
return "0"
x = x.replace(",", ".").replace("+", "").replace(" ", "")
if "-" in x:
x = "-" + x.replace("-", "")
if len(x) == 0 or x == "-":
return "0"
# Normal cases: *.??
if len(x) > 2 and x[-3] == ".":
return x.replace(".", "")
# Pathological cases
x = x + "00"
if len(x) == 3:
return x # Single digit
if len(x) >= 4:
if x[-4] == ".":
x = x[:-1] # One digit after separator
return x.replace(".", "")
to_euros(cents, currency)
Return a string representation of euros (major currency unit).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cents
|
Series
|
A Pandas Series containing amounts in cents (minor currency unit). |
required |
currency
|
Series
|
A Pandas Series containing currency symbols (typ. Categorical). |
required |
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: A pandas Series with values converted to euros and formatted as strings. |
Source code in src/bankr/helpers.py
def to_euros(cents: pd.Series, currency: pd.Series) -> pd.Series:
"""Return a string representation of euros (major currency unit).
Args:
cents (pd.Series): A Pandas Series containing amounts in cents (minor currency unit).
currency (pd.Series): A Pandas Series containing currency symbols (typ. Categorical).
Returns:
pd.Series: A pandas Series with values converted to euros and formatted as strings.
"""
return (cents / 100.0).apply(lambda x: f"{x:.2f}") + currency.astype(str)