Implement fuzzy category input

This commit is contained in:
2023-06-27 17:29:07 +02:00
parent ba0c906e3c
commit 3622ceb7ca
4 changed files with 159 additions and 1 deletions

126
src/fzf.py Normal file
View File

@@ -0,0 +1,126 @@
from __future__ import print_function
import errno
import os.path
import subprocess
import sys
from pkg_resources import resource_exists, resource_filename
__all__ = 'BUNDLED_EXECUTABLE', 'iterfzf'
EXECUTABLE_NAME = 'fzf.exe' if sys.platform == 'win32' else 'fzf'
BUNDLED_EXECUTABLE = (
resource_filename(__name__, EXECUTABLE_NAME)
if resource_exists(__name__, EXECUTABLE_NAME)
else (
os.path.join(os.path.dirname(__file__), EXECUTABLE_NAME)
if os.path.isfile(
os.path.join(os.path.dirname(__file__), EXECUTABLE_NAME)
)
else None
)
)
def iterfzf(
# CHECK: When the signature changes, __init__.pyi file should also change.
iterable,
# Search mode:
extended=True, exact=False, case_sensitive=None,
# Interface:
multi=False, mouse=True, print_query=False,
# Layout:
prompt='> ',
ansi=None,
preview=None,
# Misc:
query='', encoding=None, executable=BUNDLED_EXECUTABLE or EXECUTABLE_NAME
):
cmd = [executable, '--no-sort', '--prompt=' + prompt]
cmd = [executable, '--prompt=' + prompt]
if not extended:
cmd.append('--no-extended')
if case_sensitive is not None:
cmd.append('+i' if case_sensitive else '-i')
if exact:
cmd.append('--exact')
if multi:
cmd.append('--multi')
if not mouse:
cmd.append('--no-mouse')
if print_query:
cmd.append('--print-query')
if query:
cmd.append('--query=' + query)
if preview:
cmd.append('--preview=' + preview)
if ansi:
cmd.append('--ansi')
encoding = encoding or sys.getdefaultencoding()
proc = None
stdin = None
byte = None
lf = u'\n'
cr = u'\r'
for line in iterable:
if byte is None:
byte = isinstance(line, bytes)
if byte:
lf = b'\n'
cr = b'\r'
elif isinstance(line, bytes) is not byte:
raise ValueError(
'element values must be all byte strings or all '
'unicode strings, not mixed of them: ' + repr(line)
)
if lf in line or cr in line:
raise ValueError(r"element values must not contain CR({1!r})/"
r"LF({2!r}): {0!r}".format(line, cr, lf))
if proc is None:
proc = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=None
)
stdin = proc.stdin
if not byte:
line = line.encode(encoding)
try:
stdin.write(line + b'\n')
stdin.flush()
except IOError as e:
if e.errno != errno.EPIPE and errno.EPIPE != 32:
raise
break
stdin.close()
if proc is None or proc.wait() not in [0, 1]:
if print_query:
return None, None
else:
return None
try:
stdin.close()
except IOError as e:
if e.errno != errno.EPIPE and errno.EPIPE != 32:
raise
stdout = proc.stdout
decode = (lambda b: b) if byte else (lambda t: t.decode(encoding))
output = [decode(ln.strip(b'\r\n\0')) for ln in iter(stdout.readline, b'')]
if print_query:
try:
if multi:
return output[0], output[1:]
else:
return output[0], output[1]
except IndexError:
return output[0], None
else:
if multi:
return output
else:
try:
return output[0]
except IndexError:
return None

View File

@@ -4,6 +4,9 @@ from pathlib import Path
from typing import List
UNKNOWN_CATEGORY = 'account2'
class CsvConfig(BaseModel):
"""
Class to define how to parse a certain CSV file. We use the

26
src/predict.py Normal file
View File

@@ -0,0 +1,26 @@
from src.models import Transaction, UNKNOWN_CATEGORY
from src.fzf import iterfzf
from typing import List
def get_categories(transactions: List[Transaction]) -> List[str]:
categories = set([t.account2 for t in transactions])
categories.discard(UNKNOWN_CATEGORY)
return list(categories)
def add_account2(transactions: List[Transaction]):
categories = get_categories(transactions)
unmapped_transactions = filter(lambda t: t.account2 == UNKNOWN_CATEGORY, transactions)
for t in unmapped_transactions:
add_account2_interactive(t, categories)
def add_account2_interactive(transaction: Transaction, categories: List[str]):
t = transaction
account2 = None
prompt = f"{t.account1} {t.date} {t.description} {t.debit} > "
while account2 is None:
account2 = iterfzf(categories, prompt=prompt)
transaction.account2 = account2
print(f"Assigned category '{account2}'.")

View File

@@ -5,6 +5,8 @@ import sys
import datetime
import src.utils
import src.write
import src.models
import src.predict
from src.models import Config, CsvConfig, Transaction
from typing import List, Dict
@@ -47,7 +49,7 @@ def get_transactions(csv_file: str, config: CsvConfig) -> List[Transaction]:
credit=amount,
date=date_to_date(t['date']),
account1=config.account1,
account2="account2",
account2=src.models.UNKNOWN_CATEGORY,
description=t['description'],
csv_file=csv_file,
row=csv_file + ", " + ", ".join(row))
@@ -97,6 +99,7 @@ def process_csv_files(config: Config):
find_duplicates(transactions)
mappings = src.utils.read_mappings(config.mappings_file)
apply_mappings(transactions, mappings)
src.predict.add_account2(transactions)
src.utils.write_mappings(transactions, config.mappings_file)
src.write.render_to_file(transactions, config.output_file)