Function Reference
This is a docstrings based and automatically generated Python function reference of Bankr.
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 a Transaction to the Book.
Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("iban")
def add(iban: str):
"""Add a Transaction to the Book."""
_bankr_title("Addition Mode")
# TODO Check, if string iban is a valid IBAN in the Book, stop otherwise
# TODO Commands add and edit have quite similar code snippets
book = bf.unpickle_book(data_path)
page = bf.create_transaction(iban)
tract = str(page["uuid"][0])
page.set_index("uuid", inplace=True)
page = bf.condition_book(page, data_path, "%d.%m.%Y")
book = bc.add_page_to_book(page, book)
book = bf.condition_book(book, data_path, "%d.%m.%Y")
tt.show_transaction(book, tract, True)
while True:
selection = input("Bankr Action - Add this Transaction to the Book (y/n)? ")
if selection == "y":
bf.pickle_book(book, data_path)
_bankr_info(f"Transaction {tract} added.", True)
break
elif selection == "n":
_bankr_info("No addition.", True)
break
cat(manual, uuid)
Categorize Transactions of a Page or the Book.
Basis of the automatic categorization is filters.yaml. Manual and auto cat work only
on transactions without a category.
Source code in src/bankr/cli/commands.py
@click.command()
@click.option("-m", "--manual", is_flag=True, help="Manually categorize Transactions without category")
@click.option("-u", "--uuid", type=click.STRING, help="Manually categorize a Transaction with a given ID")
def cat(manual: bool, uuid: str):
"""Categorize Transactions of a Page or the Book.
Basis of the automatic categorization is `filters.yaml`. Manual and auto cat work only
on transactions without a category.
"""
book = bf.unpickle_book(data_path)
if uuid and bc.tract_existing(book, uuid):
cats = bf.read_bankr_yaml(data_path / "cats.yaml")
_bankr_title("TractID Mode")
tt.show_transaction(book, uuid, True)
selection = bc.select_category(cats)
if selection != "quit":
book.loc[uuid, "cat"] = selection
_bankr_info(
f"{i18n.t('general.uuid')} {uuid} {i18n.t('general.categorized')}",
True,
)
elif uuid and not bc.tract_existing(book, uuid):
click.echo("Bankr Error - Invalid Transaction ID")
return
elif manual:
cats = bf.read_bankr_yaml(data_path / "cats.yaml")
_bankr_title("Manual Mode")
bc.mancat_tracts(book, cats)
_bankr_info(
f"{i18n.t('general.book')} {i18n.t('general.categorized')} - {sum(book['cat'] == 'none')} {i18n.t('general.wo_cat')}",
True,
)
else:
filtrs = bf.read_bankr_yaml(data_path / "filters.yaml")
_bankr_title("Auto Mode")
bc.autocat_tracts(book, filtrs) # noqa: F823
_bankr_info(
f"{i18n.t('general.book')} {i18n.t('general.categorized')} - {sum(book['cat'] == 'none')} {i18n.t('general.wo_cat')}",
True,
)
bf.pickle_book(book, data_path)
_bankr_info(f"{i18n.t('general.book')} {i18n.t('general.saved')}", True)
delete(uuid)
Delete a Transaction from the Book.
The Transaction to delete is defined by its Transaction ID or UUID.
Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("uuid")
def delete(uuid: str):
"""Delete a Transaction from the Book.
The Transaction to delete is defined by its Transaction ID or UUID.
"""
_bankr_title("Deletion Mode")
book = bf.unpickle_book(data_path)
tt.show_transaction(book, uuid, True)
while True:
selection = input("Bankr Action - Delete this Transaction from the Book (y/n)? ")
if selection == "y":
book = bc.delete_transaction(book, uuid)
bf.pickle_book(book, data_path)
_bankr_info(f"Transaction {uuid} deleted.", True)
break
elif selection == "n":
_bankr_info("No deletion.", True)
break
edit(uuid)
Edit a Transaction in the Book.
Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("uuid")
def edit(uuid: str):
"""Edit a Transaction in the Book."""
_bankr_title("Edit Transaction Mode")
book = bf.unpickle_book(data_path)
book = bf.condition_book(book, data_path, "%d.%m.%Y")
tract = bc.select_transaction(book, uuid)
print("-" * 120)
print(f"IBAN: {tract['iban']}")
print(f"Transaction: {tract.name}")
print("-" * 120)
tract = bf.edit_transaction(tract)
book = bc.update_transaction(book, tract)
tt.show_transaction(book, uuid, True)
while True:
selection = input("Bankr Action - Update this Transaction within the Book (y/n)? ")
if selection == "y":
bf.pickle_book(book, data_path)
_bankr_info(f"Transaction {uuid} updated.", True)
break
elif selection == "n":
_bankr_info(f"No update of Transaction {uuid}.", True)
break
excel(transfer)
Import/export the Book from/into an Excel file (not valid in v0.0.1!).
Limitation: The Excel version of the Book has no Excel data formating, and it uses the internal data representation (no i18n).
Source code in src/bankr/cli/commands.py
@click.command()
@click.argument("transfer", type=click.Choice(["import", "export"]))
def excel(transfer: str):
"""Import/export the Book from/into an Excel file (not valid in v0.0.1!).
Limitation: The Excel version of the Book has no Excel data formating, and it uses the internal data
representation (no i18n).
"""
if transfer == "import":
_bankr_title("Excel Import")
book = bf.unexcel_book(data_path)
book = bf.condition_book(book, data_path, "%d.%m.%Y")
_bankr_info(
f"{i18n.t('general.book')} {i18n.t('general.imported')} {i18n.t('general.from_excel')}",
True,
)
bf.pickle_book(book, data_path)
_bankr_info(f"{i18n.t('general.book')} {i18n.t('general.saved')}", True)
else:
_bankr_title("Excel Export")
book = bf.unpickle_book(data_path)
bf.excel_book(book, data_path)
_bankr_info(
f"{i18n.t('general.book')} {i18n.t('general.exported')} {i18n.t('general.to_excel')}",
True,
)
new()
Create a new Book in DATA_PATH, see bankr.yaml.
Creates an almost empty Book, which contains a zero value "Initial Transaction" 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 Transaction" for each
account in `accounts.yaml`.
"""
_bankr_title("New Book Mode")
bf.create_new_book(data_path)
_bankr_info("New Book created! Return to a backup, if processed erroneously.", True)
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"
_bankr_title(subtitle)
# Parse CSV to Page
page = bf.parse_csv(data_path, csv_file)
_bankr_info(f"{csv_file} {i18n.t('general.parsed')}", verbose)
book = bf.unpickle_book(data_path)
# Auto-categorize Page
filtrs = bf.read_bankr_yaml(data_path / "filters.yaml")
bc.autocat_tracts(page, filtrs)
_bankr_info(f"{i18n.t('general.page')} {i18n.t('general.categorized')}", verbose)
if add:
# Import Page to Book, and save it
book = bc.add_page_to_book(page, book)
book = bf.condition_book(book, data_path, "%d.%m.%Y")
_bankr_info(f"{i18n.t('general.page')} {i18n.t('general.imported')}", verbose)
bf.pickle_book(book, data_path)
_bankr_info(f"{i18n.t('general.book')} {i18n.t('general.saved')}", True)
else:
# Print statistics of the relevant IBAN
iban = csv_file.split("-")[0]
click.echo(f"- {i18n.t('general.page')}")
page_balance = tt.show_iban_stats(page, iban)
click.echo(f"- {i18n.t('general.book')}")
book_balance = tt.show_iban_stats(book, iban)
click.echo(f" {i18n.t('general.new_balance'):<32}{book_balance + page_balance}\n")
while True:
selection = input("Bankr Action - Show Transactions of the created Page (y/n)? ") #
if selection == "y":
tract = tt.show_page(page, 0, 0, "", "")
if tract == "overflow":
_bankr_info("Maximum number of Transactions is 200.", True)
break
elif tract == "quit":
break
else:
tt.show_transaction(page, tract, True)
break
elif selection == "n":
break
plot(year, span, quarter, term, full)
NOT OPERATIONAL for v0.0.3!
Plot time information on CLI.
Plot the amounts within a category per time interval on CLI. The time interval is a month, a quarter, a term or half-year, or a year. When ambiguous, larger intervals win.
Limitations: Very small fractions of the total amount can not be plotted on CLI. Therefore, it can only provide a first impression of the actual distribution.
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 plot")
@click.option("-s", "--span", type=click.IntRange(min=1, max=10, clamp=True), help="Number of years to plot")
@click.option("-q", "--quarter", is_flag=True, help="Plot quarters")
@click.option("-t", "--term", is_flag=True, help="Plot terms/half-years")
@click.option("-f", "--full", is_flag=True, help="Plot full years")
def plot(year: int, span: int, quarter: bool, term: bool, full: bool):
"""NOT OPERATIONAL for v0.0.3!
Plot time information on CLI.
Plot the amounts within a category per time interval on CLI. The time interval
is a month, a quarter, a term or half-year, or a year. When ambiguous, larger
intervals win.
Limitations: Very small fractions of the total amount can not be plotted on CLI.
Therefore, it can only provide a first impression of the actual distribution.
"""
book = bf.unpickle_book(data_path)
if not year:
year = 0
if not span and year:
span = 1
amounts = bc.calc_cat_per_interval(book, year, span, quarter=quarter, term=term, full=full)
plot_title = i18n.t("general.monthly")
if quarter:
plot_title = i18n.t("general.quarterly")
if term:
plot_title = i18n.t("general.per_term")
if full:
plot_title = i18n.t("general.yearly")
_bankr_title("")
tp.plot_cat_per_interval(amounts, plot_title)
show(year, month, cat, iban)
Show the Transactions of an interval or an IBAN.
Options restrict the displayed Transactions to the given time interval (month or year), or limit these to an IBAN.
Source code in src/bankr/cli/commands.py
@click.command()
@click.option("-y", "--year", type=click.IntRange(min=1900, max=2100, clamp=True), help="Year to show")
@click.option("-m", "--month", type=click.IntRange(min=1, max=12, clamp=True), help="month to show")
@click.option("-c", "--cat", type=click.STRING, help="Category to show") # TODO Apply i18n
@click.option("-i", "--iban", type=click.STRING, help="IBAN to show")
def show(year: int, month: int, cat: str, iban: str):
"""Show the Transactions of an interval or an IBAN.
Options restrict the displayed Transactions to the given time interval (month or year),
or limit these to an IBAN.
"""
if not year:
year = 0
if not month:
month = 0
if not iban:
iban = ""
elif not bc.iban_valid(iban):
iban = ""
book = bf.unpickle_book(data_path)
_bankr_title("Page Mode")
tract = tt.show_page(book, year, month, cat, iban)
if tract == "overflow":
_bankr_info("Maximum number of Transactions is 200.", True)
elif tract == "quit":
return
else:
tt.show_transaction(book, tract, True)
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.
"""
book = bf.unpickle_book(data_path)
if not span:
span = 200
period = "m"
if quarter:
period = "q"
if term:
period = "t"
if full:
period = "f"
if year: # Transaction Statistics
_bankr_title(i18n.t("general.statistics"))
amounts = bc.sum_per_cat_period(book, year, span, period=period, pivot=True)
amounts = bc.append_totals(amounts)
tt.show_income_per_interval(amounts)
else: # Statistics of the Book
_bankr_title(f"{i18n.t('general.book_stats')}")
accounts = bf.read_bankr_yaml(data_path / "accounts.yaml")
extended_accounts: list[dict] = bc.calc_book_stats(book, accounts)
tt.show_book_stats(extended_accounts)
Functions related to File Activity
Bankr module for parsing of CSVs or Pages, and other file operations.
condition_book(book, path, date_format)
Conditioning of a Page or the Book to fit to book-v1.yaml.
Conditioning steps are
(1) Condition dates to Pandas datetime64[ns] (column date is mandatory),
(2) Condition the local IBANs and the Bankr cat to Pandas category,
(3) Remove NaNs from cat, which are not allowed,
(4) Remove unnecessary spaces in desc.
Source code in src/bankr/files.py
def condition_book(book: pd.DataFrame, path: Path, date_format: str) -> pd.DataFrame:
"""Conditioning of a Page or the Book to fit to `book-v1.yaml`.
Conditioning steps are
(1) Condition dates to Pandas `datetime64[ns]` (column *date* is mandatory),
(2) Condition the local IBANs and the *Bankr cat* to Pandas `category`,
(3) Remove NaNs from `cat`, which are not allowed,
(4) Remove unnecessary spaces in *desc*.
"""
book_format = read_bankr_yaml(path / "book-v1.yaml")
# Condition dates
for key in book_format:
if book_format[key] == "datetime64[ns]":
book[key] = pd.to_datetime(book[key], format=date_format)
else:
book[key] = book[key].astype(book_format[key])
# Condition categoricals to "iban", "cat", and "curr"
# TODO Check "curr"
# TODO Improve Pandas notation
accounts = read_bankr_yaml(path / "accounts.yaml")
iban_list = []
for account in accounts:
iban_list.append(account["iban"])
book.iban = book.iban.cat.set_categories(iban_list)
cats = read_bankr_yaml(path / "cats.yaml")
cat_list = []
for cat in cats:
cat_list.append(cat["cat"])
book.cat = book.cat.cat.set_categories(cat_list)
# Remove NaNs from "cat"
book["cat"] = book["cat"].fillna(value="none")
# Replace NAType data with empty strings in string-based columns
# Remove unnecessary spaces in strings
for key in book_format:
if book_format[key] == "string":
book[key] = book[key].fillna("")
book[key] = book[key].apply(_remove_spaces).astype(book_format[key])
# Sort columns
book = book[[*book_format]]
return book
create_new_book(data_path)
Create new Book.
Creates an almost empty book, which contains zero value "Initial Transaction" for each
account in accounts.yaml. The path to be used is taken from bankr.yaml. If an existing
Book is overwritten accidentially, use the backup file, see the documentation.
Source code in src/bankr/files.py
def create_new_book(data_path: Path) -> None:
"""Create new Book.
Creates an almost empty book, which contains zero value "Initial Transaction" for each
account in `accounts.yaml`. The path to be used is taken from `bankr.yaml`. If an existing
Book is overwritten accidentially, use the backup file, see the documentation.
"""
book_format = read_bankr_yaml(data_path / "book-v1.yaml")
accounts = read_bankr_yaml(data_path / "accounts.yaml")
tracts = len(accounts)
yesterday = datetime.today() - timedelta(days=1)
date_values = [yesterday.strftime("%d.%m.%Y")] * tracts
iban_values = []
for n in range(tracts):
iban_values.append(accounts[n]["iban"])
cents_values = [0] * tracts
curr_values = ["€"] * tracts
cat_values = ["none"] * tracts
desc_values = ["Initial Transaction"] * tracts
source_values = ["initial"] * tracts
empty_values = [""] * tracts
book = pd.DataFrame()
for key in book_format:
if key == "date" or key == "valuta":
book[key] = pd.DataFrame(date_values)
elif key == "iban":
book[key] = pd.DataFrame(iban_values)
elif key == "cents":
book[key] = pd.DataFrame(cents_values)
elif key == "curr":
book[key] = pd.DataFrame(curr_values)
elif key == "cat":
book[key] = pd.DataFrame(cat_values)
elif key == "desc":
book[key] = pd.DataFrame(desc_values)
elif key == "source":
book[key] = pd.DataFrame(source_values)
else:
book[key] = pd.DataFrame(empty_values)
book["uuid"] = [uuid.uuid4() for _ in range(tracts)]
book = book.set_index("uuid")
book = condition_book(book, data_path, "%d.%m.%Y")
pickle_book(book, data_path)
create_transaction(iban)
Create a Page with one Transaction interactively.
Source code in src/bankr/files.py
def create_transaction(iban: str) -> pd.DataFrame:
"""Create a Page with one Transaction interactively."""
# TODO The dtypes of the Pages returned from `parse_csv` and `create_transaction` are inconsistent.
# TODO Offer cat with choices.
# TODO Select today, if date/valuta is invalid. Accept empty date strings.
ask = [
iq.Text("date", message="Date (dd.mm.YYYY)", validate=_validate_date),
iq.List("curr", message="Currency", choices=["€", "$"], default="€"),
iq.Text("cents", message="Value ({curr})"),
iq.Text("cat", message="Category"),
iq.Text("valuta", message="Valuta (dd.mm.YYYY)"),
iq.Text("offset", message="Offset (Empty, or IBAN)", validate=_validate_iban),
iq.Text("name", message="Account holder (*)"),
iq.Text("process", message="Process (*)"),
iq.Text("desc", message="Description (*)"),
iq.Text("creditor", message="Creditor ID (*)"),
iq.Text("mandate", message="Mandate Reference (*)"),
iq.Text("customer", message="Customer Reference (*)"),
]
tract = iq.prompt(ask)
page = pd.DataFrame({key: [value] for key, value in tract.items()})
page["cents"] = page["cents"].apply(to_cents)
page["iban"] = iban
page["uuid"] = uuid.uuid4()
page["source"] = "interactive"
page = page.astype("string")
return page
edit_transaction(tract)
Edit a Transaction.
Source code in src/bankr/files.py
def edit_transaction(tract: pd.Series) -> pd.Series:
"""Edit a Transaction."""
tract = tract.fillna("") # <NA> not allowed as default
uuid = tract.name
iban = tract["iban"]
source = tract["source"]
ask = [
iq.Text(
"date", message="Date (dd.mm.YYYY)", validate=_validate_date, default=tract["date"].strftime("%d.%m.%Y")
),
iq.List("curr", message="Currency", choices=["€", "$"], default=tract["curr"]),
iq.Text("cents", message="Value ({curr})", default=f"{tract['cents'] / 100: >10.2f}"),
iq.Text("cat", message="Category", default=tract["cat"]),
iq.Text("valuta", message="Valuta (dd.mm.YYYY)", default=tract["date"].strftime("%d.%m.%Y")),
# iq.Text("offset", message="Offset (Empty, or IBAN)", validate=_validate_iban, default=tract["offset"]), # TODO Offset might be an invalid IBAN.
iq.Text("offset", message="Offset (Empty, or IBAN)", default=tract["offset"]),
iq.Text("name", message="Account holder (*)", default=tract["name"]),
iq.Text("process", message="Process (*)", default=tract["process"]),
iq.Text("desc", message="Description (*)", default=tract["desc"]),
iq.Text("creditor", message="Creditor ID (*)", default=tract["creditor"]),
iq.Text("mandate", message="Mandate Reference (*)", default=tract["mandate"]),
iq.Text("customer", message="Customer Reference (*)", default=tract["customer"]),
]
editions = iq.prompt(ask)
tract = pd.Series(editions)
tract["date"] = datetime.strptime(tract["date"], "%d.%m.%Y")
tract["valuta"] = datetime.strptime(tract["valuta"], "%d.%m.%Y")
tract["cents"] = int(to_cents(tract["cents"]))
tract["iban"] = iban
tract.rename(uuid, inplace=True)
tract["source"] = source + " (edited)"
return tract
excel_book(book, path)
Export the book as book.xlsx into path/xlsx.
Do not export, if there is already a book-v1.xlsx present.
Remark Function not oerational in v0.0.1 of Bankr.
Source code in src/bankr/files.py
def excel_book(book: pd.DataFrame, path: Path) -> None:
"""Export the book as `book.xlsx` into `path/xlsx`.
Do not export, if there is already a `book-v1.xlsx` present.
*Remark* Function not oerational in v0.0.1 of Bankr.
"""
# TODO Check if ok with index uuid and source
xlsx_path = path / "xlsx" / "book-v1.xlsx"
if (xlsx_path).is_file():
sys.exit("Bankr Error - Excel already present. Book was not exported.")
try:
book.to_excel(xlsx_path, sheet_name="book", index=False)
except RuntimeError as e:
sys.exit(f"Bankr Error - Book could not be exported to Excel.\n{e}")
parse_csv(path, csv_file)
Parse CSV and create a Page.
Parse the CSV, defined by <bank_code>.yaml, and condition the parsed data.
Source code in src/bankr/files.py
def parse_csv(path: Path, csv_file: str) -> pd.DataFrame:
"""Parse CSV and create a Page.
Parse the CSV, defined by `<bank_code>.yaml`, and condition the parsed data.
"""
# TODO The dtypes of the Pages returned from `parse_csv` and `create_transaction` are inconsistent.
# Check IBAN
iban = csv_file.split("-")[0]
source = csv_file.replace(".", "-").split("-")[1]
if not IBAN(iban).is_valid:
sys.exit("Bankr Error - Filename contains invalid IBAN")
# Get CSV format dictionary from JSON
csv_format_path = path / ("csv/" + IBAN(iban).bank_code + ".yaml")
csv_format = read_bankr_yaml(csv_format_path)
# Read CSV and add missing data series
csv_path = path / ("csv/" + csv_file)
try:
csv = pd.read_csv(
csv_path,
sep=csv_format["sep"],
encoding=csv_format["encoding"],
dtype="string",
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}")
# Create Transaction IDs
csv["uuid"] = [uuid.uuid4() for _ in range(csv.shape[0])]
csv.uuid = csv.uuid.astype("string")
csv.set_index("uuid", inplace=True)
# Create cents - tricky operation, see documentation
csv.cents = csv.cents.apply(to_cents).astype("string")
csv["iban"] = iban
csv.iban = csv.iban.astype("string")
csv["source"] = source
csv["source"] = csv["source"].astype("string")
missing = csv_format["missing"]
for key in missing:
csv[key] = missing[key]
csv[key] = csv[key].astype("string")
page = condition_book(csv, path, csv_format["dateformat"])
return page
pickle_book(book, path)
Save the Book as book-v1.pickle in path.
If an Excel version of the Book is present under xlsx, do not pickle.
Backup the old book-v1.pickle as book-v1_<datetime>.pickle, before pickling a new version.
Source code in src/bankr/files.py
def pickle_book(book: pd.DataFrame, path: Path) -> None:
"""Save the Book as `book-v1.pickle` in `path`.
If an Excel version of the Book is present under `xlsx`, do not pickle.
Backup the old `book-v1.pickle` as `book-v1_<datetime>.pickle`, before pickling a new version.
"""
# No pickle, if XLSX version is present
if (path / "xlsx" / "book-v1.xlsx").is_file():
sys.exit("Bankr Error - Excel version present. Book could not be saved.")
# Create backup
pickle_path = path / "book-v1.pickle"
if (pickle_path).is_file():
pickle_path.rename(path / ("book-v1_" + datetime.now().strftime("%Y%m%d-%H%M%S") + ".pickle"))
# And create "book-v1.pickle"
try:
book.to_pickle(pickle_path)
except RuntimeError as e:
sys.exit(f"Bankr Error - Book could not be saved.\n{e}")
read_bankr_yaml(path)
Read parameters from YAML file format.
Source code in src/bankr/files.py
def read_bankr_yaml(path: Path) -> dict:
"""Read parameters from YAML file format."""
try:
with open(path, "r") as file:
bankr_yaml = yaml.safe_load(file)
return bankr_yaml
except FileNotFoundError as e:
sys.exit(f"Bankr Error - YAML file not found.\n{e}")
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/files.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.
"""
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(".", "")
unexcel_book(path)
Import the Book from xlsx/book.xlsx and rename the Excel file.
We make a backup from book-v1.xlsx as book-v1-<datetime>.xlsx.
Remark Function not oerational in v0.0.1 of Bankr.
Source code in src/bankr/files.py
def unexcel_book(path: Path) -> pd.DataFrame:
"""Import the Book from `xlsx/book.xlsx` and rename the Excel file.
We make a backup from `book-v1.xlsx` as `book-v1-<datetime>.xlsx`.
*Remark* Function not oerational in v0.0.1 of Bankr.
"""
# TODO Check if ok with index uuid and source
xlsx_path = path / "xlsx" / "book-v1.xlsx"
try:
book = pd.read_excel(xlsx_path, sheet_name="book", index_col=None)
book = condition_book(book, path, "%d.%m.%Y")
except FileNotFoundError as e:
sys.exit(f"Bankr Error - Book could not be imported from Excel.\n{e}")
xlsx_path.rename(path / ("xlsx/book-v1_" + datetime.now().strftime("%Y%m%d-%H%M%S") + ".xlsx"))
return book
unpickle_book(path)
Read the Book from book-v1.pickle in path.
Raise error, if the pickled Book is not found.
Source code in src/bankr/files.py
def unpickle_book(path: Path) -> pd.DataFrame:
"""Read the Book from `book-v1.pickle` in `path`.
Raise error, if the pickled Book is not found.
"""
try:
book = pd.read_pickle(path / "book-v1.pickle")
return book
except FileNotFoundError as e:
sys.exit(f"Bankr Error - Book not found.\n{e}")
Functions related to Calculations
Bankr module for data manipulation and statistics.
add_page_to_book(page, book)
Add a Page to the Book.
This is an in memory and in place operation.
Source code in src/bankr/calc.py
def add_page_to_book(page: pd.DataFrame, book: pd.DataFrame) -> pd.DataFrame:
"""Add a Page to the Book.
This is an in memory and in place operation.
"""
book = pd.concat([page, book], axis=0, ignore_index=False)
book.sort_values(by=["date", "iban"], ascending=[False, True], inplace=True)
return book
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/calc.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.
"""
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
autocat_tracts(page, filtrs)
Auto-categorize a Page or the Book in place.
Applies only to Transactions 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["cat"] = "none". Index reset needed for "catcon" setup. (4) catcon["keys"] limits replacement, where the filter is fullfilled.
Source code in src/bankr/calc.py
def autocat_tracts(page: pd.DataFrame, filtrs: list) -> None:
"""Auto-categorize a Page or the Book in place.
Applies only to Transactions 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["cat"] = "none". Index reset needed for "catcon" setup.
(4) catcon["keys"] limits replacement, where the filter is fullfilled.
"""
catcon = pd.DataFrame(columns=["nones", "keys"], index=range(page.shape[0]))
page.reset_index(inplace=True)
catcon.nones = page["cat"].str.contains("none")
for filtr in filtrs:
category = filtr["cat"]
column = filtr["col"]
keys = filtr["keys"]
catcon["keys"] = page[column].str.contains(keys, case=False)
page["cat"].where(~catcon.all(axis=1), other=category, inplace=True)
page.set_index("uuid", inplace=True)
calc_book_stats(book, accounts)
Calculate the account statistics of the Book.
tbc
Source code in src/bankr/calc.py
def calc_book_stats(book: pd.DataFrame, accounts: pd.DataFrame) -> list[dict]:
"""Calculate the account statistics of the Book.
tbc
"""
extended_accounts: list[dict] = []
for account in accounts:
account["tracts"] = sum(book["iban"] == account["iban"])
account["balance"] = Money.m(book[book["iban"] == account["iban"]].cents.sum() / 100.0)
try:
account["first_tract"] = book[book["iban"] == account["iban"]].date.min().strftime("%d.%m.%Y")
except: # noqa: E722
account["first_tract"] = ""
try:
account["last_tract"] = book[book["iban"] == account["iban"]].date.max().strftime("%d.%m.%Y")
except: # noqa: E722
account["last_tract"] = ""
extended_accounts.append(account)
# book_stats = pd.DataFrame(accounts)
return extended_accounts
delete_transaction(book, uuid)
Delete a Transaction from the Book or from a Page.
Source code in src/bankr/calc.py
def delete_transaction(book: pd.DataFrame, uuid: str) -> pd.DataFrame:
"""Delete a Transaction from the Book or from a Page."""
try:
return book.drop(index=uuid, inplace=False)
except KeyError:
sys.exit(f"Bankr Error - Transaction {uuid} not found.")
iban_valid(iban)
Is a given IBAN string a valid IBAN?
Source code in src/bankr/calc.py
def iban_valid(iban: str) -> bool:
"""Is a given IBAN string a valid IBAN?"""
return IBAN(iban, allow_invalid=True).is_valid
list_years_with_tracts(book)
bla
Source code in src/bankr/calc.py
def list_years_with_tracts(book: pd.DataFrame) -> list[int]:
"""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
mancat_tracts(book, cats)
Manually categorize the Book in place.
Loop over all Transactions with cat == "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 a "quit" to the
caller.
Source code in src/bankr/calc.py
def mancat_tracts(book: pd.DataFrame, cats: list) -> None:
"""Manually categorize the Book in place.
Loop over all Transactions with `cat == "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 a `"quit"` to the
caller.
"""
all_tracts = book.shape[0]
none_tracts = sum(book["cat"] == "none")
none = 1
for n in range(all_tracts):
tract = book.iloc[n]
if tract["cat"] != "none":
continue
print("+" * 120)
print(f"{i18n.t('general.transaction')} {none}/{none_tracts}")
tt.show_transaction(book, tract.name, True)
selection = select_category(cats)
if selection == "quit":
return
book.loc[tract.name, "cat"] = selection
print(f"{i18n.t('general.new_cat')}: {i18n.t('cats.' + selection)}")
none += 1
time.sleep(0.5)
select_category(cats)
Provide a list of categories for selection.
The categories are enumerated as they show up in cats.yaml.
It returns the internal name of the category selected.
Source code in src/bankr/calc.py
def select_category(cats: list) -> str:
"""Provide a list of categories for selection.
The categories are enumerated as they show up in `cats.yaml`.
It returns the internal name of the category selected.
"""
n = 0
for cat in cats:
print(f"[{n:>2}] - {i18n.t('cats.' + cat['cat']):<15}{cat['desc'][:58]}")
n = n + 1
print("-" * 80)
while True:
try:
selection = input("Bankr Action - Select a category by [number] or (q)uit. ")
if selection == "q":
return "quit" # Quit
selection = int(selection)
if selection < 0 or selection >= len(cats):
raise ValueError
break
except ValueError:
continue
print("-" * 80)
return cats[selection]["cat"]
select_transaction(book, uuid)
Select a Transaction from the Book or a Page.
Source code in src/bankr/calc.py
def select_transaction(book: pd.DataFrame, uuid: str) -> pd.Series:
"""Select a Transaction from the Book or a Page."""
try:
return book.loc[uuid]
except KeyError:
sys.exit(f"Bankr Error - Transaction {uuid} not found.")
sum_per_cat_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/calc.py
def sum_per_cat_period(book: pd.DataFrame, 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
"""
# 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 Transactions 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", "cat"], 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="cat", values="main")
else:
return stacked
tract_existing(book, uuid)
Is a Transaction ID (UUID) existing in the Book?
Source code in src/bankr/calc.py
def tract_existing(book: pd.DataFrame, uuid: str) -> bool:
"""Is a Transaction ID (UUID) existing in the Book?"""
return uuid in book.index.values
update_transaction(book, tract)
Update a Transaction of the Book or of a Page.
If the Transaction ID is not existing, the Transaction will be appended.
Source code in src/bankr/calc.py
def update_transaction(book: pd.DataFrame, tract: pd.Series) -> pd.DataFrame:
"""Update a Transaction of the Book or of a Page.
If the Transaction ID is not existing, the Transaction will be appended.
"""
uuid = tract.name
try:
book.loc[uuid] = tract
return book
except KeyError:
sys.exit(f"Bankr Error - Transaction {uuid} could not be replaced or appended.")
Functions related to TUI
Bankr module for data presentation on TUI.
show_book_stats(extended_accounts)
Show the account statistics of the Book
tbc
Source code in src/bankr/tuitable.py
def show_book_stats(extended_accounts: pd.DataFrame) -> None:
"""Show the account statistics of the Book
tbc
"""
sum_tracts: int = 0
sum_balance = Money.m(0, "€")
iban: str = i18n.t("general.iban")
tract: str = i18n.t("general.tract")
balance: str = i18n.t("general.balance")
first_tract: str = i18n.t("general.first_tract")
last_tract: str = i18n.t("general.last_tract")
account_desc: str = i18n.t("general.account_desc")
stats: str = PrettyTable(field_names=[iban, tract, balance, first_tract, last_tract, account_desc])
stats.align = "l"
stats.align[tract] = "r"
stats.align[balance] = "r"
for account in extended_accounts:
sum_tracts = sum_tracts + account["tracts"]
sum_balance = sum_balance + account["balance"]
stats.add_row(
[
account["iban"],
account["tracts"],
account["balance"],
account["first_tract"],
account["last_tract"],
account["desc"][:40],
]
)
print(f"{i18n.t('general.total_amount')}: {sum_balance} ({sum_tracts} {i18n.t('general.transactions')})\n")
print(stats, "\n")
show_iban_stats(page, iban)
Statistics of an IBAN.
It provides the number of Transactions, the first and last Transaction date, and the IBAN balance.
Source code in src/bankr/tuitable.py
def show_iban_stats(page: pd.DataFrame, iban: str) -> Money:
"""Statistics of an IBAN.
It provides the number of Transactions, the first and last Transaction date,
and the IBAN balance.
"""
try:
first_transaction = page[page.iban == iban].date.min().strftime("%d.%m.%Y")
except: # noqa: E722
first_transaction = f"{i18n.t('general.none'):>10}"
try:
last_transaction = page[page.iban == iban].date.max().strftime("%d.%m.%Y")
except: # noqa: E722
last_transaction = f"{i18n.t('general.none'):>10}"
try:
value = Money.m(page[page.iban == iban].cents.sum() / 100.0)
except: # noqa: E722
value = Money.m(0.0)
# Print IBAN statistics
print(f" {i18n.t('general.no_tracts'):<30}{len(page[page.iban == iban]):>10}")
print(f" {i18n.t('general.first_transaction'):<30}{first_transaction}")
print(f" {i18n.t('general.last_transaction'):<30}{last_transaction}")
print(f" {i18n.t('general.fin_bal'):<30}{value}\n")
return value
show_income_per_interval(amounts)
Create a Pretty Table of an "amounts table".
See the documentation for an explanation of an "amounts table".
Source code in src/bankr/tuitable.py
def show_income_per_interval(amounts: pd.DataFrame) -> None:
"""Create a Pretty Table of an "amounts table".
See the documentation for an explanation of an "amounts table".
"""
# Header
cols = list(amounts)
cols.remove("internal")
amounts = amounts.drop(labels="internal", axis=1)
pnames = [i18n.t("general.perd")]
for col in cols:
pnames.append(i18n.t("cats." + col)[:3])
ptable: str = PrettyTable(field_names=pnames)
ptable.align = "r"
# Rows
index = list(amounts.index)
index[-1] = i18n.t("general.total")
for row in range(amounts.shape[0]):
ptable.add_row([index[row]] + [round(value) for value in amounts.iloc[row, :].to_list()])
print(ptable) # TODO Add legend
show_page(book, year, month, cat, iban)
Show a table of Transactions or a Page of the Book.
The arguments year and month limit the selected Transactions (0 is unlimited).
The selection can also be limited to a category or an IBAN ("" is unlimited).
The number of Transactions must not exceed 200.
Source code in src/bankr/tuitable.py
def show_page(book: pd.DataFrame, year: int, month: int, cat: str, iban: str) -> str:
"""Show a table of Transactions or a Page of the Book.
The arguments `year` and `month` limit the selected Transactions (`0` is unlimited).
The selection can also be limited to a category or an IBAN (`""` is unlimited).
The number of Transactions must not exceed 200.
"""
book["year"] = book["date"].dt.year
book["month"] = book["date"].dt.month
page = book
if year != 0:
page = page[page["year"] == year]
print(f"{i18n.t('general.year'):<15} {year}")
if month != 0:
page = page[page["month"] == month]
print(f"{i18n.t('general.month'):<15} {month:02d}")
if cat:
page = page[page["cat"] == cat]
print(f"{i18n.t('general.cat'):<15} {cat}")
if iban:
page = page[page["iban"] == iban]
print(f"{i18n.t('general.iban'):<15} {iban}")
tracts: int = page.shape[0]
print(f"{i18n.t('general.transactions'):<15} {tracts}")
if tracts > 200:
return "overflow" # To much Transactions selected
# Headline of Transaction table
pnum = "#"
pdate = i18n.t("general.date")
pamount = i18n.t("general.amount")
pcat = i18n.t("general.cat")
poffset = i18n.t("general.offset")
pname = i18n.t("general.name")
field_names = [pnum, pdate, pamount, pcat, poffset, pname]
if iban:
pprocess = i18n.t("general.process")
field_names.extend([pprocess])
else:
piban = i18n.t("general.iban")
field_names[2:2] = [piban]
ppage: str = PrettyTable(field_names=field_names)
ppage.align = "l"
ppage.align[pnum] = "r"
# Transactions table
for n in range(0, tracts):
pnum = n + 1
pdate = page["date"].iloc[n].strftime("%d.%m.%Y")
pamount = f"{Money.m(page['cents'].iloc[n] / 100, page['curr'].iloc[n])}"
pcat = f"{i18n.t('cats.' + page['cat'].iloc[n])}"
poffset = page["offset"].iloc[n]
pname = str(page["name"].iloc[n])[:20]
field_names = [pnum, pdate, pamount, pcat, poffset, pname]
if iban:
pprocess = str(page["process"].iloc[n])[:20]
field_names.extend([pprocess])
else:
piban = page["iban"].iloc[n]
field_names[2:2] = [piban]
ppage.add_row(field_names)
print(ppage)
# Transaction details
while True:
try:
selection = input(f"{i18n.t('general.tractdetails')} ")
if selection == "q":
return "quit" # Quit
selint = int(selection)
if selint < 1 or selint > tracts:
raise ValueError
break
except ValueError:
continue
return page.index[selint - 1]
show_transaction(book, uuid, details)
Show a Transaction, given by its UUID, in tabular form.
The data source is the Book, a Page or a comparable data frame.
Source code in src/bankr/tuitable.py
def show_transaction(book: pd.DataFrame, uuid: str, details: bool) -> None:
"""Show a Transaction, given by its UUID, in tabular form.
The data source is the Book, a Page or a comparable data frame.
"""
try:
# Hint: This works only, if
# (1) the uuid is the index of the book, and
# (2) the uuid is of dtype string or category, not object.
tract = book.loc[uuid]
except KeyError:
sys.exit(f"Bankr Error - Transaction {uuid} not found.")
print("-" * 120)
print(f"{i18n.t('general.uuid'):<20}{uuid}")
print("-" * 120)
print(f"{i18n.t('general.date'):<20}{tract['date'].strftime('%d.%m.%Y')}")
print(f"{i18n.t('general.iban'):<20}{tract['iban']}")
print(f"{i18n.t('general.amount'):<20}{Money.m(tract['cents'] / 100, tract['curr'])}")
print(f"{i18n.t('general.cat'):<20}{i18n.t('cats.' + tract['cat'])}")
if pd.notna(tract["valuta"]):
print(f"{i18n.t('general.valuta'):<20}{tract['valuta'].strftime('%d.%m.%Y')}")
if details:
print(f"{i18n.t('general.offset'):<20}{tract['offset']}")
print(f"{i18n.t('general.name'):<20}{tract['name']}")
print(f"{i18n.t('general.process'):<20}{tract['process']}")
print(f"{i18n.t('general.desc'):<20}{tract['desc'][:100]}")
if details:
print(f"{i18n.t('general.creditor'):<20}{tract['creditor']}")
print(f"{i18n.t('general.mandate'):<20}{tract['mandate']}")
print(f"{i18n.t('general.customer'):<20}{tract['customer']}")
print(f"{i18n.t('general.source'):<20}{tract['source']}")
print("-" * 120)
Bankr module for plot presentation on TUI.
plot_cat_per_interval(amounts, plot_title)
Stacked bar plot of the time intervals.
Source code in src/bankr/tuiplot.py
def plot_cat_per_interval(amounts: pd.DataFrame, plot_title: str) -> None:
"""Stacked bar plot of the time intervals."""
cats = list(amounts.columns)
time = list(amounts.index)
labels = []
values = []
for n in range(amounts.shape[1]):
values.append(list(amounts[cats[n]]))
labels.append(i18n.t("cats." + cats[n]))
plt.stacked_bar(time, values, label=labels)
plt.theme("pro")
plt.hline(0, color="white")
plt.title(plot_title)
plt.plot_size(width=None, height=plt.th() / 2)
plt.xlabel(i18n.t("general.date"))
plt.ylabel(i18n.t("general.amount"))
plt.show()