generated from felixm/defaultpy
Implement fuzzy category input
This commit is contained in:
126
src/fzf.py
Normal file
126
src/fzf.py
Normal 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
|
||||
@@ -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
26
src/predict.py
Normal 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}'.")
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user